147 lines
5.0 KiB
JavaScript
147 lines
5.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { setImmediate } from 'node:timers/promises';
|
|
import { mapBounded } from '../lib/concurrency.mjs';
|
|
|
|
function deferred() {
|
|
let resolve, reject;
|
|
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
test('bounded workers keep input order when tasks finish out of order', { timeout: 2000 }, async () => {
|
|
const gates = Array.from({ length: 5 }, deferred);
|
|
const started = [];
|
|
let active = 0, maximum = 0;
|
|
const pending = mapBounded(['a', 'b', 'c', 'd', 'e'], 2, async (value, index) => {
|
|
started.push(index);
|
|
maximum = Math.max(maximum, ++active);
|
|
try { await gates[index].promise; return value.toUpperCase(); }
|
|
finally { active--; }
|
|
});
|
|
assert.deepEqual(started, [0, 1]);
|
|
gates[1].resolve();
|
|
await setImmediate();
|
|
assert.deepEqual(started, [0, 1, 2]);
|
|
gates[2].resolve();
|
|
await setImmediate();
|
|
assert.deepEqual(started, [0, 1, 2, 3]);
|
|
gates[0].resolve();
|
|
await setImmediate();
|
|
assert.deepEqual(started, [0, 1, 2, 3, 4]);
|
|
gates[4].resolve();
|
|
gates[3].resolve();
|
|
assert.deepEqual(await pending, ['A', 'B', 'C', 'D', 'E']);
|
|
assert.equal(maximum, 2);
|
|
assert.equal(active, 0);
|
|
});
|
|
|
|
test('failure stops new work and waits for all running workers before rejecting', { timeout: 2000 }, async () => {
|
|
const gates = Array.from({ length: 5 }, deferred);
|
|
const started = [];
|
|
const finished = [];
|
|
const failure = new Error('first verification failed');
|
|
let settled = false;
|
|
const pending = mapBounded([0, 1, 2, 3, 4], 3, async index => {
|
|
started.push(index);
|
|
try { await gates[index].promise; return index; }
|
|
finally { finished.push(index); }
|
|
});
|
|
const outcome = pending.then(
|
|
value => { settled = true; return { value }; },
|
|
error => { settled = true; return { error }; },
|
|
);
|
|
assert.deepEqual(started, [0, 1, 2]);
|
|
gates[1].reject(failure);
|
|
await setImmediate();
|
|
assert.deepEqual(started, [0, 1, 2]);
|
|
assert.equal(settled, false);
|
|
gates[0].resolve();
|
|
await setImmediate();
|
|
assert.deepEqual(started, [0, 1, 2]);
|
|
assert.equal(settled, false);
|
|
gates[2].resolve();
|
|
assert.equal((await outcome).error, failure);
|
|
assert.equal(settled, true);
|
|
assert.deepEqual(finished.sort(), [0, 1, 2]);
|
|
});
|
|
|
|
test('additional in-flight rejections are drained while preserving the first failure', { timeout: 2000 }, async () => {
|
|
const gates = Array.from({ length: 3 }, deferred);
|
|
const first = new Error('first');
|
|
const second = new Error('second');
|
|
let calls = 0, settled = false;
|
|
const outcome = mapBounded([0, 1, 2, 3], 3, async index => {
|
|
calls++;
|
|
await gates[index].promise;
|
|
return index;
|
|
}).then(
|
|
() => { settled = true; assert.fail('must reject'); },
|
|
error => { settled = true; return error; },
|
|
);
|
|
gates[0].reject(first);
|
|
await setImmediate();
|
|
gates[1].reject(second);
|
|
await setImmediate();
|
|
assert.equal(settled, false);
|
|
assert.equal(calls, 3);
|
|
gates[2].resolve();
|
|
assert.equal(await outcome, first);
|
|
});
|
|
|
|
test('a synchronous failure stops scheduling immediately', async () => {
|
|
const failure = new Error('synchronous failure');
|
|
const started = [];
|
|
await assert.rejects(mapBounded([0, 1, 2], 3, value => {
|
|
started.push(value);
|
|
throw failure;
|
|
}), error => error === failure);
|
|
assert.deepEqual(started, [0]);
|
|
});
|
|
|
|
test('falsy rejection values still reject and never get treated as success', async () => {
|
|
for (const failure of [undefined, null, false, 0, '']) {
|
|
let rejected = false;
|
|
await mapBounded([1], 1, () => Promise.reject(failure)).then(
|
|
() => assert.fail('must reject'),
|
|
error => { rejected = true; assert.equal(error, failure); },
|
|
);
|
|
assert.equal(rejected, true);
|
|
}
|
|
});
|
|
|
|
test('limit one executes serially and accepts synchronous return values', async () => {
|
|
const order = [];
|
|
assert.deepEqual(await mapBounded([2, 4, 6], 1, (value, index) => {
|
|
order.push(index);
|
|
return value * 2;
|
|
}), [4, 8, 12]);
|
|
assert.deepEqual(order, [0, 1, 2]);
|
|
});
|
|
|
|
test('limits larger than the item count only start existing items', async () => {
|
|
let calls = 0;
|
|
const result = await mapBounded(['x', 'y'], 1000, async value => { calls++; return value; });
|
|
assert.deepEqual(result, ['x', 'y']);
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test('empty input returns an empty array without invoking the worker', async () => {
|
|
assert.deepEqual(await mapBounded([], 4, () => assert.fail('must not run')), []);
|
|
});
|
|
|
|
test('invalid limits reject even for an empty input', async () => {
|
|
for (const limit of [0, -1, 1.5, Infinity, NaN, '4', null, undefined, Number.MAX_SAFE_INTEGER + 1]) {
|
|
await assert.rejects(mapBounded([], limit, () => {}), /正整数/);
|
|
}
|
|
});
|
|
|
|
test('input values are snapshotted before workers mutate the original collection', async () => {
|
|
const input = ['first', 'second', 'third'];
|
|
const result = await mapBounded(input, 1, async (value, index) => {
|
|
if (index === 0) input.splice(1, 2, 'changed');
|
|
return value;
|
|
});
|
|
assert.deepEqual(result, ['first', 'second', 'third']);
|
|
});
|