lifecycle-utils
Version:
A set of general utilities for the lifecycle of a JS/TS project/library
229 lines • 8.1 kB
JavaScript
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