Skip to content

Instantly share code, notes, and snippets.

@numberoverzero
Last active May 4, 2025 20:08
Show Gist options
  • Save numberoverzero/0f5754c056bc1a0d00955d799cc44ce3 to your computer and use it in GitHub Desktop.
Save numberoverzero/0f5754c056bc1a0d00955d799cc44ce3 to your computer and use it in GitHub Desktop.
Semaphore with a minimal API
export type PromiseFactory<T> = () => Promise<T>;
export type ReleaseSemaphore = () => void;
type NextAcquire = () => void;
export class Semaphore {
#available: number;
#queue: NextAcquire[];
constructor(max: number) {
this.#available = max;
this.#queue = [];
}
get available() {
return this.#available;
}
/**
* returns an idempotent function to release the acquired lock.
* @example
* const release = await lock.acquire();
* try {
* await process();
* } finally {
* release();
* release(); // safe to call twice
* }
*/
async acquire(): Promise<ReleaseSemaphore> {
if (this.#available > 0) return this.#acquire();
return new Promise<ReleaseSemaphore>((resolve) => {
this.#queue.push(() => {
resolve(this.#acquire());
});
});
}
#acquire(): ReleaseSemaphore {
this.#available--;
// guard double releases:
// const release = await lock.acquire();
// release(); release();
let called = false;
return () => {
if (!called) {
called = true;
this.#release();
}
};
}
#release(): void {
this.#available++;
this.#queue.shift()?.();
}
/** acquire, await (fn()()), release() */
async run<T>(fn: PromiseFactory<T>): Promise<T> {
const release = await this.acquire();
try {
return await fn();
} finally {
release();
}
}
}
function range(n: number): number[] {
return Array.from({ length: n }, (_, i) => i);
}
async function sleep(ms: number): Promise<void> {
await new Promise((r) => setTimeout(r, ms));
}
let taskId = 0;
async function createTask(): Promise<number> {
const id = taskId++;
console.log(`${id}: inside lock`);
const resp = (await fetch(`http://localhost:8000/proc/${id}`)).status;
console.log(`${id}: wait`);
await sleep(100);
console.log(`${id}: done`);
return resp;
}
async function main() {
const lock = new Semaphore(2);
const promises = range(7).map((i) => {
console.log(`${i}: enqueue`);
return lock.run(createTask);
});
const results = await Promise.all(promises);
console.log(results);
}
await main();
0: enqueue
1: enqueue
2: enqueue
3: enqueue
4: enqueue
5: enqueue
6: enqueue
0: inside lock
1: inside lock
0: wait
1: wait
0: done
2: inside lock
1: done
3: inside lock
2: wait
3: wait
2: done
4: inside lock
3: done
5: inside lock
4: wait
5: wait
4: done
6: inside lock
5: done
6: wait
6: done
[
200, 200, 200,
200, 200, 200,
200
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment