I need a self-contained abstraction for debouncing a function call. While deno/async provides such a function, AFAICT there is no way to cancel an already running function (for example, to kill a child process). So, a few days ago, I attempted to implement this requirement myself:
function debounced<T>(
fn: (signal: AbortSignal) => Promise<T>,
ms: number,
): () => Promise<T> {
let controller: AbortController | null = null;
return async () => {
controller?.abort();
controller = new AbortController();
const signal = controller.signal;
await delay(ms, { signal });
return await fn(signal);
};
}
This test should demonstrate the expected behavior:
Deno.test(async function debouncedAbortsRunningFunctions() {
// Arrange
using time = new FakeTime();
const enter = spy();
const exit = spy();
const fn = debounced(async (signal) => {
enter();
// simulate a long running function
await delay(100, { signal });
exit();
}, 100); // 100ms delay before calling the function
// Act
const p1 = fn();
time.tick(50);
const p2 = fn();
time.tick(150);
const p3 = fn();
time.tick(1000);
const actual = await Promise.allSettled([p1, p2, p3]);
// Assert
assertEquals(actual[0].status, "rejected");
assertEquals(actual[1].status, "rejected");
assertEquals(actual[2].status, "fulfilled");
assertSpyCalls(enter, 2); // 2 because the first call was aborted during the delay
assertSpyCalls(exit, 1); // 1 because the second call was aborted mid-execution
});
However, I'm getting error: Promise resolution is still pending but the event loop has already resolved.
Running equivalent code in NodeJs gives (uncaught error) error: (in promise) AbortError: The signal has been aborted
.
I assume that suggests that somhow an unbound/unawaited promise escapes to be a "background task", so the AbortError exception raises to the event loop. I just don't see how and where that could happen.
At this point I have tried numerous implementations of debounced()
, using new Promise(...)
and whatnot.
I am lost. Any help is appreciated!
Your test does indeed reject p1
long before it is await
ed (through Promise.allSettled
), that is where the unhandled rejection is coming from.
I'm not that familiar with deno/testing, but I would suggest you try
// act
const p1 = fn();
const p2 = delay(50).then(fn);
const p3 = delay(200).then(fn);
const promise = Promise.allSettled([p1, p2, p3]);
time.tick(1200);
const actual = await promise;
where no rejection should be able to escape the handling of Promise.allSettled
(unless there is a synchronous exception between the promise construction and the allSettled
call).