// Flags: --expose-internals
'use strict';
const common = require('../common');
const assert = require('node:assert');
const { createHook } = require('node:async_hooks');
const { setImmediate } = require('node:timers/promises');
const { throttle } = require('node:util');
const { TIMEOUT_MAX } = require('internal/timers');
process.on('unhandledRejection', common.mustNotCall());
for (const value of [undefined, null, true, 0, 'fn', {}, [], Symbol()]) {
assert.throws(() => throttle(value, 1, 1), {
code: 'ERR_INVALID_ARG_TYPE',
});
}
for (const value of [undefined, null, true, '1', {}, [], Symbol()]) {
assert.throws(() => throttle(() => {}, value, 1), {
code: 'ERR_INVALID_ARG_TYPE',
});
}
for (const value of [-1, 0, 0.5, NaN, Infinity]) {
assert.throws(() => throttle(() => {}, value, 1), {
code: 'ERR_OUT_OF_RANGE',
});
}
for (const value of [undefined, null, true, '1', {}, [], Symbol()]) {
assert.throws(() => throttle(() => {}, 1, value), {
code: 'ERR_INVALID_ARG_TYPE',
});
}
for (const value of [-1, 0.5, NaN, Infinity, TIMEOUT_MAX + 1]) {
assert.throws(() => throttle(() => {}, 1, value), {
code: 'ERR_OUT_OF_RANGE',
});
}
for (const value of [null, true, 1, 'options', []]) {
assert.throws(() => throttle(() => {}, 1, 1, value), {
code: 'ERR_INVALID_ARG_TYPE',
});
}
assert.throws(() => throttle(() => {}, 1, 1, { signal: {} }), {
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => throttle(() => {}, 1, 1, { strict: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});
for (const value of [null, true, 0, 'invalid', {}, []]) {
assert.throws(() => throttle(() => {}, 1, 1, { overflow: value }), {
code: 'ERR_INVALID_ARG_VALUE',
});
}
for (const option of ['concurrency', 'maxPending']) {
for (const value of [null, true, '1', {}, [], Symbol()]) {
assert.throws(() => throttle(() => {}, 1, 1, { [option]: value }), {
code: 'ERR_INVALID_ARG_TYPE',
});
}
}
for (const value of [-1, 0, 0.5, NaN, -Infinity]) {
assert.throws(() => throttle(() => {}, 1, 1, { concurrency: value }), {
code: 'ERR_OUT_OF_RANGE',
});
}
for (const value of [-1, 0.5, NaN, -Infinity]) {
assert.throws(() => throttle(() => {}, 1, 1, { maxPending: value }), {
code: 'ERR_OUT_OF_RANGE',
});
}
// Explicit Infinity values are accepted.
throttle(() => {}, 1, 1, {
concurrency: Infinity,
maxPending: Infinity,
});
{
const reason = new Error('already aborted');
assert.throws(
() => throttle(() => {}, 1, 1, {
signal: AbortSignal.abort(reason),
}),
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
);
}
// Keep windows open across synchronous assertions, even if the process is
// descheduled. Window expiration is covered in test-util-throttle-timing.js.
(async () => {
{
let running = 0;
let maxRunning = 0;
let timeoutCount = 0;
const releases = [];
const values = [];
const hook = createHook({
init(_asyncId, type) {
if (type === 'Timeout') timeoutCount++;
},
});
const throttled = throttle(common.mustCall((value) => {
running++;
maxRunning = Math.max(maxRunning, running);
values.push(value);
const { promise, resolve } = Promise.withResolvers();
releases.push(() => {
running--;
resolve(value);
});
return promise;
}, 4), 10, 1_000, { concurrency: 2 });
hook.enable();
const calls = [
throttled(1),
throttled(2),
throttled(3),
throttled(4),
];
hook.disable();
assert.deepStrictEqual(values, [1, 2]);
assert.strictEqual(throttled.hasImmediateCapacity(), false);
assert.strictEqual(throttled.activeCount, 2);
assert.strictEqual(throttled.pendingCount, 2);
assert.strictEqual(timeoutCount, 0);
releases[0]();
await setImmediate();
assert.deepStrictEqual(values, [1, 2, 3]);
assert.strictEqual(throttled.activeCount, 2);
assert.strictEqual(throttled.pendingCount, 1);
releases[1]();
await setImmediate();
assert.deepStrictEqual(values, [1, 2, 3, 4]);
assert.strictEqual(throttled.activeCount, 2);
assert.strictEqual(throttled.pendingCount, 0);
releases[2]();
releases[3]();
assert.deepStrictEqual(await Promise.all(calls), [1, 2, 3, 4]);
assert.strictEqual(throttled.activeCount, 0);
assert.strictEqual(maxRunning, 2);
}
{
const deferred = Promise.withResolvers();
const throttled = throttle(common.mustCall(() => deferred.promise), 10, 100, {
concurrency: 1,
overflow: 'drop',
});
const first = throttled();
const dropped = throttled();
assert.strictEqual(throttled.activeCount, 1);
assert.strictEqual(throttled.pendingCount, 0);
assert.strictEqual(throttled.hasImmediateCapacity(), false);
await setImmediate();
await assert.rejects(dropped, { code: 'ERR_THROTTLED' });
deferred.resolve('result');
assert.strictEqual(await first, 'result');
assert.strictEqual(throttled.activeCount, 0);
}
{
let timeoutCount = 0;
const hook = createHook({
init(_asyncId, type) {
if (type === 'Timeout') timeoutCount++;
},
});
const values = [];
const throttled = throttle(common.mustCall((value) => {
values.push(value);
return value;
}, 3), 2, TIMEOUT_MAX, { overflow: 'drop' });
hook.enable();
const first = throttled(1);
const second = throttled(2);
const dropped = throttled(3);
hook.disable();
assert.deepStrictEqual(values, [1, 2]);
assert.strictEqual(timeoutCount, 0);
assert.strictEqual(throttled.pending, null);
assert.strictEqual(throttled.pendingCount, 0);
await setImmediate();
await assert.rejects(dropped, { code: 'ERR_THROTTLED' });
assert.deepStrictEqual(await Promise.all([first, second]), [1, 2]);
throttled.cancel();
assert.strictEqual(await throttled(4), 4);
assert.deepStrictEqual(values, [1, 2, 4]);
}
{
const reason = new Error('cancelled');
const throttled = throttle(common.mustCall((value) => value, 2), 1, TIMEOUT_MAX);
const first = throttled(1);
const second = throttled(2);
const third = throttled(3);
const secondRejection = assert.rejects(
second,
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
);
const thirdRejection = assert.rejects(
third,
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
);
throttled.cancel(reason);
assert.strictEqual(throttled.pending, null);
assert.strictEqual(throttled.pendingCount, 0);
assert.strictEqual(await first, 1);
await Promise.all([secondRejection, thirdRejection]);
// Canceling resets the limiter, so the next call can run immediately.
assert.strictEqual(await throttled(4), 4);
throttled.cancel();
}
{
const reason = new Error('stop');
const controller = new AbortController();
const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX, {
signal: controller.signal,
});
const first = throttled(1);
const second = throttled(2);
const secondRejection = assert.rejects(
second,
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
);
controller.abort(reason);
assert.strictEqual(throttled.hasImmediateCapacity(), false);
assert.strictEqual(await first, 1);
await secondRejection;
assert.strictEqual(throttled.pending, null);
assert.strictEqual(throttled.pendingCount, 0);
await assert.rejects(
throttled(3),
(error) => error.code === 'ABORT_ERR' && error.cause === reason,
);
}
{
const expected = new Error('failure');
const throttled = throttle(common.mustCall((value) => {
if (value === 2) throw expected;
return value;
}, 2), 1, 20);
assert.strictEqual(await throttled(1), 1);
await assert.rejects(throttled(2), (error) => error === expected);
}
{
const expected = new Error('async failure');
const throttled = throttle(common.mustCall(async () => {
throw expected;
}), 1, 0);
await assert.rejects(throttled(), (error) => error === expected);
}
{
function original(first, second) {
return first + second;
}
const throttled = throttle(original, 1, 0);
assert.strictEqual(throttled.name, original.name);
assert.strictEqual(throttled.length, original.length);
assert.strictEqual(await throttled(1, 2), 3);
}
{
const values = [];
const throttled = throttle(common.mustCall((value) => {
values.push(value);
return value;
}, 5), 1, 0);
const calls = [];
for (let i = 0; i < 5; i++) calls.push(throttled(i));
assert.deepStrictEqual(values, [0, 1, 2, 3, 4]);
assert.deepStrictEqual(await Promise.all(calls), [0, 1, 2, 3, 4]);
}
{
let timeoutCount = 0;
const hook = createHook({
init(_asyncId, type) {
if (type === 'Timeout') timeoutCount++;
},
});
const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX);
hook.enable();
assert.strictEqual(throttled.hasImmediateCapacity(), true);
const call = throttled(1);
assert.strictEqual(throttled.hasImmediateCapacity(), false);
if (throttled.hasImmediateCapacity()) throttled(2);
hook.disable();
assert.strictEqual(timeoutCount, 0);
assert.strictEqual(throttled.pendingCount, 0);
assert.strictEqual(await call, 1);
}
{
let timeoutCount = 0;
const hook = createHook({
init(_asyncId, type) {
if (type === 'Timeout') timeoutCount++;
},
});
const throttled = throttle(common.mustCall((value) => value), 1, TIMEOUT_MAX);
hook.enable();
const first = throttled(1);
const second = throttled(2);
const third = throttled(3);
hook.disable();
const secondRejection = assert.rejects(second, { code: 'ABORT_ERR' });
const thirdRejection = assert.rejects(third, { code: 'ABORT_ERR' });
assert.strictEqual(timeoutCount, 1);
assert.strictEqual(throttled.unref(), throttled);
assert.strictEqual(throttled.ref(), throttled);
throttled.cancel();
assert.strictEqual(await first, 1);
await Promise.all([secondRejection, thirdRejection]);
}
})().then(common.mustCall());