- throttle: consume(0) resolves immediately - throttle: updateRate(0) makes consume instant (unlimited) - semaphore: release without acquire clamps active to 0 All 66 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
175 lines
4.9 KiB
JavaScript
175 lines
4.9 KiB
JavaScript
const { describe, it } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const Semaphore = require('../lib/semaphore');
|
|
|
|
describe('Semaphore', () => {
|
|
it('clamps limit to at least 1', () => {
|
|
assert.equal(new Semaphore(0).limit, 1);
|
|
assert.equal(new Semaphore(-5).limit, 1);
|
|
assert.equal(new Semaphore(undefined).limit, 1);
|
|
assert.equal(new Semaphore(3).limit, 3);
|
|
});
|
|
|
|
it('acquire resolves immediately when slots available', async () => {
|
|
const sem = new Semaphore(2);
|
|
await sem.acquire();
|
|
await sem.acquire();
|
|
assert.equal(sem.active, 2);
|
|
});
|
|
|
|
it('acquire blocks when all slots taken', async () => {
|
|
const sem = new Semaphore(1);
|
|
await sem.acquire();
|
|
|
|
let resolved = false;
|
|
const p = sem.acquire().then(() => { resolved = true; });
|
|
|
|
// Give microtask a chance to resolve
|
|
await new Promise(r => setTimeout(r, 10));
|
|
assert.equal(resolved, false, 'should not resolve while slot is taken');
|
|
assert.equal(sem.pending, 1);
|
|
|
|
sem.release();
|
|
await p;
|
|
assert.equal(resolved, true);
|
|
});
|
|
|
|
it('FIFO ordering', async () => {
|
|
const sem = new Semaphore(1);
|
|
await sem.acquire(); // take the one slot
|
|
|
|
const order = [];
|
|
const p1 = sem.acquire().then(() => order.push(1));
|
|
const p2 = sem.acquire().then(() => order.push(2));
|
|
const p3 = sem.acquire().then(() => order.push(3));
|
|
|
|
assert.equal(sem.pending, 3);
|
|
|
|
sem.release(); await p1;
|
|
sem.release(); await p2;
|
|
sem.release(); await p3;
|
|
|
|
assert.deepEqual(order, [1, 2, 3]);
|
|
});
|
|
|
|
it('release with no waiters decrements active', async () => {
|
|
const sem = new Semaphore(2);
|
|
await sem.acquire();
|
|
assert.equal(sem.active, 1);
|
|
sem.release();
|
|
assert.equal(sem.active, 0);
|
|
});
|
|
|
|
it('release never goes below 0', () => {
|
|
const sem = new Semaphore(2);
|
|
sem.release();
|
|
assert.equal(sem.active, 0);
|
|
sem.release();
|
|
assert.equal(sem.active, 0);
|
|
});
|
|
|
|
it('acquire rejects immediately if signal already aborted', async () => {
|
|
const sem = new Semaphore(2);
|
|
const ac = new AbortController();
|
|
ac.abort();
|
|
|
|
await assert.rejects(sem.acquire(ac.signal), /Aborted/);
|
|
assert.equal(sem.active, 0, 'no slot should be acquired');
|
|
});
|
|
|
|
it('abort while waiting in queue removes entry and rejects', async () => {
|
|
const sem = new Semaphore(1);
|
|
await sem.acquire(); // take the slot
|
|
|
|
const ac = new AbortController();
|
|
const p = sem.acquire(ac.signal);
|
|
|
|
assert.equal(sem.pending, 1);
|
|
ac.abort();
|
|
await assert.rejects(p, /Aborted/);
|
|
assert.equal(sem.pending, 0, 'entry should be removed from queue');
|
|
|
|
// Release original slot - should not cause issues
|
|
sem.release();
|
|
assert.equal(sem.active, 0);
|
|
});
|
|
|
|
it('abort listener is cleaned up when slot is granted via release', async () => {
|
|
const sem = new Semaphore(1);
|
|
await sem.acquire();
|
|
|
|
const ac = new AbortController();
|
|
let rejected = false;
|
|
const p = sem.acquire(ac.signal).catch(() => { rejected = true; });
|
|
|
|
sem.release(); // grants slot to the waiter
|
|
await p;
|
|
|
|
// Now abort after the slot was already granted
|
|
ac.abort();
|
|
await new Promise(r => setTimeout(r, 10));
|
|
assert.equal(rejected, false, 'reject should not fire after slot was granted');
|
|
});
|
|
|
|
it('updateLimit wakes waiters', async () => {
|
|
const sem = new Semaphore(1);
|
|
await sem.acquire();
|
|
|
|
const resolved = [];
|
|
const p1 = sem.acquire().then(() => resolved.push(1));
|
|
const p2 = sem.acquire().then(() => resolved.push(2));
|
|
|
|
sem.updateLimit(3);
|
|
await Promise.all([p1, p2]);
|
|
assert.deepEqual(resolved, [1, 2]);
|
|
});
|
|
|
|
it('updateLimit to lower value does not kill active slots', async () => {
|
|
const sem = new Semaphore(3);
|
|
await sem.acquire();
|
|
await sem.acquire();
|
|
await sem.acquire();
|
|
assert.equal(sem.active, 3);
|
|
|
|
sem.updateLimit(1);
|
|
assert.equal(sem.active, 3, 'existing active slots should not be evicted');
|
|
|
|
sem.release();
|
|
sem.release();
|
|
sem.release();
|
|
assert.equal(sem.active, 0);
|
|
|
|
// Now only 1 slot should be available
|
|
await sem.acquire();
|
|
let blocked = false;
|
|
const p = sem.acquire().then(() => { blocked = true; });
|
|
await new Promise(r => setTimeout(r, 10));
|
|
assert.equal(blocked, false, 'should block at limit 1');
|
|
sem.release();
|
|
await p;
|
|
});
|
|
|
|
it('pending getter tracks queue size', async () => {
|
|
const sem = new Semaphore(1);
|
|
assert.equal(sem.pending, 0);
|
|
|
|
await sem.acquire();
|
|
sem.acquire(); // blocked
|
|
sem.acquire(); // blocked
|
|
assert.equal(sem.pending, 2);
|
|
|
|
sem.release();
|
|
await new Promise(r => setTimeout(r, 5));
|
|
assert.equal(sem.pending, 1);
|
|
});
|
|
|
|
it('release without acquire clamps active to 0', () => {
|
|
const sem = new Semaphore(2);
|
|
assert.equal(sem.active, 0);
|
|
sem.release();
|
|
assert.equal(sem.active, 0, 'should not go negative');
|
|
sem.release();
|
|
assert.equal(sem.active, 0, 'should still be 0');
|
|
});
|
|
});
|