UNPKG

lifecycle-utils

Version:

A set of general utilities for the lifecycle of a JS/TS project/library

229 lines 8.1 kB
import { MultiKeyMap } from "./MultiKeyMap.js"; import { Queue } from "./Queue.js"; const locks = new MultiKeyMap(); export async function withLock(scope, acquireLockSignalOrCallback, callback) { let acquireLockSignal = undefined; if (acquireLockSignalOrCallback instanceof AbortSignal) acquireLockSignal = acquireLockSignalOrCallback; else if (acquireLockSignalOrCallback != null) callback = acquireLockSignalOrCallback; if (callback == null) throw new Error("callback is required"); if (acquireLockSignal?.aborted) throw acquireLockSignal.reason; const scopeClone = scope.slice(); let state = locks.get(scopeClone); if (state != null) await createQueuePromise(state[0 /* LockIndex.queue */], acquireLockSignal, state); else { state = [new Queue(), new Queue()]; locks.set(scopeClone, state); } try { return await callback(); } finally { releaseNextLock(scopeClone, state); } } export async function withSharedLock(scope, acquireLockSignalOrCallback, callback) { let acquireLockSignal = undefined; if (acquireLockSignalOrCallback instanceof AbortSignal) acquireLockSignal = acquireLockSignalOrCallback; else if (acquireLockSignalOrCallback != null) callback = acquireLockSignalOrCallback; if (callback == null) throw new Error("callback is required"); if (acquireLockSignal?.aborted) throw acquireLockSignal.reason; const scopeClone = scope.slice(); let state = locks.get(scopeClone); if (state == null) { state = [new Queue(), new Queue(), 1]; locks.set(scopeClone, state); } else { const shared = state[2 /* LockIndex.shared */]; if (typeof shared === "number" && state[0 /* LockIndex.queue */].isEmpty) state[2 /* LockIndex.shared */] = shared + 1; else await createSharedQueuePromise(state[0 /* LockIndex.queue */], acquireLockSignal); } try { return await callback(); } finally { releaseSharedLock(scopeClone, state); } } /** * Check whether a lock is currently active for a given `scope` values. */ export function isLockActive(scope) { return locks.has(scope) ?? false; } /** * Acquire an exclusive lock for the given `scope`. * * An exclusive lock prevents any other exclusive or shared locks from being held at the same time. * Lock requests are acquired in the order they are made; consecutive shared lock requests are acquired together. */ export async function acquireLock(scope, acquireLockSignal) { if (acquireLockSignal?.aborted) throw acquireLockSignal.reason; const scopeClone = scope.slice(); let state = locks.get(scopeClone); if (state != null) await createQueuePromise(state[0 /* LockIndex.queue */], acquireLockSignal, state); else { state = [new Queue(), new Queue()]; locks.set(scopeClone, state); } return LockHandle._create(scopeClone, state, false); } /** * Acquire a shared lock for the given `scope`. * * Multiple shared locks can be held in parallel, while a regular lock requires exclusive access. * Lock requests are acquired in the order they are made; consecutive shared lock requests acquired together. * A new shared lock joins an active shared lock immediately when no regular lock requests are waiting. */ export async function acquireSharedLock(scope, acquireLockSignal) { if (acquireLockSignal?.aborted) throw acquireLockSignal.reason; const scopeClone = scope.slice(); let state = locks.get(scopeClone); if (state == null) { state = [new Queue(), new Queue(), 1]; locks.set(scopeClone, state); } else { const shared = state[2 /* LockIndex.shared */]; if (typeof shared === "number" && state[0 /* LockIndex.queue */].isEmpty) state[2 /* LockIndex.shared */] = shared + 1; else await createSharedQueuePromise(state[0 /* LockIndex.queue */], acquireLockSignal); } return LockHandle._create(scopeClone, state, true); } /** * Wait for a lock to be released for a given `scope` values. */ export async function waitForLockRelease(scope, signal) { if (signal?.aborted) throw signal.reason; const [queue, onDelete] = locks.get(scope) ?? []; if (queue == null || onDelete == null) return; await createQueuePromise(onDelete, signal); } export class LockHandle { scope; /** @internal */ _state; /** @internal */ _shared; constructor(scope, state, shared) { this.scope = scope; this._state = state; this._shared = shared; } dispose() { const state = this._state; if (state == null) return; this._state = undefined; if (this._shared) releaseSharedLock(this.scope, state); else releaseNextLock(this.scope, state); } [Symbol.dispose]() { this.dispose(); } /** @internal */ static _create(scope, state, shared) { return new LockHandle(scope, state, shared); } } function releaseNextLock(scope, state) { const queue = state[0 /* LockIndex.queue */]; if (!queue.isEmpty) { const entry = queue.first; if (typeof entry === "function") { queue.shift(); return void entry(); } return void activateSharedLocks(state); } locks.delete(scope); const onDelete = state[1 /* LockIndex.onDelete */]; for (const callback of onDelete.values()) callback(); onDelete.clear(); } function releaseSharedLock(scope, state) { const shared = state[2 /* LockIndex.shared */]; if (shared > 1) state[2 /* LockIndex.shared */] = shared - 1; else { state.length = 2 /* LockIndex.shared */; releaseNextLock(scope, state); } } function activateSharedLocks(state) { const queue = state[0 /* LockIndex.queue */]; let sharedUsageCount = 0; for (const entry of queue.values()) { if (typeof entry === "function") break; sharedUsageCount++; entry[0](); } state[2 /* LockIndex.shared */] = (state[2 /* LockIndex.shared */] ?? 0) + sharedUsageCount; queue.delete(0, sharedUsageCount); } function createQueuePromise(queue, signal, state) { if (signal == null) return new Promise((accept) => void queue.push(accept)); return new Promise((accept, reject) => { function onAcquireLock() { signal.removeEventListener("abort", onAbort); accept(); } const queueLength = queue.length; function onAbort() { const itemIndex = queue.lastIndexOf(onAcquireLock, queueLength); if (itemIndex >= 0) { queue.delete(itemIndex); if (state != null && itemIndex === 0 && state[2 /* LockIndex.shared */] != null && !queue.isEmpty && typeof queue.first !== "function") activateSharedLocks(state); } signal.removeEventListener("abort", onAbort); reject(signal.reason); } queue.push(onAcquireLock); signal.addEventListener("abort", onAbort); }); } function createSharedQueuePromise(queue, signal) { if (signal == null) return new Promise((accept) => void queue.push([accept])); return new Promise((accept, reject) => { function onAcquireLock() { signal.removeEventListener("abort", onAbort); accept(); } const queueLength = queue.length; const entry = [onAcquireLock]; function onAbort() { const itemIndex = queue.lastIndexOf(entry, queueLength); if (itemIndex >= 0) queue.delete(itemIndex); signal.removeEventListener("abort", onAbort); reject(signal.reason); } queue.push(entry); signal.addEventListener("abort", onAbort); }); } //# sourceMappingURL=withLock.js.map