🔒 A tiny, modern wrapper around the Web Locks API
Provides a clean Promise-based andasync dispose-friendly interface for lock management.
- 🧩 Minimal and dependency-free — pure TypeScript
- 🔁 Promise-based lifecycle with
release() - 🪄 Built-in support for
await usingviaSymbol.asyncDispose - 🧠 Works with
navigator.locksor any customLockManager(great for testing)
npm install disposable-lock
# or
pnpm add disposable-lockimport { lock } from "disposable-lock";
async function main() {
const { request } = lock("user-data");
// --- Standard lock acquisition ---
const acquired = await request({ mode: "exclusive" });
if (acquired) {
console.log(`✅ Lock acquired: ${acquired.name}`);
await doSomething();
await acquired.release();
}
// --- ifAvailable: true ---
const maybeLock = await request({ ifAvailable: true });
if (maybeLock) {
console.log(`✅ Lock acquired (ifAvailable): ${maybeLock.name}`);
await doSomething();
await maybeLock.release();
} else {
console.log("⚠️ Lock not available (ifAvailable: true), skipping critical section");
}
}import { lock } from "disposable-lock";
async function autoRelease() {
const cacheLock = lock("cache-update");
// --- Standard await using ---
await using acquired = await cacheLock.request();
if (acquired) {
console.log("Lock acquired, performing critical section...");
await doSomething();
}
// --- await using with ifAvailable: true ---
await using maybeLock = await cacheLock.request({ ifAvailable: true });
if (maybeLock) {
console.log("Lock acquired (ifAvailable), performing critical section...");
await doSomething();
} else {
console.log("Lock not available (ifAvailable: true), safe to skip");
}
}request()returns aReleasableLockwhen successful, ornullif the lock could not be obtained- Wrapping with
await usingensures automatic release at the end of the block ifAvailable: trueattempts to acquire the lock but immediately returnsnullif unavailable
Creates a lock handler bound to the given name.
Returns an object with:
| Method | Description |
|---|---|
request(options?: LockOptions) |
Request a lock. Returns a ReleasableLock or a null. |
query() |
Query the current state (held and pending locks) for this name. |
Throws if navigator.locks is unavailable and no custom LockManager is provided.
Represents a successfully acquired lock.
| Property / Method | Type | Description |
|---|---|---|
name |
string |
Lock name |
mode |
"exclusive" | "shared" |
Lock mode |
release() |
() => Promise<boolean> |
Releases the lock |
[Symbol.asyncDispose]() |
() => Promise<void> |
Enables await using syntax |
const userLock = lock("user-data");
const state = await userLock.query();
if (state.held) {
console.log("Currently held by:", state.held.map(x => x.clientId));
}
if (state.pending) {
console.log("Pending requests:", state.pending.length);
}When running in Node.js or a test environment where navigator.locks is not available,
you can provide your own LockManager instance:
import { lock } from "disposable-lock";
import { createMockLockManager } from "./test-utils"; // your mock
const locks = createMockLockManager();
const fileLock = lock("file-write", { locks });The Web Locks API provides powerful coordination primitives in browsers, but its callback-based API can be cumbersome. disposable-lock offers a clean, composable Promise interface and modern async dispose support — perfect for structured concurrency and testable code.
MIT © juner