@dmytromykhailiuk/execution-blocker
v1.0.0
Published
Promise-based FIFO execution lock with independent queues per id — run async logic strictly one at a time. Zero dependencies.
Maintainers
Readme
@dmytromykhailiuk/execution-blocker
Promise-based FIFO execution lock with independent queues per id — run async logic strictly one at a time. Zero dependencies, works in any browser and in Node.
Full documentation: open Docs in a browser — every method, with examples, a table of contents and cross-links. This README is the short form.
⚠️ The rule that makes it work: a lock taken with
block()must be released — call the returned function in afinally, or every later caller of that queue waits forever.run()is the safe counterpart: it acquires, executes and releases even when the task throws. Reach forblock()only when acquire and release genuinely live in different places.
Built for the async logic that must not overlap: refreshing an auth token once instead of five
times in parallel, serializing writes to a file or IndexedDB, keeping "read, then update" atomic,
draining actions against one resource in order. JavaScript won't interleave your statements —
but every await is a door for another caller to walk through. An execution blocker closes it:
callers of the same queue line up and run strictly one after another, in the order they arrived.
Install
npm i @dmytromykhailiuk/execution-blockerQuick start
import { createExecutionBlocker } from "@dmytromykhailiuk/execution-blocker";
const blocker = createExecutionBlocker();
// Ten parallel calls — the body still runs strictly one at a time.
const refreshToken = () =>
blocker.run("auth", async () => {
if (!isExpired(token)) return token; // later callers see the fresh token
token = await api.refresh(); // executed once, not ten times
return token;
});run(id, fn)acquires theidqueue, runsfn, and releases — even whenfnthrows. It resolves withfn's result.- Tasks on the same id run one after another, FIFO. Tasks on different ids don't wait for each other.
- Omit the id (
run(fn),block()) to use the shared"default"queue.
API
const blocker = createExecutionBlocker();
blocker.run(id?, fn); // acquire → fn() → release; resolves with fn's result
blocker.block(id?); // resolves with release() once every earlier holder is done
blocker.isLocked(id?); // is anything holding or waiting for this queue?
blocker.pending(id?); // holders + waiters currently in this queueEach createExecutionBlocker() call is an isolated world — two blockers never see each other's
queues. Create one per domain and share it via a module export.
block() — manual acquire / release
For the rare case where acquire and release live in different places (a stream that opens here and closes in a callback there):
const release = await blocker.block("file:write");
try {
await stream.write(chunk);
} finally {
release(); // idempotent — a second call is a safe no-op
}release is idempotent, so calling it twice can't free the next waiter early. But not calling
it deadlocks the queue — which is why run() is the default choice.
Independent queues
The id picks the queue, and only same-id callers line up:
blocker.run("user:42", updateProfile); // ┐ run one after another
blocker.run("user:42", updateSettings); // ┘
blocker.run("user:7", updateProfile); // runs immediately — different queueIds are plain strings — build them from your domain: `user:${id}`, `file:${path}`.
A queue that empties is deleted internally; there is no cleanup to do.
Error handling
A task that throws does not poison the queue: run() releases the lock in a finally, rejects
with the original error, and the next task starts as usual.
await blocker.run("q", async () => { throw new Error("boom"); }).catch(() => {});
await blocker.run("q", async () => "still works"); // "still works"TypeScript
const n = await blocker.run("q", async () => 42); // number
const s = await blocker.run(() => "sync works too"); // string
const release: Release = await blocker.block("q");run() infers its result type from the task; synchronous tasks are supported. The blocker object
is frozen — its methods cannot be reassigned.
License
MIT
