import { describe, it } from 'node:test' import assert from 'node:assert' import { Semaphore } from './semaphore.js' describe('Semaphore', () => { it('allows up to maxConcurrency parallel executions', async () => { const semaphore = new Semaphore(2) let concurrent = 0 let maxConcurrent = 0 const tasks = Array.from({ length: 4 }, () => semaphore.run(async () => { concurrent++ if (concurrent > maxConcurrent) { maxConcurrent = concurrent } await new Promise(resolve => setTimeout(resolve, 10)) concurrent-- }) ) await Promise.all(tasks) assert.strictEqual(maxConcurrent, 2) }) it('queues when at maxConcurrency', async () => { const semaphore = new Semaphore(1) const order: number[] = [] const tasks = Array.from({ length: 3 }, (_, i) => semaphore.run(async () => { order.push(i) await new Promise(resolve => setTimeout(resolve, 10)) }) ) await Promise.all(tasks) assert.deepStrictEqual(order, [0, 1, 2]) }) it('executes queued tasks FIFO', async () => { const semaphore = new Semaphore(1) const completionOrder: number[] = [] const tasks = Array.from({ length: 5 }, (_, i) => semaphore.run(async () => { await new Promise(resolve => setTimeout(resolve, 5)) completionOrder.push(i) }) ) await Promise.all(tasks) assert.deepStrictEqual(completionOrder, [0, 1, 2, 3, 4]) }) it('propagates errors without leaking slots', async () => { const semaphore = new Semaphore(1) let subsequentRan = false const errorTask = semaphore.run(async () => { throw new Error('intentional error') }) const normalTask = semaphore.run(async () => { subsequentRan = true }) await assert.rejects(errorTask, /intentional error/) await normalTask assert.strictEqual(subsequentRan, true) }) it('handles many tasks', async () => { const semaphore = new Semaphore(4) const results: number[] = [] const tasks = Array.from({ length: 100 }, (_, i) => semaphore.run(async () => { results.push(i) await new Promise(resolve => setTimeout(resolve, 1)) }) ) await Promise.all(tasks) assert.strictEqual(results.length, 100) }) })