From 1935db10192051401ab00ca2cd767955d0d3b866 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Sat, 13 Mar 2021 12:14:44 +0100 Subject: [PATCH] fix throttle function not being able to run with argument(s) --- .../components/Shared/Throttle.ts | 56 +++++++++----- .../components/Shared/Throttle.spec.ts | 76 +++++++++++++++++-- 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/src/presentation/components/Shared/Throttle.ts b/src/presentation/components/Shared/Throttle.ts index 0103e435..3dbcfcd0 100644 --- a/src/presentation/components/Shared/Throttle.ts +++ b/src/presentation/components/Shared/Throttle.ts @@ -1,20 +1,10 @@ -export function throttle( - callback: (..._: T) => void, wait: number, - timer: ITimer = NodeTimer): (..._: T) => void { - let queuedToRun: ReturnType; - let previouslyRun: number; - return function invokeFn(...args: T) { - const now = timer.dateNow(); - if (queuedToRun) { - queuedToRun = timer.clearTimeout(queuedToRun) as undefined; - } - if (!previouslyRun || (now - previouslyRun >= wait)) { - callback(...args); - previouslyRun = now; - } else { - queuedToRun = timer.setTimeout(invokeFn.bind(null, ...args), wait - (now - previouslyRun)); - } - }; +export type CallbackType = (..._: any[]) => void; + +export function throttle( + callback: CallbackType, waitInMs: number, + timer: ITimer = NodeTimer): CallbackType { + const throttler = new Throttler(timer, waitInMs, callback); + return (...args: any[]) => throttler.invoke(...args); } export interface ITimer { @@ -28,3 +18,35 @@ const NodeTimer: ITimer = { clearTimeout: (timeoutId) => clearTimeout(timeoutId), dateNow: () => Date.now(), }; + +interface IThrottler { + invoke: CallbackType; +} + +class Throttler implements IThrottler { + private queuedToRun: ReturnType; + private previouslyRun: number; + constructor( + private readonly timer: ITimer, + private readonly waitInMs: number, + private readonly callback: CallbackType) { + if (!timer) { throw new Error('undefined timer'); } + if (!waitInMs) { throw new Error('no delay to throttle'); } + if (waitInMs < 0) { throw new Error('negative delay'); } + if (!callback) { throw new Error('undefined callback'); } + } + public invoke(...args: any[]): void { + const now = this.timer.dateNow(); + if (this.queuedToRun) { + this.queuedToRun = this.timer.clearTimeout(this.queuedToRun) as undefined; + } + if (!this.previouslyRun || (now - this.previouslyRun >= this.waitInMs)) { + this.callback(...args); + this.previouslyRun = now; + } else { + const nextCall = () => this.invoke(...args); + const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun); + this.queuedToRun = this.timer.setTimeout(nextCall, nextCallDelayInMs); + } + } +} diff --git a/tests/unit/presentation/components/Shared/Throttle.spec.ts b/tests/unit/presentation/components/Shared/Throttle.spec.ts index ccc77a8f..f992164c 100644 --- a/tests/unit/presentation/components/Shared/Throttle.spec.ts +++ b/tests/unit/presentation/components/Shared/Throttle.spec.ts @@ -5,6 +5,32 @@ import { EventSource } from '@/infrastructure/Events/EventSource'; import { IEventSubscription } from '@/infrastructure/Events/IEventSource'; describe('throttle', () => { + it('throws if callback is undefined', () => { + // arrange + const expectedError = 'undefined callback'; + const callback = undefined; + // act + const act = () => throttle(callback, 500); + // assert + expect(act).to.throw(expectedError); + }); + describe('throws if waitInMs is negative or zero', () => { + // arrange + const testCases = [ + { value: 0, expectedError: 'no delay to throttle' }, + { value: -2, expectedError: 'negative delay' }, + ]; + const callback = () => { return; }; + for (const testCase of testCases) { + it(`"${testCase.value}" throws "${testCase.expectedError}"`, () => { + // act + const waitInMs = testCase.value; + const act = () => throttle(callback, waitInMs); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); it('should call the callback immediately', () => { // arrange const timer = new TimerMock(); @@ -21,16 +47,17 @@ describe('throttle', () => { const timer = new TimerMock(); let totalRuns = 0; const callback = () => totalRuns++; - const throttleFunc = throttle(callback, 500, timer); + const waitInMs = 500; + const throttleFunc = throttle(callback, waitInMs, timer); // act throttleFunc(); - totalRuns--; + totalRuns--; // So we don't count the initial run throttleFunc(); - timer.tick(500); + timer.tickNext(waitInMs); // assert expect(totalRuns).to.equal(1); }); - it('calls the callback at most once at given time', () => { + it('should call the callback at most once at given time', () => { // arrange const timer = new TimerMock(); let totalRuns = 0; @@ -40,11 +67,43 @@ describe('throttle', () => { const throttleFunc = throttle(callback, waitInMs, timer); // act for (let i = 0; i < totalCalls; i++) { - timer.tick(waitInMs / totalCalls * i); + timer.setCurrentTime(waitInMs / totalCalls * i); throttleFunc(); } // assert - expect(totalRuns).to.equal(2); // initial and at the end + expect(totalRuns).to.equal(2); // one initial and one at the end + }); + it('should call the callback as long as delay is waited', () => { + // arrange + const timer = new TimerMock(); + let totalRuns = 0; + const callback = () => totalRuns++; + const waitInMs = 500; + const expectedTotalRuns = 10; + const throttleFunc = throttle(callback, waitInMs, timer); + // act + for (let i = 0; i < expectedTotalRuns; i++) { + throttleFunc(); + timer.tickNext(waitInMs); + } + // assert + expect(totalRuns).to.equal(expectedTotalRuns); + }); + it('should call arguments as expected', () => { + // arrange + const timer = new TimerMock(); + const expected = [ 1, 2, 3 ]; + const actual = new Array(); + const callback = (arg: number) => { actual.push(arg); }; + const waitInMs = 500; + const throttleFunc = throttle(callback, waitInMs, timer); + // act + for (const arg of expected) { + throttleFunc(arg); + timer.tickNext(waitInMs); + } + // assert + expect(expected).to.deep.equal(actual); }); }); @@ -69,7 +128,10 @@ class TimerMock implements ITimer { public dateNow(): number { return this.currentTime; } - public tick(ms: number): void { + public tickNext(ms: number): void { + this.setCurrentTime(this.currentTime + ms); + } + public setCurrentTime(ms: number): void { this.currentTime = ms; this.timeChanged.notify(this.currentTime); }