mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
util: implement debounce
I found myself using debounce quite a bit recently while testing some recent other additions (quic and dtls testing, perf_hooks improvements, etc). I was using an npm dependency right up until I realized just how generally useful it is to actually have it Just There. So, since it was a holiday and I just felt like it... util.debounce(...) Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/65899 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
1 parent
312db1e3d2
commit
27cdfbe13b
4 files changed
+725
No files matched your search
@@ -387,6 +387,104 @@ The `--throw-deprecation` command-line flag and `process.throwDeprecation`
|
||||
property take precedence over `--trace-deprecation` and
|
||||
`process.traceDeprecation`.
|
||||
|
||||
## `util.debounce(fn, wait[, options])`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `fn` {Function} The function to debounce.
|
||||
* `wait` {integer} The number of milliseconds to delay `fn`.
|
||||
* `options` {Object}
|
||||
* `leading` {boolean} When `true`, invokes `fn` immediately when a new
|
||||
debounce window begins. **Default:** `false`.
|
||||
* `rejectOnCancel` {boolean} When `true`, a call superseded by a later call
|
||||
rejects with an `AbortError`. **Default:** `false`.
|
||||
* `signal` {AbortSignal} An `AbortSignal` that cancels pending calls and
|
||||
prevents future calls when aborted.
|
||||
* Returns: {Function} The debounced function.
|
||||
|
||||
Creates a function that delays calling `fn` until `wait` milliseconds have
|
||||
elapsed since the most recent invocation. The debounced function returns a
|
||||
{Promise} for the value returned by `fn`. If `fn` throws or returns a rejected
|
||||
promise, the returned promise is rejected with the same reason.
|
||||
|
||||
When the debounced function is called more than once before the delay expires,
|
||||
`fn` receives the arguments from the most recent call. By default, the promises
|
||||
from all calls resolve or reject with the result of that invocation. If
|
||||
`options.rejectOnCancel` is `true`, the promises from superseded calls reject
|
||||
with an `AbortError` instead.
|
||||
|
||||
When `options.leading` is `true`, the first call in a debounce window invokes
|
||||
`fn` immediately. Calls made during that window are delayed until `wait`
|
||||
milliseconds have elapsed since the most recent call. A trailing invocation
|
||||
only occurs if the debounced function was called again during the window.
|
||||
The window begins before `fn` is invoked, so recursive calls and calls made
|
||||
while an asynchronous `fn` is pending are part of the same window if they occur
|
||||
before the delay expires. This also applies to calls made after a synchronous
|
||||
`fn` returns but before the delay expires.
|
||||
|
||||
If `options.signal` is aborted, pending and future calls reject with an
|
||||
`AbortError`, with the signal's reason set as the error's `cause`, and `fn` is
|
||||
not invoked by those calls. If the signal is already aborted, `debounce()`
|
||||
throws an `AbortError`.
|
||||
|
||||
The returned function has the following properties:
|
||||
|
||||
* `cancel([reason])` cancels the current debounce window. Its pending promises
|
||||
reject with an `AbortError`. If provided, `reason` is set as the error's
|
||||
`cause`.
|
||||
* `flush()` cancels the delay and invokes `fn` immediately. It has no effect if
|
||||
no invocation is pending.
|
||||
* `pending` {Promise|null} is the promise returned by the most recent call in
|
||||
the current debounce window, or `null` if no invocation is pending.
|
||||
* `pendingCount` {integer} is the number of calls awaiting the invocation in
|
||||
the current debounce window.
|
||||
* `ref()` makes the pending and future timeout keep the Node.js event loop
|
||||
active. Returns the debounced function.
|
||||
* `unref()` allows the event loop to exit while a timeout is pending. This also
|
||||
applies to future timeouts. Returns the debounced function.
|
||||
|
||||
When invoked, `fn` has the debounced function as its `this` value. After a
|
||||
trailing invocation, a new debounce window can begin even if a promise returned
|
||||
by `fn` is still pending. The debounced function preserves the `name` and
|
||||
`length` of `fn`.
|
||||
|
||||
```mjs
|
||||
import { setTimeout as wait } from 'node:timers/promises';
|
||||
import { debounce } from 'node:util';
|
||||
|
||||
const fn = debounce(async (value) => {
|
||||
await wait(100);
|
||||
return value;
|
||||
}, 50);
|
||||
|
||||
const first = fn(1);
|
||||
const second = fn(2);
|
||||
|
||||
console.log(await first); // 2
|
||||
console.log(await second); // 2
|
||||
```
|
||||
|
||||
A debounced function can be used to trigger an action after a period of
|
||||
inactivity. Each call resets the timeout:
|
||||
|
||||
```cjs
|
||||
const { debounce } = require('node:util');
|
||||
|
||||
const onInactivity = debounce(() => {
|
||||
console.log('No activity for 5 seconds');
|
||||
}, 5_000).unref();
|
||||
|
||||
process.stdin.on('data', (data) => {
|
||||
console.log(`Received ${data.length} bytes`);
|
||||
onInactivity();
|
||||
});
|
||||
|
||||
// Start the initial inactivity timeout.
|
||||
onInactivity();
|
||||
```
|
||||
|
||||
## `util.diff(actual, expected)`
|
||||
|
||||
<!-- YAML
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypePush,
|
||||
ObjectDefineProperties,
|
||||
PromiseReject,
|
||||
PromiseWithResolvers,
|
||||
ReflectApply,
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
AbortError,
|
||||
} = require('internal/errors');
|
||||
const {
|
||||
validateAbortSignal,
|
||||
validateBoolean,
|
||||
validateFunction,
|
||||
validateInteger,
|
||||
validateObject,
|
||||
} = require('internal/validators');
|
||||
const { addAbortListener } = require('internal/events/abort_listener');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const { TIMEOUT_MAX } = require('internal/timers');
|
||||
const { clearTimeout, setTimeout } = require('timers');
|
||||
const { markPromiseAsHandled } = internalBinding('util');
|
||||
|
||||
/**
|
||||
* @typedef {object} DebounceOptions
|
||||
* @property {AbortSignal} [signal] An AbortSignal that cancels pending and future calls.
|
||||
* @property {boolean} [leading] Whether to invoke on the leading edge.
|
||||
* @property {boolean} [rejectOnCancel] Whether to reject superseded calls.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a function that delays calling `fn` until `wait` milliseconds have
|
||||
* elapsed since its most recent invocation.
|
||||
* @param {Function} fn
|
||||
* @param {number} wait
|
||||
* @param {DebounceOptions} [options]
|
||||
* @returns {Function}
|
||||
*/
|
||||
function debounce(fn, wait, options = kEmptyObject) {
|
||||
validateFunction(fn, 'fn');
|
||||
validateInteger(wait, 'wait', 0, TIMEOUT_MAX);
|
||||
validateObject(options, 'options');
|
||||
|
||||
const {
|
||||
leading = false,
|
||||
rejectOnCancel = false,
|
||||
signal,
|
||||
} = options;
|
||||
|
||||
validateBoolean(leading, 'options.leading');
|
||||
validateBoolean(rejectOnCancel, 'options.rejectOnCancel');
|
||||
validateAbortSignal(signal, 'options.signal');
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new AbortError(undefined, { __proto__: null, cause: signal.reason });
|
||||
}
|
||||
|
||||
let args;
|
||||
let abortError;
|
||||
let pendingCalls = [];
|
||||
let refed = true;
|
||||
let timeout;
|
||||
|
||||
function rejectPending(error) {
|
||||
const calls = pendingCalls;
|
||||
pendingCalls = [];
|
||||
for (let i = 0; i < calls.length; i++) {
|
||||
calls[i].reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelWithError(error) {
|
||||
if (timeout === undefined) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
args = undefined;
|
||||
rejectPending(error);
|
||||
}
|
||||
|
||||
function cancel(reason) {
|
||||
if (timeout === undefined) return;
|
||||
const error = reason === undefined ?
|
||||
new AbortError() :
|
||||
new AbortError(undefined, { __proto__: null, cause: reason });
|
||||
cancelWithError(error);
|
||||
}
|
||||
|
||||
function abort() {
|
||||
if (abortError === undefined) {
|
||||
abortError = signal.reason === undefined ?
|
||||
new AbortError() :
|
||||
new AbortError(undefined, { __proto__: null, cause: signal.reason });
|
||||
}
|
||||
cancelWithError(abortError);
|
||||
}
|
||||
|
||||
function ref() {
|
||||
refed = true;
|
||||
timeout?.ref();
|
||||
return debounced;
|
||||
}
|
||||
|
||||
function unref() {
|
||||
refed = false;
|
||||
timeout?.unref();
|
||||
return debounced;
|
||||
}
|
||||
|
||||
function invoke(callArgs, calls) {
|
||||
if (signal?.aborted) {
|
||||
abort();
|
||||
for (let i = 0; i < calls.length; i++) {
|
||||
calls[i].reject(abortError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = ReflectApply(fn, debounced, callArgs);
|
||||
} catch (error) {
|
||||
for (let i = 0; i < calls.length; i++) {
|
||||
calls[i].reject(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < calls.length; i++) {
|
||||
calls[i].resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
function invokePending() {
|
||||
const callArgs = args;
|
||||
args = undefined;
|
||||
const calls = pendingCalls;
|
||||
pendingCalls = [];
|
||||
if (calls.length !== 0) invoke(callArgs, calls);
|
||||
}
|
||||
|
||||
function onTimeout() {
|
||||
timeout = undefined;
|
||||
invokePending();
|
||||
}
|
||||
|
||||
function flush() {
|
||||
if (pendingCalls.length === 0) return;
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
invokePending();
|
||||
}
|
||||
|
||||
function debounced(...callArgs) {
|
||||
if (signal?.aborted) abort();
|
||||
if (abortError !== undefined) return PromiseReject(abortError);
|
||||
|
||||
const invokeNow = leading && timeout === undefined;
|
||||
if (timeout !== undefined) {
|
||||
if (pendingCalls.length !== 0) {
|
||||
markPromiseAsHandled(pendingCalls[pendingCalls.length - 1].promise);
|
||||
if (rejectOnCancel) {
|
||||
rejectPending(new AbortError('The debounced call was superseded'));
|
||||
}
|
||||
}
|
||||
timeout.refresh();
|
||||
} else {
|
||||
timeout = setTimeout(onTimeout, wait);
|
||||
if (!refed) timeout.unref();
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
abort();
|
||||
return PromiseReject(abortError);
|
||||
}
|
||||
|
||||
const call = PromiseWithResolvers();
|
||||
if (invokeNow) {
|
||||
invoke(callArgs, [call]);
|
||||
} else {
|
||||
ArrayPrototypePush(pendingCalls, call);
|
||||
args = callArgs;
|
||||
}
|
||||
return call.promise;
|
||||
}
|
||||
|
||||
function createFn(value) {
|
||||
return {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
ObjectDefineProperties(debounced, {
|
||||
__proto__: null,
|
||||
cancel: createFn(cancel),
|
||||
flush: createFn(flush),
|
||||
ref: createFn(ref),
|
||||
unref: createFn(unref),
|
||||
pending: {
|
||||
__proto__: null,
|
||||
enumerable: false,
|
||||
get() {
|
||||
return pendingCalls.length === 0 ? null : pendingCalls[pendingCalls.length - 1].promise;
|
||||
},
|
||||
},
|
||||
pendingCount: {
|
||||
__proto__: null,
|
||||
enumerable: false,
|
||||
get() { return pendingCalls.length; },
|
||||
},
|
||||
length: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: fn.length,
|
||||
},
|
||||
name: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: fn.name,
|
||||
},
|
||||
});
|
||||
|
||||
if (signal !== undefined) {
|
||||
addAbortListener(signal, abort);
|
||||
}
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
||||
module.exports = debounce;
|
||||
@@ -668,6 +668,15 @@ defineLazyProperties(
|
||||
['diff'],
|
||||
);
|
||||
|
||||
ObjectDefineProperties(module.exports, {
|
||||
debounce: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() { return require('internal/util/debounce'); },
|
||||
},
|
||||
});
|
||||
|
||||
defineLazyProperties(
|
||||
module.exports,
|
||||
'internal/util/trace_sigint',
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
// Flags: --expose-internals
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('node:assert');
|
||||
const { createHook } = require('node:async_hooks');
|
||||
const { setTimeout } = require('node:timers/promises');
|
||||
const { debounce } = require('node:util');
|
||||
const { TIMEOUT_MAX } = require('internal/timers');
|
||||
|
||||
for (const value of [undefined, null, true, 0, 'fn', {}, [], Symbol()]) {
|
||||
assert.throws(() => debounce(value, 1), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
});
|
||||
}
|
||||
|
||||
for (const value of [undefined, null, true, '1', {}, [], Symbol()]) {
|
||||
assert.throws(() => debounce(() => {}, value), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
});
|
||||
}
|
||||
|
||||
for (const value of [-1, 0.5, NaN, Infinity, TIMEOUT_MAX + 1]) {
|
||||
assert.throws(() => debounce(() => {}, value), {
|
||||
code: 'ERR_OUT_OF_RANGE',
|
||||
});
|
||||
}
|
||||
|
||||
for (const value of [null, true, 1, 'options', []]) {
|
||||
assert.throws(() => debounce(() => {}, 1, value), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
});
|
||||
}
|
||||
|
||||
assert.throws(() => debounce(() => {}, 1, { rejectOnCancel: 1 }), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
});
|
||||
assert.throws(() => debounce(() => {}, 1, { leading: 1 }), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
});
|
||||
assert.throws(() => debounce(() => {}, 1, { signal: {} }), {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
});
|
||||
|
||||
{
|
||||
const reason = new Error('already aborted');
|
||||
assert.throws(
|
||||
() => debounce(() => {}, 1, { signal: AbortSignal.abort(reason) }),
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
{
|
||||
const debounced = debounce(common.mustCall(function(...args) {
|
||||
assert.strictEqual(this, debounced);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
assert.deepStrictEqual(args, ['last', 2]);
|
||||
return args[1];
|
||||
}), 100);
|
||||
|
||||
assert.strictEqual(typeof debounced.cancel, 'function');
|
||||
assert.strictEqual(typeof debounced.flush, 'function');
|
||||
assert.strictEqual(typeof debounced.ref, 'function');
|
||||
assert.strictEqual(typeof debounced.unref, 'function');
|
||||
assert.strictEqual(debounced.unref(), debounced);
|
||||
assert.strictEqual(debounced.ref(), debounced);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
assert.strictEqual(debounced.pendingCount, 0);
|
||||
|
||||
const first = debounced('first', 1);
|
||||
const second = debounced('last', 2);
|
||||
assert(first instanceof Promise);
|
||||
assert(second instanceof Promise);
|
||||
assert.notStrictEqual(first, second);
|
||||
assert.strictEqual(debounced.pending, second);
|
||||
assert.strictEqual(debounced.pendingCount, 2);
|
||||
assert.strictEqual(debounced.unref(), debounced);
|
||||
assert.strictEqual(debounced.ref(), debounced);
|
||||
|
||||
debounced.flush();
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
assert.strictEqual(debounced.pendingCount, 0);
|
||||
debounced.flush();
|
||||
assert.deepStrictEqual(await Promise.all([first, second]), [2, 2]);
|
||||
}
|
||||
|
||||
{
|
||||
const values = [];
|
||||
const debounced = debounce(common.mustCall((value) => {
|
||||
values.push(value);
|
||||
return value;
|
||||
}, 3), 100, { leading: true });
|
||||
|
||||
const first = debounced(1);
|
||||
assert.deepStrictEqual(values, [1]);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
assert.strictEqual(debounced.pendingCount, 0);
|
||||
|
||||
// With no trailing call pending, flush does not end the debounce window.
|
||||
debounced.flush();
|
||||
const second = debounced(2);
|
||||
const third = debounced(3);
|
||||
assert.deepStrictEqual(values, [1]);
|
||||
assert.strictEqual(debounced.pending, third);
|
||||
assert.strictEqual(debounced.pendingCount, 2);
|
||||
|
||||
debounced.flush();
|
||||
assert.deepStrictEqual(values, [1, 3]);
|
||||
assert.deepStrictEqual(
|
||||
await Promise.all([first, second, third]),
|
||||
[1, 3, 3],
|
||||
);
|
||||
|
||||
const fourth = debounced(4);
|
||||
assert.deepStrictEqual(values, [1, 3, 4]);
|
||||
assert.strictEqual(await fourth, 4);
|
||||
debounced.cancel();
|
||||
}
|
||||
|
||||
{
|
||||
const debounced = debounce(common.mustCall((value) => value), 1, {
|
||||
leading: true,
|
||||
});
|
||||
assert.strictEqual(await debounced(42), 42);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
await setTimeout(10);
|
||||
}
|
||||
|
||||
{
|
||||
let recursive;
|
||||
const values = [];
|
||||
const debounced = debounce(common.mustCall((value) => {
|
||||
values.push(value);
|
||||
if (value === 1) recursive = debounced(2);
|
||||
return value;
|
||||
}, 2), 100);
|
||||
|
||||
const first = debounced(1);
|
||||
debounced.flush();
|
||||
assert.deepStrictEqual(values, [1]);
|
||||
assert.strictEqual(debounced.pending, recursive);
|
||||
assert.strictEqual(await first, 1);
|
||||
|
||||
debounced.flush();
|
||||
assert.deepStrictEqual(values, [1, 2]);
|
||||
assert.strictEqual(await recursive, 2);
|
||||
}
|
||||
|
||||
{
|
||||
let recursive;
|
||||
const values = [];
|
||||
const debounced = debounce(common.mustCall((value) => {
|
||||
values.push(value);
|
||||
if (value === 1) recursive = debounced(2);
|
||||
return value;
|
||||
}, 2), 100, { leading: true });
|
||||
|
||||
const first = debounced(1);
|
||||
assert.deepStrictEqual(values, [1]);
|
||||
assert.strictEqual(debounced.pending, recursive);
|
||||
assert.strictEqual(await first, 1);
|
||||
|
||||
debounced.flush();
|
||||
assert.deepStrictEqual(values, [1, 2]);
|
||||
assert.strictEqual(await recursive, 2);
|
||||
}
|
||||
|
||||
{
|
||||
let recursive;
|
||||
const values = [];
|
||||
const debounced = debounce(common.mustCall((value) => {
|
||||
values.push(value);
|
||||
if (value === 2) recursive = debounced(3);
|
||||
return value;
|
||||
}, 3), 100, { leading: true });
|
||||
|
||||
const first = debounced(1);
|
||||
const second = debounced(2);
|
||||
debounced.flush();
|
||||
assert.deepStrictEqual(values, [1, 2, 3]);
|
||||
assert.deepStrictEqual(
|
||||
await Promise.all([first, second, recursive]),
|
||||
[1, 2, 3],
|
||||
);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
debounced.cancel();
|
||||
}
|
||||
|
||||
{
|
||||
const debounced = debounce(common.mustCall(async (value) => {
|
||||
await Promise.resolve();
|
||||
return value;
|
||||
}), 100);
|
||||
const result = debounced(42);
|
||||
debounced.flush();
|
||||
assert.strictEqual(await result, 42);
|
||||
}
|
||||
|
||||
{
|
||||
const expected = new Error('failure');
|
||||
const debounced = debounce(common.mustCall(() => { throw expected; }), 100);
|
||||
const result = debounced();
|
||||
debounced.flush();
|
||||
await assert.rejects(result, (error) => error === expected);
|
||||
}
|
||||
|
||||
{
|
||||
const debounced = debounce(common.mustNotCall(), 100);
|
||||
const first = debounced();
|
||||
const second = debounced();
|
||||
const reason = new Error('cancelled');
|
||||
debounced.cancel(reason);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
debounced.cancel();
|
||||
await assert.rejects(
|
||||
first,
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
await assert.rejects(
|
||||
second,
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const debounced = debounce(common.mustCall((value) => value), 100, {
|
||||
rejectOnCancel: true,
|
||||
});
|
||||
const first = debounced(1);
|
||||
const firstRejection = assert.rejects(first, {
|
||||
code: 'ABORT_ERR',
|
||||
name: 'AbortError',
|
||||
});
|
||||
const second = debounced(2);
|
||||
assert.strictEqual(debounced.pendingCount, 1);
|
||||
await firstRejection;
|
||||
debounced.flush();
|
||||
assert.strictEqual(await second, 2);
|
||||
}
|
||||
|
||||
{
|
||||
const reason = new Error('stop');
|
||||
const controller = new AbortController();
|
||||
const debounced = debounce(common.mustNotCall(), 100, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
const result = debounced();
|
||||
controller.abort(reason);
|
||||
await assert.rejects(
|
||||
result,
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
}
|
||||
|
||||
{
|
||||
const reason = new Error('stop future calls');
|
||||
const controller = new AbortController();
|
||||
const debounced = debounce(common.mustNotCall(), 100, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
controller.abort(reason);
|
||||
await assert.rejects(
|
||||
debounced(),
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
await assert.rejects(
|
||||
debounced(),
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
assert.strictEqual(debounced.pending, null);
|
||||
assert.strictEqual(debounced.pendingCount, 0);
|
||||
}
|
||||
|
||||
{
|
||||
const reason = new Error('abort during leading call');
|
||||
const controller = new AbortController();
|
||||
const state = {};
|
||||
controller.signal.addEventListener('abort', common.mustCall(() => {
|
||||
state.result = state.debounced();
|
||||
}));
|
||||
state.debounced = debounce(common.mustNotCall(), 100, {
|
||||
leading: true,
|
||||
signal: controller.signal,
|
||||
});
|
||||
controller.abort(reason);
|
||||
await assert.rejects(
|
||||
state.result,
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const reason = new Error('abort during flush');
|
||||
const controller = new AbortController();
|
||||
const state = {};
|
||||
controller.signal.addEventListener('abort', common.mustCall(() => {
|
||||
state.debounced.flush();
|
||||
}));
|
||||
state.debounced = debounce(common.mustNotCall(), 100, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
const result = state.debounced();
|
||||
controller.abort(reason);
|
||||
await assert.rejects(
|
||||
result,
|
||||
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const controller = new AbortController();
|
||||
const debounced = debounce(common.mustCall(() => 1), 100, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
const result = debounced();
|
||||
debounced.flush();
|
||||
controller.abort();
|
||||
assert.strictEqual(await result, 1);
|
||||
}
|
||||
|
||||
{
|
||||
const debounced = debounce(common.mustCall((value) => value), 1);
|
||||
assert.strictEqual(await debounced(42), 42);
|
||||
}
|
||||
|
||||
{
|
||||
const debounced = debounce(common.mustCall((value) => value, 2), 100);
|
||||
const first = debounced(1);
|
||||
debounced.flush();
|
||||
assert.strictEqual(await first, 1);
|
||||
const second = debounced(2);
|
||||
debounced.flush();
|
||||
assert.strictEqual(await second, 2);
|
||||
}
|
||||
|
||||
{
|
||||
function original(first, second) {
|
||||
return first + second;
|
||||
}
|
||||
const debounced = debounce(original, 100);
|
||||
assert.strictEqual(debounced.name, original.name);
|
||||
assert.strictEqual(debounced.length, original.length);
|
||||
}
|
||||
|
||||
{
|
||||
let timeoutCount = 0;
|
||||
const hook = createHook({
|
||||
init(_asyncId, type) {
|
||||
if (type === 'Timeout') timeoutCount++;
|
||||
},
|
||||
});
|
||||
const debounced = debounce(common.mustCall((value) => value), 100);
|
||||
hook.enable();
|
||||
const first = debounced(1);
|
||||
const second = debounced(2);
|
||||
const third = debounced(3);
|
||||
hook.disable();
|
||||
assert.strictEqual(timeoutCount, 1);
|
||||
debounced.flush();
|
||||
assert.deepStrictEqual(await Promise.all([first, second, third]), [3, 3, 3]);
|
||||
}
|
||||
|
||||
{
|
||||
const debounced = debounce(common.mustNotCall(), 100);
|
||||
debounced();
|
||||
const pending = debounced();
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
process.once('unhandledRejection', common.mustCall((error, unhandled) => {
|
||||
assert.strictEqual(error.code, 'ABORT_ERR');
|
||||
assert.strictEqual(unhandled, pending);
|
||||
resolve();
|
||||
}));
|
||||
debounced.cancel();
|
||||
await promise;
|
||||
}
|
||||
|
||||
// Give canceled timers time to expose any accidental extra invocation.
|
||||
await setTimeout(110);
|
||||
})().then(common.mustCall());
|
||||
Reference in new issue
Block a user