123 lines
3.5 KiB
TypeScript
123 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import { lazyBoundedMap, mapWithConcurrency } from '@/lib/utils/concurrency';
|
||
|
||
const tick = (ms = 5) => new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
||
describe('mapWithConcurrency', () => {
|
||
it('returns results in input order regardless of completion order', async () => {
|
||
// Later items resolve first, but results must stay aligned with input.
|
||
const out = await mapWithConcurrency([30, 10, 20], 3, async (ms, i) => {
|
||
await tick(ms);
|
||
return i;
|
||
});
|
||
expect(out).toEqual([0, 1, 2]);
|
||
});
|
||
|
||
it('never runs more than `limit` workers at once', async () => {
|
||
let active = 0;
|
||
let peak = 0;
|
||
await mapWithConcurrency(
|
||
Array.from({ length: 9 }, (_, i) => i),
|
||
3,
|
||
async (n) => {
|
||
active += 1;
|
||
peak = Math.max(peak, active);
|
||
await tick();
|
||
active -= 1;
|
||
return n;
|
||
},
|
||
);
|
||
expect(peak).toBeLessThanOrEqual(3); // never exceeds the pool
|
||
});
|
||
|
||
it('clamps the limit to the item count (no over-spawn)', async () => {
|
||
let active = 0;
|
||
let peak = 0;
|
||
const out = await mapWithConcurrency([1, 2], 100, async (n) => {
|
||
active += 1;
|
||
peak = Math.max(peak, active);
|
||
await tick();
|
||
active -= 1;
|
||
return n;
|
||
});
|
||
expect(peak).toBeLessThanOrEqual(2);
|
||
expect(out).toEqual([1, 2]);
|
||
});
|
||
|
||
it('stops pulling new items once shouldContinue() turns false', async () => {
|
||
const processed: number[] = [];
|
||
let done = 0;
|
||
await mapWithConcurrency(
|
||
[1, 2, 3, 4, 5, 6],
|
||
1,
|
||
async (n) => {
|
||
processed.push(n);
|
||
done += 1;
|
||
return n;
|
||
},
|
||
{ shouldContinue: () => done < 3 },
|
||
);
|
||
// limit 1 + stop after 3 ⇒ items 4–6 are never started.
|
||
expect(processed).toEqual([1, 2, 3]);
|
||
});
|
||
|
||
it('handles an empty list without spawning workers', async () => {
|
||
expect(await mapWithConcurrency([], 4, async (n) => n)).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('lazyBoundedMap', () => {
|
||
it('returns promises immediately and resolves them without a barrier', async () => {
|
||
const started: number[] = [];
|
||
const promises = lazyBoundedMap([0, 1, 2], 1, async (n) => {
|
||
started.push(n);
|
||
await tick();
|
||
return n * 10;
|
||
});
|
||
expect(promises).toHaveLength(3); // the array of promises exists synchronously
|
||
expect(await promises[0]).toBe(0); // the first resolves on its own…
|
||
expect(started.length).toBeLessThan(3); // …without forcing the last item to run (no barrier)
|
||
expect(await Promise.all(promises)).toEqual([0, 10, 20]); // order + values preserved
|
||
});
|
||
|
||
it('caps in-flight work at `limit`', async () => {
|
||
let active = 0;
|
||
let peak = 0;
|
||
await Promise.all(
|
||
lazyBoundedMap(
|
||
Array.from({ length: 8 }, (_, i) => i),
|
||
3,
|
||
async (n) => {
|
||
active += 1;
|
||
peak = Math.max(peak, active);
|
||
await tick();
|
||
active -= 1;
|
||
return n;
|
||
},
|
||
),
|
||
);
|
||
expect(peak).toBeLessThanOrEqual(3);
|
||
expect(peak).toBeGreaterThan(1);
|
||
});
|
||
|
||
it('skips items via shouldContinue without running fn', async () => {
|
||
const ran: number[] = [];
|
||
let done = 0;
|
||
const out = await Promise.all(
|
||
lazyBoundedMap(
|
||
[1, 2, 3, 4, 5],
|
||
1,
|
||
async (n) => {
|
||
ran.push(n);
|
||
done += 1;
|
||
return n;
|
||
},
|
||
{ shouldContinue: () => done < 2 },
|
||
),
|
||
);
|
||
expect(ran).toEqual([1, 2]); // fn ran only twice
|
||
expect(out).toEqual([1, 2, undefined, undefined, undefined]); // skipped → undefined
|
||
});
|
||
});
|