# HG changeset patch # User cin # Date 2021-04-12 00:01:37 # Node ID 4031b379ac689e410562910b4445c51dd95e9493 # Parent 563928fdf335b3f2b51129005510ed9507d962aa Rewritten safe::debounce diff --git a/src/main/ts/safe.ts b/src/main/ts/safe.ts --- a/src/main/ts/safe.ts +++ b/src/main/ts/safe.ts @@ -1,4 +1,5 @@ -import { ICancellable, Constructor, IDestroyable, ICancellation, IRemovable } from "./interfaces"; +import { Cancellation } from "./Cancellation"; +import { ICancellable, Constructor, IDestroyable, ICancellation, IRemovable, PromiseOrValue } from "./interfaces"; let _nextOid = 0; const _oid = typeof Symbol === "function" ? @@ -311,37 +312,36 @@ export function delay(timeMs: number, ct }); } -export function debounce(func: (this: This, ...args: T) => R | PromiseLike, wait: number) { +/** + * Wraps the specified function to delay its execution by the specified timeout. + * If the wrapped function was called multiple times before the timeout has elapsed, + * the target function will be executed only once. + * Each call to the wrapped function will return a promise is the timeout hasn't + * elapsed the subsequent calls to the wrapped function will reject previous promises. + * + * @param func function that accepts a cancellation token as a single + * parameter and returns the callback which will be invoked. + * @param wait A timeout to wait before the callback should be invoked + * @returns The function withe the same parameters as the original callback. + */ +export function debounce(func: (ct: ICancellation) => ((this: This, ...args: T) => PromiseOrValue), wait: number) { let cancel: (e?: any) => void = _noop; - const fn = function executedFunction(this: This, ...args: T) { - return new Promise((resolve, reject) => { + let pending = Promise.resolve(); - // used to cleanup currently allocated resources - const _cleanup = () => { - cancel = _noop; - clearTimeout(handle); - }; + const fn = async function executedFunction(this: This, ...args: T) { + cancel(); + const ct = new Cancellation(_cancel => cancel = _cancel); - // used in case of cancellation of the current operation - const _cancel = (e: any) => { - _cleanup(); - reject(e); - }; - - // performs actual work - const _later = () => { - _cleanup(); - resolve(func.apply(this, args)); - }; - - // cancel previously queued operation - if (cancel !== _noop) - cancel(new Error("Operation cancelled due to debouncing")); - cancel = _cancel; - - const handle = setTimeout(_later, wait); - }); + await pending; + let resolve = _noop; + pending = new Promise(_resolve => resolve = _resolve); + try { + await delay(wait, ct); + return func(ct).apply(this, args); + } finally { + resolve(); + } }; fn.cancel = (e?: any) => cancel(e); diff --git a/src/test/ts/tests/ContainerTests.ts b/src/test/ts/tests/ContainerTests.ts --- a/src/test/ts/tests/ContainerTests.ts +++ b/src/test/ts/tests/ContainerTests.ts @@ -109,17 +109,19 @@ test("Load configuration from module", a test("Optional dependency with child container", async t => { const container = new Container<{ foo?: Foo; - box: Box; + box: Box; + bar: Bar; }>(); await container.fluent({ - box: it => it.factory($ => new Box($("foo"))) + box: it => it.factory($ => new Box($("bar"))), + bar: it => it.factory($ => new Bar({ host: "local", foo: $("foo") }, "bar")) }); const child = await container.createChildContainer() .fluent({ foo: it => it.factory(() => new Foo()) - }) + }); const box = child.resolve("box"); t.assert(!isNull(box.getValue()), "'foo' dependency is declared in child container"); -}); \ No newline at end of file +}); diff --git a/src/test/ts/tests/SafeTests.ts b/src/test/ts/tests/SafeTests.ts --- a/src/test/ts/tests/SafeTests.ts +++ b/src/test/ts/tests/SafeTests.ts @@ -103,7 +103,7 @@ test("debounce tests", async (t, trace) return count; } - const f = debounce(increment, 100); + const f = debounce(() => increment, 100); f().then(undefined, () => rejected++); f().then(undefined, () => rejected++); @@ -113,7 +113,7 @@ test("debounce tests", async (t, trace) t.equal(count, 1, "The operation should run once"); const acc = debounce( - (...values: number[]) => count = values.reduce((a, v) => v + a, count), + () => (...values: number[]) => count = values.reduce((a, v) => v + a, count), 100 ); @@ -138,15 +138,15 @@ test("debounce tests", async (t, trace) let cancel: (e?: any) => void = notImplemented; const ct = new Cancellation(c => cancel = c); - const d = debounce(async () => { - ct.throwIfRequested(); + const d = debounce(() => async (_ct: ICancellation) => { + _ct.throwIfRequested(); trace.debug("do async increment"); await fork(); count++; return count; }, 0); - const p = d().then(undefined, () => rejected++); + const p = d(ct).then(undefined, () => rejected++); cancel(); await p;