semafy
Version:
A robust cross-agent synchronization library.
1 lines • 234 kB
Source Map (JSON)
{"version":3,"file":"semafy.cjs","sources":["../src/types/cvStatus.ts","../src/errors/constants.ts","../src/errors/lockError.ts","../src/errors/multiLockError.ts","../src/errors/multiUnlockError.ts","../src/errors/ownershipError.ts","../src/errors/relockError.ts","../src/errors/timeoutError.ts","../src/mutexes/mutex.ts","../src/utils/constants.ts","../src/mutexes/recursiveMutex.ts","../src/types/atomicsStatus.ts","../src/mutexes/recursiveTimedMutex.ts","../src/locks/lockGuard.ts","../src/condVars/conditionVariable.ts","../src/mutexes/timedMutex.ts","../src/mutexes/sharedMutex.ts","../src/mutexes/sharedTimedMutex.ts","../src/locks/lock.ts","../src/locks/tryLock.ts","../src/locks/multiLock.ts","../src/locks/sharedLock.ts","../src/locks/uniqueLock.ts","../src/callOnce/callOnce.ts","../src/callOnce/onceFlag.ts","../src/semaphores/countingSemaphore.ts","../src/barriers/latch.ts"],"sourcesContent":["/**\n * Represents the possible status codes\n * returned by {@link ConditionVariable} operations.\n */\nexport type CVStatus = typeof CV_OK | typeof CV_TIMED_OUT;\n\n/**\n * The {@link ConditionVariable} was awakened via notification.\n */\nexport const CV_OK = \"ok\";\n\n/**\n * The {@link ConditionVariable} was awakened by timeout expiration.\n */\nexport const CV_TIMED_OUT = \"timed-out\";\n","// Generic\nexport const ERR_TIMEOUT = \"Operation timed out\";\n\nexport const ERR_NEGATIVE_VALUE = \"Value cannot be negative\";\n\nexport const ERR_OVERFLOW = \"Cannot exceed maximum value\";\n\n// Barriers\n\nexport const ERR_LATCH_INPUT_UNDERFLOW =\n \"Operation not permitted. Latch decrement cannot be negative\";\n\nexport const ERR_LATCH_INPUT_OVERFLOW =\n \"Operation not permitted. Latch decrement cannot exceed current count\";\n\n// Condition Variable\nexport const ERR_CV_VALUE = \"Unexpected value in shared memory location\";\n\n// Mutex\nexport const ERR_LOCK = \"A lock has encountered an error\";\n\nexport const ERR_LOCK_OWNERSHIP =\n \"Operation not permitted. Lock must be acquired first\";\n\nexport const ERR_LOCK_RELOCK =\n \"Attempted relock of already acquired lock. Deadlock would occur\";\n\nexport const ERR_LOCK_TIMEOUT = \"Timed out acquiring lock\";\n\n// Recursive Mutex\n\nexport const ERR_REC_MUTEX_OVERFLOW =\n \"Operation not permitted. Additional lock would exceed the maximum levels of ownership\";\n\n// Mutex Management\nexport const ERR_MULTI_LOCK = \"Failed to acquire all locks\";\n\nexport const ERR_MULTI_UNLOCK = \"Failed to unlock all locks\";\n\n// Semaphore\nexport const ERR_SEM_INPUT_NEG =\n \"Operation not permitted. Semaphore release value cannot be negative\";\n\nexport const ERR_SEM_INPUT_OVERFLOW =\n \"Operation not permitted. Semaphore release would cause overflow\";\n","import { ERR_LOCK } from \"./constants\";\n\n/**\n * Represents a generic error originating from a lock.\n */\nexport class LockError extends Error {\n /**\n * @param message - An optional custom error message.\n */\n constructor(message?: string) {\n super(message ?? ERR_LOCK);\n this.name = this.constructor.name;\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n","import type { BasicLockable } from \"../types/basicLockable\";\n\nimport { ERR_MULTI_LOCK } from \"./constants\";\nimport { LockError } from \"./lockError\";\n\n/**\n * Represents an error that occurs when attempting to lock multiple {@link BasicLockable} objects simultaneously.\n *\n * This error provides detailed information about the failure of locking operations, including specifics\n * about any errors that occurred. It ensures that any partial state due to errors can be adequately handled.\n */\nexport class MultiLockError extends LockError {\n /**\n * @param locks - The array of all lockable objects that were part of the operation.\n * @param numLocked - The number of locks successfully updated before failure.\n * @param lockErrors - An array of [index, error] pairs that contain the index of the lock in\n * the `locks` array and the error that occurred while attempting to lock it. Useful for\n * understanding why lock acquisition failed.\n * @param unlockErrors - An array of [index, error] pairs that contain the index of the lock in\n * the `locks` array and the error that occurred while attempting rollback. Useful for\n * debugging unexpected issues during unlocking.\n * @param message - An optional custom error message that describes the error.\n */\n constructor(\n public locks: BasicLockable[],\n public numLocked: number,\n public lockErrors: [number, unknown][] = [],\n public unlockErrors: [number, unknown][] = [],\n message?: string,\n ) {\n super(message ?? ERR_MULTI_LOCK);\n }\n}\n","import type { BasicLockable } from \"../types/basicLockable\";\n\nimport { ERR_MULTI_UNLOCK } from \"./constants\";\nimport { LockError } from \"./lockError\";\n\n/**\n * Represents an error that occurs when attempting to unlock multiple {@link BasicLockable} objects simultaneously.\n *\n * This error provides detailed information about the failure of unlocking operations, including specifics\n * about any errors that occurred. It ensures that any partial state due to errors can be adequately handled.\n */\nexport class MultiUnlockError extends LockError {\n /**\n * @param locks - The array of all lockable objects that were part of the operation.\n * @param numUnlocked - The number of unlocks successfully updated before failure.\n * @param unlockErrors - An array of [index, error] pairs that contain the index of the lock in\n * the `locks` array and the error that occurred while attempting to unlock it. Useful for\n * debugging unexpected issues during unlocking.\n * @param message - An optional custom error message that describes the error.\n */\n constructor(\n public locks: BasicLockable[],\n public numUnlocked: number,\n public unlockErrors: [number, unknown][] = [],\n message?: string,\n ) {\n super(message ?? ERR_MULTI_UNLOCK);\n }\n}\n","import { ERR_LOCK_OWNERSHIP } from \"./constants\";\nimport { LockError } from \"./lockError\";\n\n/**\n * Represents an ownership error originating from a lock.\n */\nexport class OwnershipError extends LockError {\n /**\n * @param message - An optional custom error message.\n */\n constructor(message?: string) {\n super(message ?? ERR_LOCK_OWNERSHIP);\n }\n}\n","import { ERR_LOCK_RELOCK } from \"./constants\";\nimport { LockError } from \"./lockError\";\n\n/**\n * Represents an error relocking a lock.\n */\nexport class RelockError extends LockError {\n /**\n * @param message - An optional custom error message.\n */\n constructor(message?: string) {\n super(message ?? ERR_LOCK_RELOCK);\n }\n}\n","import { ERR_TIMEOUT } from \"./constants\";\n\n/**\n * Represents an error that occurs when a process exceeds a set time.\n */\nexport class TimeoutError extends Error {\n /**\n * Absolute time in milliseconds after which the timeout error was thrown.\n * Can be `undefined` if not specified.\n */\n deadline?: number;\n\n /**\n * Duration in milliseconds after which the timeout error was thrown.\n * Can be `undefined` if not specified.\n */\n timeout?: number;\n\n /**\n * @param message - A custom error message. Defaults to `undefined`.\n * @param timeout - The timeout duration in milliseconds. Defaults to `undefined`.\n * @param deadline - The absolute time in milliseconds. Defaults to `undefined`.\n */\n constructor(message?: string, timeout?: number, deadline?: number) {\n super(message ?? ERR_TIMEOUT);\n this.deadline = deadline;\n this.timeout = timeout;\n this.name = TimeoutError.name;\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, TimeoutError);\n }\n }\n}\n","import type { Lockable } from \"../types/lockable\";\nimport type { SharedResource } from \"../types/sharedResource\";\nimport type { SyncLockable } from \"../types/sync/syncLockable\";\n\nimport { OwnershipError } from \"../errors/ownershipError\";\nimport { RelockError } from \"../errors/relockError\";\n\n/**\n * Represents the mutex lock state.\n */\nexport const LOCK_BIT = 1;\n\n/**\n * Provides synchronization across agents (main thread and workers)\n * to allow exclusive access to shared resources / blocks of code.\n *\n * A mutex is owned from the time an agent successfully locks it\n * and until the agent unlocks it. During ownership, any other agents\n * attempting to lock the mutex will block (or receive `false` from\n * `tryLock` methods). When unlocked, any blocked agent will have\n * the chance to acquire owernship.\n *\n * A locked mutex should not be relocked by the owner. Attempts\n * for additional locks will throw an error, and calls to `tryLock`\n * methods will return `false`.\n *\n * Behavior is undefined if:\n * - The mutex is destroyed while being owned.\n * - The agent is terminated while owning the mutex.\n * - The mutex's shared memory location is modified externally.\n *\n * @privateRemarks\n * 1. {@link https://en.cppreference.com/w/cpp/thread/mutex | C++ std::mutex}\n */\nexport class Mutex implements Lockable, SyncLockable, SharedResource {\n /**\n * The size in bytes of the mutex.\n */\n static readonly ByteLength = Int32Array.BYTES_PER_ELEMENT;\n\n /**\n * Indicates whether the current agent owns the lock.\n */\n protected _isOwner: boolean;\n\n /**\n * The shared memory for the mutex.\n */\n protected _mem: Int32Array;\n\n constructor();\n /**\n * @param sharedBuffer The {@link SharedArrayBuffer} that backs the mutex.\n * @param byteOffset The byte offset within `sharedBuffer`. Defaults to `0`.\n *\n * @throws A {@link RangeError} for any of the following:\n * - `byteOffset` is negative or not a multiple of `4`.\n * - The byte length of `sharedBuffer` is less than {@link ByteLength}.\n * - The space in `sharedBuffer` starting from `byteOffset` is less than {@link ByteLength}.\n */\n constructor(sharedBuffer: SharedArrayBuffer, byteOffset?: number);\n constructor(sharedBuffer?: SharedArrayBuffer, byteOffset = 0) {\n // Sanitize input\n sharedBuffer ??= new SharedArrayBuffer(Mutex.ByteLength);\n\n // Initialize properties\n this._isOwner = false;\n this._mem = new Int32Array(sharedBuffer, byteOffset, 1);\n\n // Initialize shared memory location\n Atomics.and(this._mem, 0, LOCK_BIT);\n }\n\n get buffer(): SharedArrayBuffer {\n return this._mem.buffer as SharedArrayBuffer;\n }\n\n get byteLength(): number {\n return this._mem.byteLength;\n }\n\n get byteOffset(): number {\n return this._mem.byteOffset;\n }\n\n get ownsLock(): boolean {\n return this._isOwner;\n }\n\n /**\n * @throws A {@link RelockError} If the lock is already locked by the caller.\n */\n async lock(): Promise<void> {\n // If already has lock\n if (this._isOwner) {\n throw new RelockError();\n }\n\n // Acquire lock\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n const res = Atomics.waitAsync(this._mem, 0, LOCK_BIT);\n if (res.async) {\n await res.value;\n }\n }\n this._isOwner = true;\n }\n\n /**\n * @throws A {@link RelockError} If the lock is already locked by the caller.\n */\n lockSync(): void {\n // If already has lock\n if (this._isOwner) {\n throw new RelockError();\n }\n\n // Acquire lock\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n Atomics.wait(this._mem, 0, LOCK_BIT);\n }\n this._isOwner = true;\n }\n\n tryLock(): boolean {\n return this.tryLockSync();\n }\n\n tryLockSync(): boolean {\n // If already has lock\n if (this._isOwner) {\n return false;\n }\n // Try to acquire lock\n return (this._isOwner = Atomics.or(this._mem, 0, LOCK_BIT) === 0);\n }\n\n /**\n * @throws An {@link OwnershipError} If the mutex is not owned by the caller.\n */\n unlock(): void {\n return this.unlockSync();\n }\n\n /**\n * @throws An {@link OwnershipError} If the mutex is not owned by the caller.\n */\n unlockSync(): void {\n // Check if lock owned\n if (!this._isOwner) {\n throw new OwnershipError();\n }\n // Release lock\n Atomics.store(this._mem, 0, 0);\n this._isOwner = false;\n // Notify blocked agents\n Atomics.notify(this._mem, 0);\n }\n}\n","export const MAX_INT32_VALUE = 2147483647; // (2**31) - 1\n\nexport const MIN_INT32_VALUE = -2147483648; // -(2**31)\n","import type { Lockable } from \"../types/lockable\";\nimport type { SharedResource } from \"../types/sharedResource\";\nimport type { SyncLockable } from \"../types/sync/syncLockable\";\n\nimport { ERR_REC_MUTEX_OVERFLOW } from \"../errors/constants\";\nimport { OwnershipError } from \"../errors/ownershipError\";\nimport { MAX_INT32_VALUE } from \"../utils/constants\";\n\n/**\n * Represents the mutex lock state.\n */\nexport const LOCK_BIT = 1;\n\n/**\n * Provides synchronization across agents (main thread and workers)\n * to allow exclusive recursive access to shared resources / blocks of code.\n *\n * A mutex is owned once an agent successfully locks it.\n * During ownership, the agent may acquire additional locks from the\n * mutex. Ownership ends when the agent releases all aquired locks.\n *\n * While owned, any other agents attempting to lock the mutex will\n * block (or receive `false` from `tryLock` methods). When unlocked,\n * any blocked agent will have the chance to acquire owernship.\n *\n * The maximum number of times a mutex can be locked recursively\n * is defined by {@link RecursiveMutex.Max}. Once reached, attempts\n * for additional locks will throw an error, and calls to `tryLock` methods\n * will return `false`.\n *\n * Behavior is undefined if:\n * - The mutex is destroyed while being owned.\n * - The agent is terminated while owning the mutex.\n * - The mutex's shared memory location is modified externally.\n *\n * @privateRemarks\n * 1. {@link https://en.cppreference.com/w/cpp/thread/recursive_mutex | C++ std::recursive_mutex}\n */\nexport class RecursiveMutex implements Lockable, SyncLockable, SharedResource {\n /**\n * The size in bytes of the mutex.\n */\n static readonly ByteLength = Int32Array.BYTES_PER_ELEMENT;\n\n /**\n * The maximum levels of recursive ownership.\n */\n static readonly Max = MAX_INT32_VALUE;\n\n /**\n * The number of locks acquired by the agent.\n */\n protected _depth: number;\n\n /**\n * The shared atomic memory for the mutex.\n */\n protected _mem: Int32Array;\n\n constructor();\n /**\n * @param sharedBuffer The {@link SharedArrayBuffer} that backs the mutex.\n * @param byteOffset The byte offset within `sharedBuffer`. Defaults to `0`.\n *\n * @throws A {@link RangeError} for any of the following:\n * - `byteOffset` is negative or not a multiple of `4`.\n * - The byte length of `sharedBuffer` is less than {@link ByteLength}.\n * - The space in `sharedBuffer` starting from `byteOffset` is less than {@link ByteLength}.\n */\n constructor(sharedBuffer: SharedArrayBuffer, byteOffset?: number);\n constructor(sharedBuffer?: SharedArrayBuffer, byteOffset = 0) {\n // Sanitize input\n sharedBuffer ??= new SharedArrayBuffer(RecursiveMutex.ByteLength);\n\n // Initialize properties\n this._depth = 0;\n this._mem = new Int32Array(sharedBuffer, byteOffset, 1);\n\n // Initialize shared memory location\n Atomics.and(this._mem, 0, LOCK_BIT);\n }\n\n get buffer(): SharedArrayBuffer {\n return this._mem.buffer as SharedArrayBuffer;\n }\n\n get byteLength(): number {\n return this._mem.byteLength;\n }\n\n get byteOffset(): number {\n return this._mem.byteOffset;\n }\n\n get ownsLock(): boolean {\n return this._depth > 0;\n }\n\n /**\n * @throws A {@link RangeError} If the mutex is already locked the maximum amount of times.\n */\n async lock(): Promise<void> {\n // If at capacity\n if (this._depth === RecursiveMutex.Max) {\n throw new RangeError(ERR_REC_MUTEX_OVERFLOW);\n }\n\n // Acquire lock if not owned\n if (this._depth === 0) {\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n await Atomics.waitAsync(this._mem, 0, LOCK_BIT).value;\n }\n }\n\n // Increment ownership\n ++this._depth;\n }\n\n /**\n * @throws A {@link RangeError} If the mutex is already locked the maximum amount of times.\n */\n lockSync(): void {\n // If at capacity\n if (this._depth === RecursiveMutex.Max) {\n throw new RangeError(ERR_REC_MUTEX_OVERFLOW);\n }\n\n // Acquire lock if not owned\n if (this._depth === 0) {\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n Atomics.wait(this._mem, 0, LOCK_BIT);\n }\n }\n\n // Increment ownership\n ++this._depth;\n }\n\n tryLock(): boolean {\n return this.tryLockSync();\n }\n\n tryLockSync(): boolean {\n // If at capacity\n if (this._depth === RecursiveMutex.Max) {\n return false;\n }\n\n // Try to acquire lock if not owned\n if (this._depth === 0 && Atomics.or(this._mem, 0, LOCK_BIT)) {\n return false;\n }\n\n // Increment ownership\n ++this._depth;\n return true;\n }\n\n /**\n * @throws A {@link OwnershipError} If the mutex is not owned by the caller.\n */\n unlock(): void {\n return this.unlockSync();\n }\n\n /**\n * @throws A {@link OwnershipError} If the mutex is not owned by the caller.\n */\n unlockSync(): void {\n // Check if lock owned\n if (this._depth <= 0) {\n throw new OwnershipError();\n }\n\n // Check if lock owned recursively\n if (this._depth > 1) {\n --this._depth;\n return;\n }\n\n // Release lock\n Atomics.store(this._mem, 0, 0);\n this._depth = 0;\n\n // Notify blocked agents\n Atomics.notify(this._mem, 0);\n }\n}\n","/**\n * Represents the possible status codes returned by {@link Atomics} operations.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics| Atomics}\n */\nexport type AtomicsStatus =\n | typeof ATOMICS_NOT_EQUAL\n | typeof ATOMICS_OK\n | typeof ATOMICS_TIMED_OUT;\n\n/**\n * Indicates that the expected value was not found at the atomic location.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync | Atomics}\n */\nexport const ATOMICS_NOT_EQUAL = \"not-equal\";\n\n/**\n * Indicates the {@link Atomics} operation completed successfully.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics | Atomics}\n */\nexport const ATOMICS_OK = \"ok\";\n\n/**\n * Indicates the {@link Atomics} operation\n * did not complete within the given time.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync | Atomics}\n */\nexport const ATOMICS_TIMED_OUT = \"timed-out\";\n","import { ATOMICS_TIMED_OUT } from \"../types/atomicsStatus\";\nimport type { SyncTimedLockable } from \"../types/sync/syncTimedLockable\";\nimport type { TimedLockable } from \"../types/timedLockable\";\n\nimport { LOCK_BIT, RecursiveMutex } from \"./recursiveMutex\";\n\n/**\n * Provides synchronization across agents (main thread and workers)\n * to allow exclusive recursive access to shared resources / blocks of code.\n *\n * A mutex is owned once an agent successfully locks it.\n * During ownership, the agent may acquire additional locks from the\n * mutex. Ownership ends when the agent releases all aquired locks.\n *\n * While owned, any other agents attempting to lock the mutex will\n * block (or receive `false` from `tryLock` methods). When unlocked,\n * any blocked agent will have the chance to acquire owernship.\n *\n * The maximum number of times a mutex can be locked recursively\n * is defined by {@link RecursiveTimedMutex.Max}. Once reached, attempts\n * for additional locks will throw an error, and calls to `tryLock` methods\n * will return `false`.\n *\n * Behavior is undefined if:\n * - The mutex is destroyed while being owned.\n * - The agent is terminated while owning the mutex.\n * - The mutex's shared memory location is modified externally.\n *\n * Timeout precision for time-based methods may vary due to system load\n * and inherent limitations of JavaScript timing. Developers should\n * consider this possible variability in their applications.\n *\n * @privateRemarks\n * 1. {@link https://en.cppreference.com/w/cpp/thread/recursive_mutex | C++ std::recursive_mutex}\n */\nexport class RecursiveTimedMutex\n extends RecursiveMutex\n implements TimedLockable, SyncTimedLockable\n{\n async tryLockFor(timeout: number): Promise<boolean> {\n return this.tryLockUntil(performance.now() + timeout);\n }\n\n tryLockForSync(timeout: number): boolean {\n return this.tryLockUntilSync(performance.now() + timeout);\n }\n\n async tryLockUntil(timestamp: number): Promise<boolean> {\n // If at capacity\n if (this._depth === RecursiveTimedMutex.Max) {\n return false;\n }\n\n // If not owned, try to acquire lock for a given amount of time\n if (this._depth === 0) {\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n const timeout = timestamp - performance.now();\n const res = Atomics.waitAsync(this._mem, 0, LOCK_BIT, timeout);\n const value = res.async ? await res.value : res.value;\n // If time expired\n if (value === ATOMICS_TIMED_OUT) {\n return false;\n }\n }\n }\n\n // Increment ownership\n ++this._depth;\n return true;\n }\n\n tryLockUntilSync(timestamp: number): boolean {\n // If at capacity\n if (this._depth === RecursiveTimedMutex.Max) {\n return false;\n }\n\n // If not owned, try to acquire lock for a given amount of time\n if (this._depth === 0) {\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n const timeout = timestamp - performance.now();\n const value = Atomics.wait(this._mem, 0, LOCK_BIT, timeout);\n // If time expired\n if (value === ATOMICS_TIMED_OUT) {\n return false;\n }\n }\n }\n\n // Increment ownership\n ++this._depth;\n return true;\n }\n}\n","import type { BasicLockable } from \"../types/basicLockable\";\nimport type { SyncBasicLockable } from \"../types/sync/syncBasicLockable\";\n\n/**\n * Acquires the mutex and executes the provided callback, automatically\n * unlocking afterwards. Blocks until the lock is available.\n *\n * @param mutex The mutex to acquire.\n * @param callbackfn The callback function.\n *\n * @returns A promise resolved to the return value of `callbackfn`.\n */\nexport async function lockGuard<T>(\n mutex: BasicLockable,\n callbackfn: () => T | Promise<T>,\n): Promise<T> {\n // Acquire lock\n await mutex.lock();\n try {\n // Execute callback\n return await callbackfn();\n } finally {\n // Release lock\n await mutex.unlock();\n }\n}\n\n/**\n * Acquires the mutex and executes the provided callback, automatically\n * unlocking afterwards. Blocks until the lock is available.\n *\n * @param mutex The mutex to acquire.\n * @param callbackfn The callback function.\n *\n * @returns The return value of `callbackfn`.\n */\nexport function lockGuardSync<T>(\n mutex: SyncBasicLockable,\n callbackfn: () => T,\n): T {\n // Acquire lock\n mutex.lockSync();\n try {\n // Execute callback\n return callbackfn();\n } finally {\n // Release lock\n mutex.unlockSync();\n }\n}\n","import { ATOMICS_NOT_EQUAL, ATOMICS_TIMED_OUT } from \"../types/atomicsStatus\";\nimport type { BasicLockable } from \"../types/basicLockable\";\nimport { type CVStatus, CV_OK, CV_TIMED_OUT } from \"../types/cvStatus\";\nimport type { SharedResource } from \"../types/sharedResource\";\n\nimport { ERR_CV_VALUE } from \"../errors/constants\";\nimport { OwnershipError } from \"../errors/ownershipError\";\n\n/**\n * A condition variable manages an atomic wait/block mechanism that\n * is tightly coupled with a mutex for safe cross-agent synchronization.\n *\n * Behavior is undefined if:\n * - The shared memory location is modified externally.\n *\n * @privateRemarks\n * 1. {@link https://en.cppreference.com/w/cpp/thread/condition_variable | C++ std::condition_variable}\n * 1. {@link https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html | Alexander Terekhov, Howard Hinnant. (2007-09-09). Mutex, Lock, Condition Variable Rationale}\n */\nexport class ConditionVariable implements SharedResource {\n /**\n * The size in bytes of the condition variable.\n */\n static readonly ByteLength = Int32Array.BYTES_PER_ELEMENT;\n\n /**\n * The shared atomic memory where the condition variable stores its state.\n */\n private _mem: Int32Array;\n\n constructor();\n /**\n * @param sharedBuffer The {@link SharedArrayBuffer} that backs the condition variable.\n * @param byteOffset The byte offset within `sharedBuffer`. Defaults to `0`.\n *\n * @throws A {@link RangeError} for any of the following:\n * - `byteOffset` is negative or not a multiple of `4`.\n * - The byte length of `sharedBuffer` is less than {@link ByteLength}.\n * - The space in `sharedBuffer` starting from `byteOffset` is less than {@link ByteLength}.\n */\n constructor(sharedBuffer: SharedArrayBuffer, byteOffset?: number);\n constructor(sharedBuffer?: SharedArrayBuffer, byteOffset = 0) {\n // Sanitize input\n sharedBuffer ??= new SharedArrayBuffer(ConditionVariable.ByteLength);\n\n // Initialize properties\n this._mem = new Int32Array(sharedBuffer, byteOffset, 1);\n\n // Initialize shared memory location\n Atomics.store(this._mem, 0, 0);\n }\n\n get buffer(): SharedArrayBuffer {\n return this._mem.buffer as SharedArrayBuffer;\n }\n\n get byteLength(): number {\n return this._mem.byteLength;\n }\n\n get byteOffset(): number {\n return this._mem.byteOffset;\n }\n\n /**\n * Notify waiting agents that are blocked on this condition variable.\n *\n * @param count - The number of agents to notify.\n *\n * @returns The number of agents that were notified.\n */\n notify(count: number): number {\n return Atomics.notify(this._mem, 0, count);\n }\n\n /**\n * Notify all waiting agents that are blocked on this condition variable.\n *\n * @returns The number of agents that were notified.\n */\n notifyAll(): number {\n return Atomics.notify(this._mem, 0);\n }\n\n /**\n * Notify one waiting agent that is blocked on this condition variable.\n *\n * @returns The number of agents that were notified.\n */\n notifyOne(): number {\n return Atomics.notify(this._mem, 0, 1);\n }\n\n /**\n * Blocks the current agent until this condition variable is notified.\n * The associated mutex is released before blocking and re-acquired\n * after waking up.\n *\n * @param mutex The mutex that must be locked by the current agent.\n *\n * @throws An {@link OwnershipError} If the mutex is not owned by the caller.\n * @throws A {@link RangeError} If the shared memory data is unexpected.\n */\n async wait(mutex: BasicLockable): Promise<void> {\n await this.waitFor(mutex, Infinity);\n }\n\n /**\n * Blocks the current agent until this condition variable is notified,\n * or an optional timeout expires. The associated mutex is released\n * before blocking and re-acquired after waking up.\n *\n * @param mutex The mutex that must be locked by the current agent.\n * @param timeout A timeout in milliseconds after which the wait is aborted.\n *\n * @throws An {@link OwnershipError} If the mutex is not owned by the caller.\n * @throws A {@link RangeError} If the shared memory data is unexpected.\n *\n * @returns A {@link CVStatus} representing the result of the operation.\n */\n async waitFor(mutex: BasicLockable, timeout: number): Promise<CVStatus> {\n // Check mutex is owned\n if (!mutex.ownsLock) {\n throw new OwnershipError();\n }\n try {\n // Start waiting BEFORE releasing mutex\n const res = Atomics.waitAsync(this._mem, 0, 0, timeout);\n // Release mutex\n await mutex.unlock();\n // Wait for notification\n const value = res.async ? await res.value : res.value;\n // Check for unexpected value at shared memory location\n if (value === ATOMICS_NOT_EQUAL) {\n throw new RangeError(ERR_CV_VALUE);\n }\n // Return status\n return value === ATOMICS_TIMED_OUT ? CV_TIMED_OUT : CV_OK;\n } finally {\n // Re-acquire mutex\n await mutex.lock();\n }\n }\n\n /**\n * Blocks the current agent until this condition variable is notified,\n * or until a specified point in time is reached. The associated mutex\n * is released before blocking and re-acquired after waking up.\n *\n * @param mutex The mutex that must be locked by the current agent.\n * @param timestamp The absolute time in milliseconds at which the wait is aborted.\n *\n * @throws A {@link OwnershipError} If the mutex is not owned by the caller.\n * @throws A {@link RangeError} If the shared memory data is unexpected.\n *\n * @returns A {@link CVStatus} representing the result of the operation.\n */\n async waitUntil(mutex: BasicLockable, timestamp: number): Promise<CVStatus> {\n return this.waitFor(mutex, timestamp - performance.now());\n }\n}\n","import type { TimedLockable } from \"../types/timedLockable\";\nimport { ATOMICS_TIMED_OUT } from \"../types/atomicsStatus\";\nimport type { SyncTimedLockable } from \"../types/sync/syncTimedLockable\";\n\nimport { LOCK_BIT, Mutex } from \"./mutex\";\n\n/**\n * Provides synchronization across agents (main thread and workers)\n * to allow exclusive access to shared resources / blocks of code.\n *\n * A mutex is owned from the time an agent successfully locks it\n * and until the agent unlocks it. During ownership, any other agents\n * attempting to lock the mutex will block (or receive `false` from\n * `tryLock` methods). When unlocked, any blocked agent will have\n * the chance to acquire owernship.\n *\n * A locked mutex should not be relocked by the owner. Attempts\n * for additional locks will throw an error, and calls to `tryLock`\n * methods will return `false`.\n *\n * Behavior is undefined if:\n * - The mutex is destroyed while being owned.\n * - The agent is terminated while owning the mutex.\n * - The mutex's shared memory location is modified externally.\n *\n * Timeout precision for time-based methods may vary due to system load\n * and inherent limitations of JavaScript timing. Developers should\n * consider this possible variability in their applications.\n *\n * @privateRemarks\n * 1. {@link https://en.cppreference.com/w/cpp/thread/unique_lock | C++ std::unique_lock}\n */\nexport class TimedMutex\n extends Mutex\n implements TimedLockable, SyncTimedLockable\n{\n async tryLockFor(timeout: number): Promise<boolean> {\n return this.tryLockUntil(performance.now() + timeout);\n }\n\n tryLockForSync(timeout: number): boolean {\n return this.tryLockUntilSync(performance.now() + timeout);\n }\n\n async tryLockUntil(timestamp: number): Promise<boolean> {\n // If already has lock\n if (this._isOwner) {\n return false;\n }\n // Try to acquire lock for a given amount of time\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n const timeout = timestamp - performance.now();\n const res = Atomics.waitAsync(this._mem, 0, LOCK_BIT, timeout);\n const value = res.async ? await res.value : res.value;\n // If time expired\n if (value === ATOMICS_TIMED_OUT) {\n return false;\n }\n }\n return (this._isOwner = true);\n }\n\n tryLockUntilSync(timestamp: number): boolean {\n // If already has lock\n if (this._isOwner) {\n return false;\n }\n // Try to acquire lock for a given amount of time\n while (Atomics.or(this._mem, 0, LOCK_BIT)) {\n const timeout = timestamp - performance.now();\n const value = Atomics.wait(this._mem, 0, LOCK_BIT, timeout);\n // If time expired\n if (value === ATOMICS_TIMED_OUT) {\n return false;\n }\n }\n return (this._isOwner = true);\n }\n}\n","import type { Lockable } from \"../types/lockable\";\nimport type { SharedResource } from \"../types/sharedResource\";\nimport type { SharedLockable } from \"../types/sharedLockable\";\n\nimport { OwnershipError } from \"../errors/ownershipError\";\nimport { RelockError } from \"../errors/relockError\";\nimport { lockGuard } from \"../locks/lockGuard\";\n\nimport { ConditionVariable } from \"../condVars/conditionVariable\";\nimport { TimedMutex } from \"./timedMutex\";\n\nexport const WRITE_BIT = 1 << 31;\nexport const READ_BITS = ~WRITE_BIT;\n\n/**\n * Provides synchronization across agents (main thread and workers)\n * to allow exclusive and shared access to resources / blocks of code.\n *\n * If one agent has acquired an exclusive lock, no other agents can acquire\n * the mutex. If one agent has acquired a shared lock, other agents can still\n * acquire the shared lock, but cannot acquire an exclusive lock. Within one\n * agent, only one lock (shared or exclusive) can be acquired at the same time.\n *\n * Shared mutexes are useful when shared data can be safely read by any number\n * of agents simultaneously, but should be written to by only one agent at a\n * time, and not readable by other agents during writing.\n *\n * Behavior is undefined if:\n * - The mutex is destroyed while being owned.\n * - The agent is terminated while owning the mutex.\n * - The mutex's shared memory location is modified externally.\n *\n * @privateRemarks\n * 1. {@link https://en.cppreference.com/w/cpp/thread/shared_mutex | C++ std::shared_mutex}\n * 1. {@link https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html | Alexander Terekhov, Howard Hinnant. (2007-09-09). Mutex, Lock, Condition Variable Rationale}\n */\nexport class SharedMutex implements Lockable, SharedLockable, SharedResource {\n /**\n * The size in bytes of the mutex.\n */\n static readonly ByteLength = 4 * Int32Array.BYTES_PER_ELEMENT;\n\n protected _gate1: ConditionVariable;\n protected _gate2: ConditionVariable;\n protected _isReader: boolean;\n protected _isWriter: boolean;\n protected _mem: Int32Array;\n protected _mutex: TimedMutex;\n\n constructor();\n /**\n * @param sharedBuffer The {@link SharedArrayBuffer} that backs the mutex.\n * @param byteOffset The byte offset within `sharedBuffer`. Defaults to `0`.\n *\n * @throws A {@link RangeError} for any of the following:\n * - `byteOffset` is negative or not a multiple of `4`.\n * - The byte length of `sharedBuffer` is less than {@link ByteLength}.\n * - The space in `sharedBuffer` starting from `byteOffset` is less than {@link ByteLength}.\n */\n constructor(sharedBuffer: SharedArrayBuffer, byteOffset?: number);\n constructor(sharedBuffer?: SharedArrayBuffer, byteOffset = 0) {\n const bInt32 = Int32Array.BYTES_PER_ELEMENT;\n\n // Sanitize input\n sharedBuffer ??= new SharedArrayBuffer(SharedMutex.ByteLength);\n\n // Initialize properties\n this._mem = new Int32Array(sharedBuffer, byteOffset, 4);\n byteOffset += bInt32;\n this._mutex = new TimedMutex(sharedBuffer, byteOffset);\n byteOffset += bInt32;\n this._gate1 = new ConditionVariable(sharedBuffer, byteOffset);\n byteOffset += bInt32;\n this._gate2 = new ConditionVariable(sharedBuffer, byteOffset);\n this._isReader = false;\n this._isWriter = false;\n }\n\n get buffer(): SharedArrayBuffer {\n return this._mem.buffer as SharedArrayBuffer;\n }\n\n get byteLength(): number {\n return this._mem.byteLength;\n }\n\n get byteOffset(): number {\n return this._mem.byteOffset;\n }\n\n get ownsLock(): boolean {\n return this._isWriter;\n }\n\n get ownsSharedLock(): boolean {\n return this._isReader;\n }\n\n // Exclusive\n\n /**\n * @throws A {@link RelockError} If the mutex is already locked by the caller.\n */\n async lock(): Promise<void> {\n // If already has lock\n if (this._isWriter || this._isReader) {\n throw new RelockError();\n }\n\n // Acquire internal lock\n await lockGuard(this._mutex, async () => {\n // Acquire write lock\n while (Atomics.or(this._mem, 0, WRITE_BIT) & WRITE_BIT) {\n await this._gate1.wait(this._mutex);\n }\n this._isWriter = true;\n\n // Wait until no readers\n while (Atomics.load(this._mem, 0) & READ_BITS) {\n await this._gate2.wait(this._mutex);\n }\n });\n }\n\n tryLock(): boolean {\n // If already has lock\n if (this._isWriter || this._isReader) {\n return false;\n }\n\n // Try to acquire internal lock\n if (this._mutex.tryLock()) {\n try {\n // Try to acquire write lock\n this._isWriter =\n Atomics.compareExchange(this._mem, 0, 0, WRITE_BIT) === 0;\n } finally {\n // Release internal lock\n this._mutex.unlock();\n }\n }\n\n // Return result\n return this._isWriter;\n }\n\n /**\n * @throws A {@link OwnershipError} If the mutex is not owned by the caller.\n */\n async unlock(): Promise<void> {\n // Check if write lock owned\n if (!this._isWriter) {\n throw new OwnershipError();\n }\n\n // Acquire internal lock\n await lockGuard(this._mutex, () => {\n // Release write lock\n Atomics.and(this._mem, 0, READ_BITS);\n this._isWriter = false;\n });\n\n // Notify agents waiting on mutex\n this._gate1.notifyAll();\n }\n\n // Shared\n\n /**\n * @throws A {@link RelockError} If the lock is already locked by the caller.\n */\n async lockShared(): Promise<void> {\n // If already has lock\n if (this._isReader || this._isWriter) {\n throw new RelockError();\n }\n\n // Acquire internal lock\n await lockGuard(this._mutex, async () => {\n // Wait until there's no writer and there's read capacity\n let state = Atomics.load(this._mem, 0);\n while (state & WRITE_BIT || (state & READ_BITS) === READ_BITS) {\n await this._gate1.wait(this._mutex);\n state = Atomics.load(this._mem, 0);\n }\n\n // Acquire a read lock\n Atomics.add(this._mem, 0, 1);\n this._isReader = true;\n });\n }\n\n tryLockShared(): boolean {\n // If already has lock\n if (this._isReader || this._isWriter) {\n return false;\n }\n\n // Try to acquire internal lock\n if (this._mutex.tryLock()) {\n try {\n // Check for writers and read capacity\n const state = Atomics.load(this._mem, 0);\n if (state & WRITE_BIT || (state & READ_BITS) === READ_BITS) {\n return false;\n }\n\n // Try to acquire read lock\n this._isReader =\n Atomics.compareExchange(this._mem, 0, state, state + 1) === state;\n } finally {\n // Release internal lock\n this._mutex.unlock();\n }\n }\n\n // Return result\n return this._isReader;\n }\n\n /**\n * @throws An {@link OwnershipError} If the mutex is not owned by the caller.\n */\n async unlockShared(): Promise<void> {\n // Check if read lock owned\n if (!this._isReader) {\n throw new OwnershipError();\n }\n\n // Acquire internal lock\n await lockGuard(this._mutex, () => {\n // Release read lock\n const state = Atomics.sub(this._mem, 0, 1);\n this._isReader = false;\n\n // If there are blocked writers\n if (state & WRITE_BIT) {\n // And no more readers\n if ((state & READ_BITS) === 1) {\n // Notify blocked writers\n this._gate2.notifyAll();\n }\n } else if (state === READ_BITS) {\n // If there are no writers\n // and readers no longer at capacity,\n // then notify blocked readers\n this._gate1.notifyAll();\n }\n });\n }\n}\n","import { CV_TIMED_OUT } from \"../types/cvStatus\";\nimport { SharedTimedLockable } from \"../types/sharedTimedLockable\";\nimport { TimedLockable } from \"../types/timedLockable\";\n\nimport { READ_BITS, SharedMutex, WRITE_BIT } from \"./sharedMutex\";\n\n/**\n * Provides synchronization across agents (main thread and workers)\n * to allow exclusive and shared access to resources / blocks of code.\n *\n * If one agent has acquired an exclusive lock, no other agents can acquire\n * the mutex. If one agent has acquired a shared lock, other agents can still\n * acquire the shared lock, but cannot acquire an exclusive lock. Within one\n * agent, only one lock (shared or exclusive) can be acquired at the same time.\n *\n * Shared mutexes are useful when shared data can be safely read by any number\n * of agents simultaneously, but should be written to by only one agent at a\n * time, and not readable by other agents during writing.\n *\n * Behavior is undefined if:\n * - The mutex is destroyed while being owned.\n * - The agent is terminated while owning the mutex.\n * - The mutex's shared memory location is modified externally.\n *\n * Timeout precision for time-based methods may vary due to system load\n * and inherent limitations of JavaScript timing. Developers should\n * consider this possible variability in their applications.\n *\n * @privateRemarks\n * 1. {@link https://en.cppreference.com/w/cpp/thread/shared_timed_mutex | C++ std::shared_timed)mutex}\n * 1. {@link https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2406.html | Alexander Terekhov, Howard Hinnant. (2007-09-09). Mutex, Lock, Condition Variable Rationale}\n */\nexport class SharedTimedMutex\n extends SharedMutex\n implements TimedLockable, SharedTimedLockable\n{\n async tryLockFor(timeout: number): Promise<boolean> {\n return this.tryLockUntil(performance.now() + timeout);\n }\n\n async tryLockUntil(timestamp: number): Promise<boolean> {\n // If already has lock\n if (this._isWriter || this._isReader) {\n return false;\n }\n\n // Try to acquire internal lock\n if (!(await this._mutex.tryLockUntil(timestamp))) {\n return false;\n }\n\n let notify = false;\n\n try {\n // Try to acquire write lock\n while (Atomics.or(this._mem, 0, WRITE_BIT) & WRITE_BIT) {\n const res = await this._gate1.waitUntil(this._mutex, timestamp);\n\n // If timed out, stop\n if (res === CV_TIMED_OUT) {\n return false;\n }\n }\n this._isWriter = true;\n\n // Wait until no readers\n while (Atomics.load(this._mem, 0) & READ_BITS) {\n const res = await this._gate2.waitUntil(this._mutex, timestamp);\n\n // If timed out, release write lock\n if (res === CV_TIMED_OUT) {\n notify = true;\n Atomics.and(this._mem, 0, READ_BITS);\n this._isWriter = false;\n return false;\n }\n }\n\n // Return success\n return true;\n } finally {\n // Release internal lock\n await this._mutex.unlock();\n\n // Notify agents waiting on mutex\n if (notify) {\n this._gate1.notifyAll();\n }\n }\n }\n\n async tryLockSharedFor(timeout: number): Promise<boolean> {\n return this.tryLockSharedUntil(performance.now() + timeout);\n }\n\n async tryLockSharedUntil(timestamp: number): Promise<boolean> {\n // If already has lock\n if (this._isReader || this._isWriter) {\n return false;\n }\n\n // Try to acquire internal lock\n if (!(await this._mutex.tryLockUntil(timestamp))) {\n return false;\n }\n\n try {\n // Wait until there's no writer and there's read capacity\n let state = Atomics.load(this._mem, 0);\n while (state & WRITE_BIT || state === READ_BITS) {\n const res = await this._gate1.waitUntil(this._mutex, timestamp);\n\n // If timed out, stop\n if (res === CV_TIMED_OUT) {\n return false;\n }\n\n state = Atomics.load(this._mem, 0);\n }\n\n // Acquire a read lock\n Atomics.add(this._mem, 0, 1);\n this._isReader = true;\n\n // Return success\n return true;\n } finally {\n // Release internal lock\n await this._mutex.unlock();\n }\n }\n}\n","import { MultiLockError } from \"../errors/multiLockError\";\nimport { BasicLockable } from \"../types/basicLockable\";\n\n/**\n * Sequentially locks the provided {@link BasicLockable} objects.\n *\n * If any lock acquisition fails, the process is halted\n * and previously acquired locks are released in reverse order.\n *\n * @param locks - An array of lockable objects to be locked sequentially.\n *\n * @throws A {@link MultiLockError} if an error occurs trying to acquire all\n * locks. Details include:\n * - `locks`: The array of all locks.\n * - `numLocked`: The number of locks successfully acquired before failure.\n * - `lockErrors`: Errors encountered while trying to acquire all locks.\n * - `unlockErrors`: Errors encountered while trying to roll back acquired locks.\n */\nexport async function lock(...locks: BasicLockable[]): Promise<void> {\n const N = locks.length;\n const lockErrors: [number, unknown][] = [];\n\n // Lock each lock. Stop at first error\n let numLocked = N;\n for (let i = 0; i < N; ++i) {\n try {\n await locks[i].lock();\n } catch (err) {\n lockErrors.push([i, err]);\n numLocked = i;\n break;\n }\n }\n\n // If successful\n if (numLocked === N) {\n return;\n }\n\n // Unlock acquired locks. Collect any errors\n const unlockErrors: [number, unknown][] = [];\n for (let i = numLocked - 1; i >= 0; --i) {\n try {\n await locks[i].unlock();\n } catch (err) {\n unlockErrors.push([i, err]);\n }\n }\n\n throw new MultiLockError(locks, numLocked, lockErrors, unlockErrors);\n}\n","import type { Lockable } from \"../types/lockable\";\n\nimport { MultiLockError } from \"../errors/multiLockError\";\nimport { MultiUnlockError } from \"../errors/multiUnlockError\";\n\n/**\n * Tries to sequentially lock the provided {@link Lockable} objects.\n *\n * If any lock acquisition fails, the process is halted\n * and previously acquired locks are released in reverse order.\n *\n * @param locks - An array of lockable objects to be locked sequentially.\n *\n * @throws A {@link MultiLockError} if an error occurs trying to acquire all\n * locks. Details include:\n * - `locks`: The array of all locks.\n * - `numLocked`: The number of locks successfully acquired before failure.\n * - `lockErrors`: Errors encountered while trying to acquire all locks.\n * - `unlockErrors`: Errors encountered while trying to roll back acquired locks.\n *\n * @throws A {@link MultiUnlockError} if, after lock failure, an errors occurs\n * while trying to roll back acquired locks. Details include:\n * - `locks`: The array of all locks.\n * - `numUnlocked`: The number of locks successfully unlocked.\n * - `unlockErrors`: Errors encountered while trying to roll back acquired locks.\n *\n * @returns `-1` if all locks are successfully acquired, otherwise the 0-based index of the lock that failed.\n */\nexport async function tryLock(...locks: Lockable[]): Promise<number> {\n const N = locks.length;\n const lockErrors: [number, unknown][] = [];\n\n // Lock each lock. Stop at first failure\n let numLocked = N;\n for (let i = 0; i < N; ++i) {\n try {\n if (!(await locks[i].tryLock())) {\n numLocked = i;\n break;\n }\n } catch (err) {\n lockErrors.push([i, err]);\n numLocked = i;\n break;\n }\n }\n\n // If successful\n if (numLocked === N) {\n return -1;\n }\n\n // If immediate failure\n if (numLocked < 1 && lockErrors.length < 1) {\n return numLocked;\n }\n\n // Unlock acquired locks. Collect any errors\n const unlockErrors: [number, unknown][] = [];\n for (let i = numLocked - 1; i >= 0; --i) {\n try {\n await locks[i].unlock();\n } catch (err) {\n unlockErrors.push([i, err]);\n }\n }\n\n // If errors occurred\n if (lockErrors.length > 0) {\n throw new MultiLockError(locks, numLocked, lockErrors, unlockErrors);\n }\n if (unlockErrors.length > 0) {\n const numUnlocked = numLocked - unlockErrors.length;\n throw new MultiUnlockError(locks, numUnlocked, unlockErrors);\n }\n\n // Return 0-based index of failed lock\n return numLocked;\n}\n","import type { BasicLockable } from \"../types/basicLockable\";\nimport type { Lockable } from \"../types/lockable\";\n\nimport { MultiUnlockError } from \"../errors/multiUnlockError\";\nimport { lock } from \"./lock\";\nimport { tryLock } from \"./tryLock\";\n\n/**\n * A mutex ownership wrapper.\n *\n * Locking a MultiLock exclusively locks the associated mutexes.\n *\n * If the given mutexes implement {@link Lockable}, then MultiLock will too.\n * Otherwise, using attempted locking (`tryLock`) will result in errors.\n */\nexport class MultiLock implements Lockable {\n /**\n * Indicates whether the current agent owns the lock.\n */\n protected _isOwner: boolean;\n\n /**\n * The associated basic lockable.\n */\n mutexes: BasicLockable[];\n\n /**\n * @param mutexes - The basic lockables to associate.\n */\n constructor(...mutexes: BasicLockable[]) {\n this._isOwner = false;\n this.mutexes = mutexes;\n }\n\n get ownsLock(): boolean {\n return this._isOwner;\n }\n\n async lock(): Promise<void> {\n await lock(...this.mutexes);\n this._isOwner = true;\n }\n\n /**\n * Exchange internal state\n */\n swap(other: MultiLock): void {\n // Swap ownership\n const tIsOwner = this._isOwner;\n this._isOwner = other._isOwner;\n other._isOwner = tIsOwner;\n // Swap mutexes\n const tMutexes = this.mutexes;\n this.mutexes = other.mutex