semafy
Version:
A robust cross-agent synchronization library.
1,354 lines (1,325 loc) • 41.5 kB
JavaScript
/*!
* semafy
* https://github.com/havelessbemore/semafy
*
* MIT License
*
* Copyright (C) 2024-2024 Michael Rojas <dev.michael.rojas@gmail.com> (https://github.com/havelessbemore)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const CV_OK = "ok";
const CV_TIMED_OUT = "timed-out";
const ERR_TIMEOUT = "Operation timed out";
const ERR_NEGATIVE_VALUE = "Value cannot be negative";
const ERR_OVERFLOW = "Cannot exceed maximum value";
const ERR_LATCH_INPUT_UNDERFLOW = "Operation not permitted. Latch decrement cannot be negative";
const ERR_LATCH_INPUT_OVERFLOW = "Operation not permitted. Latch decrement cannot exceed current count";
const ERR_CV_VALUE = "Unexpected value in shared memory location";
const ERR_LOCK = "A lock has encountered an error";
const ERR_LOCK_OWNERSHIP = "Operation not permitted. Lock must be acquired first";
const ERR_LOCK_RELOCK = "Attempted relock of already acquired lock. Deadlock would occur";
const ERR_REC_MUTEX_OVERFLOW = "Operation not permitted. Additional lock would exceed the maximum levels of ownership";
const ERR_MULTI_LOCK = "Failed to acquire all locks";
const ERR_MULTI_UNLOCK = "Failed to unlock all locks";
const ERR_SEM_INPUT_NEG = "Operation not permitted. Semaphore release value cannot be negative";
const ERR_SEM_INPUT_OVERFLOW = "Operation not permitted. Semaphore release would cause overflow";
class LockError extends Error {
/**
* @param message - An optional custom error message.
*/
constructor(message) {
super(message ?? ERR_LOCK);
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
class MultiLockError extends LockError {
/**
* @param locks - The array of all lockable objects that were part of the operation.
* @param numLocked - The number of locks successfully updated before failure.
* @param lockErrors - An array of [index, error] pairs that contain the index of the lock in
* the `locks` array and the error that occurred while attempting to lock it. Useful for
* understanding why lock acquisition failed.
* @param unlockErrors - An array of [index, error] pairs that contain the index of the lock in
* the `locks` array and the error that occurred while attempting rollback. Useful for
* debugging unexpected issues during unlocking.
* @param message - An optional custom error message that describes the error.
*/
constructor(locks, numLocked, lockErrors = [], unlockErrors = [], message) {
super(message ?? ERR_MULTI_LOCK);
this.locks = locks;
this.numLocked = numLocked;
this.lockErrors = lockErrors;
this.unlockErrors = unlockErrors;
}
}
class MultiUnlockError extends LockError {
/**
* @param locks - The array of all lockable objects that were part of the operation.
* @param numUnlocked - The number of unlocks successfully updated before failure.
* @param unlockErrors - An array of [index, error] pairs that contain the index of the lock in
* the `locks` array and the error that occurred while attempting to unlock it. Useful for
* debugging unexpected issues during unlocking.
* @param message - An optional custom error message that describes the error.
*/
constructor(locks, numUnlocked, unlockErrors = [], message) {
super(message ?? ERR_MULTI_UNLOCK);
this.locks = locks;
this.numUnlocked = numUnlocked;
this.unlockErrors = unlockErrors;
}
}
class OwnershipError extends LockError {
/**
* @param message - An optional custom error message.
*/
constructor(message) {
super(message ?? ERR_LOCK_OWNERSHIP);
}
}
class RelockError extends LockError {
/**
* @param message - An optional custom error message.
*/
constructor(message) {
super(message ?? ERR_LOCK_RELOCK);
}
}
var __defProp$a = Object.defineProperty;
var __defNormalProp$a = (obj, key, value) => key in obj ? __defProp$a(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$a = (obj, key, value) => __defNormalProp$a(obj, typeof key !== "symbol" ? key + "" : key, value);
class TimeoutError extends Error {
/**
* @param message - A custom error message. Defaults to `undefined`.
* @param timeout - The timeout duration in milliseconds. Defaults to `undefined`.
* @param deadline - The absolute time in milliseconds. Defaults to `undefined`.
*/
constructor(message, timeout, deadline) {
super(message ?? ERR_TIMEOUT);
/**
* Absolute time in milliseconds after which the timeout error was thrown.
* Can be `undefined` if not specified.
*/
__publicField$a(this, "deadline");
/**
* Duration in milliseconds after which the timeout error was thrown.
* Can be `undefined` if not specified.
*/
__publicField$a(this, "timeout");
this.deadline = deadline;
this.timeout = timeout;
this.name = TimeoutError.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TimeoutError);
}
}
}
var __defProp$9 = Object.defineProperty;
var __defNormalProp$9 = (obj, key, value) => key in obj ? __defProp$9(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$9 = (obj, key, value) => __defNormalProp$9(obj, typeof key !== "symbol" ? key + "" : key, value);
const LOCK_BIT$1 = 1;
const _Mutex = class _Mutex {
constructor(sharedBuffer, byteOffset = 0) {
/**
* Indicates whether the current agent owns the lock.
*/
__publicField$9(this, "_isOwner");
/**
* The shared memory for the mutex.
*/
__publicField$9(this, "_mem");
sharedBuffer ?? (sharedBuffer = new SharedArrayBuffer(_Mutex.ByteLength));
this._isOwner = false;
this._mem = new Int32Array(sharedBuffer, byteOffset, 1);
Atomics.and(this._mem, 0, LOCK_BIT$1);
}
get buffer() {
return this._mem.buffer;
}
get byteLength() {
return this._mem.byteLength;
}
get byteOffset() {
return this._mem.byteOffset;
}
get ownsLock() {
return this._isOwner;
}
/**
* @throws A {@link RelockError} If the lock is already locked by the caller.
*/
async lock() {
if (this._isOwner) {
throw new RelockError();
}
while (Atomics.or(this._mem, 0, LOCK_BIT$1)) {
const res = Atomics.waitAsync(this._mem, 0, LOCK_BIT$1);
if (res.async) {
await res.value;
}
}
this._isOwner = true;
}
/**
* @throws A {@link RelockError} If the lock is already locked by the caller.
*/
lockSync() {
if (this._isOwner) {
throw new RelockError();
}
while (Atomics.or(this._mem, 0, LOCK_BIT$1)) {
Atomics.wait(this._mem, 0, LOCK_BIT$1);
}
this._isOwner = true;
}
tryLock() {
return this.tryLockSync();
}
tryLockSync() {
if (this._isOwner) {
return false;
}
return this._isOwner = Atomics.or(this._mem, 0, LOCK_BIT$1) === 0;
}
/**
* @throws An {@link OwnershipError} If the mutex is not owned by the caller.
*/
unlock() {
return this.unlockSync();
}
/**
* @throws An {@link OwnershipError} If the mutex is not owned by the caller.
*/
unlockSync() {
if (!this._isOwner) {
throw new OwnershipError();
}
Atomics.store(this._mem, 0, 0);
this._isOwner = false;
Atomics.notify(this._mem, 0);
}
};
/**
* The size in bytes of the mutex.
*/
__publicField$9(_Mutex, "ByteLength", Int32Array.BYTES_PER_ELEMENT);
let Mutex = _Mutex;
const MAX_INT32_VALUE = 2147483647;
var __defProp$8 = Object.defineProperty;
var __defNormalProp$8 = (obj, key, value) => key in obj ? __defProp$8(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$8 = (obj, key, value) => __defNormalProp$8(obj, typeof key !== "symbol" ? key + "" : key, value);
const LOCK_BIT = 1;
const _RecursiveMutex = class _RecursiveMutex {
constructor(sharedBuffer, byteOffset = 0) {
/**
* The number of locks acquired by the agent.
*/
__publicField$8(this, "_depth");
/**
* The shared atomic memory for the mutex.
*/
__publicField$8(this, "_mem");
sharedBuffer ?? (sharedBuffer = new SharedArrayBuffer(_RecursiveMutex.ByteLength));
this._depth = 0;
this._mem = new Int32Array(sharedBuffer, byteOffset, 1);
Atomics.and(this._mem, 0, LOCK_BIT);
}
get buffer() {
return this._mem.buffer;
}
get byteLength() {
return this._mem.byteLength;
}
get byteOffset() {
return this._mem.byteOffset;
}
get ownsLock() {
return this._depth > 0;
}
/**
* @throws A {@link RangeError} If the mutex is already locked the maximum amount of times.
*/
async lock() {
if (this._depth === _RecursiveMutex.Max) {
throw new RangeError(ERR_REC_MUTEX_OVERFLOW);
}
if (this._depth === 0) {
while (Atomics.or(this._mem, 0, LOCK_BIT)) {
await Atomics.waitAsync(this._mem, 0, LOCK_BIT).value;
}
}
++this._depth;
}
/**
* @throws A {@link RangeError} If the mutex is already locked the maximum amount of times.
*/
lockSync() {
if (this._depth === _RecursiveMutex.Max) {
throw new RangeError(ERR_REC_MUTEX_OVERFLOW);
}
if (this._depth === 0) {
while (Atomics.or(this._mem, 0, LOCK_BIT)) {
Atomics.wait(this._mem, 0, LOCK_BIT);
}
}
++this._depth;
}
tryLock() {
return this.tryLockSync();
}
tryLockSync() {
if (this._depth === _RecursiveMutex.Max) {
return false;
}
if (this._depth === 0 && Atomics.or(this._mem, 0, LOCK_BIT)) {
return false;
}
++this._depth;
return true;
}
/**
* @throws A {@link OwnershipError} If the mutex is not owned by the caller.
*/
unlock() {
return this.unlockSync();
}
/**
* @throws A {@link OwnershipError} If the mutex is not owned by the caller.
*/
unlockSync() {
if (this._depth <= 0) {
throw new OwnershipError();
}
if (this._depth > 1) {
--this._depth;
return;
}
Atomics.store(this._mem, 0, 0);
this._depth = 0;
Atomics.notify(this._mem, 0);
}
};
/**
* The size in bytes of the mutex.
*/
__publicField$8(_RecursiveMutex, "ByteLength", Int32Array.BYTES_PER_ELEMENT);
/**
* The maximum levels of recursive ownership.
*/
__publicField$8(_RecursiveMutex, "Max", MAX_INT32_VALUE);
let RecursiveMutex = _RecursiveMutex;
const ATOMICS_NOT_EQUAL = "not-equal";
const ATOMICS_TIMED_OUT = "timed-out";
class RecursiveTimedMutex extends RecursiveMutex {
async tryLockFor(timeout) {
return this.tryLockUntil(performance.now() + timeout);
}
tryLockForSync(timeout) {
return this.tryLockUntilSync(performance.now() + timeout);
}
async tryLockUntil(timestamp) {
if (this._depth === RecursiveTimedMutex.Max) {
return false;
}
if (this._depth === 0) {
while (Atomics.or(this._mem, 0, LOCK_BIT)) {
const timeout = timestamp - performance.now();
const res = Atomics.waitAsync(this._mem, 0, LOCK_BIT, timeout);
const value = res.async ? await res.value : res.value;
if (value === ATOMICS_TIMED_OUT) {
return false;
}
}
}
++this._depth;
return true;
}
tryLockUntilSync(timestamp) {
if (this._depth === RecursiveTimedMutex.Max) {
return false;
}
if (this._depth === 0) {
while (Atomics.or(this._mem, 0, LOCK_BIT)) {
const timeout = timestamp - performance.now();
const value = Atomics.wait(this._mem, 0, LOCK_BIT, timeout);
if (value === ATOMICS_TIMED_OUT) {
return false;
}
}
}
++this._depth;
return true;
}
}
async function lockGuard(mutex, callbackfn) {
await mutex.lock();
try {
return await callbackfn();
} finally {
await mutex.unlock();
}
}
function lockGuardSync(mutex, callbackfn) {
mutex.lockSync();
try {
return callbackfn();
} finally {
mutex.unlockSync();
}
}
var __defProp$7 = Object.defineProperty;
var __defNormalProp$7 = (obj, key, value) => key in obj ? __defProp$7(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$7 = (obj, key, value) => __defNormalProp$7(obj, typeof key !== "symbol" ? key + "" : key, value);
const _ConditionVariable = class _ConditionVariable {
constructor(sharedBuffer, byteOffset = 0) {
/**
* The shared atomic memory where the condition variable stores its state.
*/
__publicField$7(this, "_mem");
sharedBuffer ?? (sharedBuffer = new SharedArrayBuffer(_ConditionVariable.ByteLength));
this._mem = new Int32Array(sharedBuffer, byteOffset, 1);
Atomics.store(this._mem, 0, 0);
}
get buffer() {
return this._mem.buffer;
}
get byteLength() {
return this._mem.byteLength;
}
get byteOffset() {
return this._mem.byteOffset;
}
/**
* Notify waiting agents that are blocked on this condition variable.
*
* @param count - The number of agents to notify.
*
* @returns The number of agents that were notified.
*/
notify(count) {
return Atomics.notify(this._mem, 0, count);
}
/**
* Notify all waiting agents that are blocked on this condition variable.
*
* @returns The number of agents that were notified.
*/
notifyAll() {
return Atomics.notify(this._mem, 0);
}
/**
* Notify one waiting agent that is blocked on this condition variable.
*
* @returns The number of agents that were notified.
*/
notifyOne() {
return Atomics.notify(this._mem, 0, 1);
}
/**
* Blocks the current agent until this condition variable is notified.
* The associated mutex is released before blocking and re-acquired
* after waking up.
*
* @param mutex The mutex that must be locked by the current agent.
*
* @throws An {@link OwnershipError} If the mutex is not owned by the caller.
* @throws A {@link RangeError} If the shared memory data is unexpected.
*/
async wait(mutex) {
await this.waitFor(mutex, Infinity);
}
/**
* Blocks the current agent until this condition variable is notified,
* or an optional timeout expires. The associated mutex is released
* before blocking and re-acquired after waking up.
*
* @param mutex The mutex that must be locked by the current agent.
* @param timeout A timeout in milliseconds after which the wait is aborted.
*
* @throws An {@link OwnershipError} If the mutex is not owned by the caller.
* @throws A {@link RangeError} If the shared memory data is unexpected.
*
* @returns A {@link CVStatus} representing the result of the operation.
*/
async waitFor(mutex, timeout) {
if (!mutex.ownsLock) {
throw new OwnershipError();
}
try {
const res = Atomics.waitAsync(this._mem, 0, 0, timeout);
await mutex.unlock();
const value = res.async ? await res.value : res.value;
if (value === ATOMICS_NOT_EQUAL) {
throw new RangeError(ERR_CV_VALUE);
}
return value === ATOMICS_TIMED_OUT ? CV_TIMED_OUT : CV_OK;
} finally {
await mutex.lock();
}
}
/**
* Blocks the current agent until this condition variable is notified,
* or until a specified point in time is reached. The associated mutex
* is released before blocking and re-acquired after waking up.
*
* @param mutex The mutex that must be locked by the current agent.
* @param timestamp The absolute time in milliseconds at which the wait is aborted.
*
* @throws A {@link OwnershipError} If the mutex is not owned by the caller.
* @throws A {@link RangeError} If the shared memory data is unexpected.
*
* @returns A {@link CVStatus} representing the result of the operation.
*/
async waitUntil(mutex, timestamp) {
return this.waitFor(mutex, timestamp - performance.now());
}
};
/**
* The size in bytes of the condition variable.
*/
__publicField$7(_ConditionVariable, "ByteLength", Int32Array.BYTES_PER_ELEMENT);
let ConditionVariable = _ConditionVariable;
class TimedMutex extends Mutex {
async tryLockFor(timeout) {
return this.tryLockUntil(performance.now() + timeout);
}
tryLockForSync(timeout) {
return this.tryLockUntilSync(performance.now() + timeout);
}
async tryLockUntil(timestamp) {
if (this._isOwner) {
return false;
}
while (Atomics.or(this._mem, 0, LOCK_BIT$1)) {
const timeout = timestamp - performance.now();
const res = Atomics.waitAsync(this._mem, 0, LOCK_BIT$1, timeout);
const value = res.async ? await res.value : res.value;
if (value === ATOMICS_TIMED_OUT) {
return false;
}
}
return this._isOwner = true;
}
tryLockUntilSync(timestamp) {
if (this._isOwner) {
return false;
}
while (Atomics.or(this._mem, 0, LOCK_BIT$1)) {
const timeout = timestamp - performance.now();
const value = Atomics.wait(this._mem, 0, LOCK_BIT$1, timeout);
if (value === ATOMICS_TIMED_OUT) {
return false;
}
}
return this._isOwner = true;
}
}
var __defProp$6 = Object.defineProperty;
var __defNormalProp$6 = (obj, key, value) => key in obj ? __defProp$6(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$6 = (obj, key, value) => __defNormalProp$6(obj, typeof key !== "symbol" ? key + "" : key, value);
const WRITE_BIT = 1 << 31;
const READ_BITS = ~WRITE_BIT;
const _SharedMutex = class _SharedMutex {
constructor(sharedBuffer, byteOffset = 0) {
__publicField$6(this, "_gate1");
__publicField$6(this, "_gate2");
__publicField$6(this, "_isReader");
__publicField$6(this, "_isWriter");
__publicField$6(this, "_mem");
__publicField$6(this, "_mutex");
const bInt32 = Int32Array.BYTES_PER_ELEMENT;
sharedBuffer ?? (sharedBuffer = new SharedArrayBuffer(_SharedMutex.ByteLength));
this._mem = new Int32Array(sharedBuffer, byteOffset, 4);
byteOffset += bInt32;
this._mutex = new TimedMutex(sharedBuffer, byteOffset);
byteOffset += bInt32;
this._gate1 = new ConditionVariable(sharedBuffer, byteOffset);
byteOffset += bInt32;
this._gate2 = new ConditionVariable(sharedBuffer, byteOffset);
this._isReader = false;
this._isWriter = false;
}
get buffer() {
return this._mem.buffer;
}
get byteLength() {
return this._mem.byteLength;
}
get byteOffset() {
return this._mem.byteOffset;
}
get ownsLock() {
return this._isWriter;
}
get ownsSharedLock() {
return this._isReader;
}
// Exclusive
/**
* @throws A {@link RelockError} If the mutex is already locked by the caller.
*/
async lock() {
if (this._isWriter || this._isReader) {
throw new RelockError();
}
await lockGuard(this._mutex, async () => {
while (Atomics.or(this._mem, 0, WRITE_BIT) & WRITE_BIT) {
await this._gate1.wait(this._mutex);
}
this._isWriter = true;
while (Atomics.load(this._mem, 0) & READ_BITS) {
await this._gate2.wait(this._mutex);
}
});
}
tryLock() {
if (this._isWriter || this._isReader) {
return false;
}
if (this._mutex.tryLock()) {
try {
this._isWriter = Atomics.compareExchange(this._mem, 0, 0, WRITE_BIT) === 0;
} finally {
this._mutex.unlock();
}
}
return this._isWriter;
}
/**
* @throws A {@link OwnershipError} If the mutex is not owned by the caller.
*/
async unlock() {
if (!this._isWriter) {
throw new OwnershipError();
}
await lockGuard(this._mutex, () => {
Atomics.and(this._mem, 0, READ_BITS);
this._isWriter = false;
});
this._gate1.notifyAll();
}
// Shared
/**
* @throws A {@link RelockError} If the lock is already locked by the caller.
*/
async lockShared() {
if (this._isReader || this._isWriter) {
throw new RelockError();
}
await lockGuard(this._mutex, async () => {
let state = Atomics.load(this._mem, 0);
while (state & WRITE_BIT || (state & READ_BITS) === READ_BITS) {
await this._gate1.wait(this._mutex);
state = Atomics.load(this._mem, 0);
}
Atomics.add(this._mem, 0, 1);
this._isReader = true;
});
}
tryLockShared() {
if (this._isReader || this._isWriter) {
return false;
}
if (this._mutex.tryLock()) {
try {
const state = Atomics.load(this._mem, 0);
if (state & WRITE_BIT || (state & READ_BITS) === READ_BITS) {
return false;
}
this._isReader = Atomics.compareExchange(this._mem, 0, state, state + 1) === state;
} finally {
this._mutex.unlock();
}
}
return this._isReader;
}
/**
* @throws An {@link OwnershipError} If the mutex is not owned by the caller.
*/
async unlockShared() {
if (!this._isReader) {
throw new OwnershipError();
}
await lockGuard(this._mutex, () => {
const state = Atomics.sub(this._mem, 0, 1);
this._isReader = false;
if (state & WRITE_BIT) {
if ((state & READ_BITS) === 1) {
this._gate2.notifyAll();
}
} else if (state === READ_BITS) {
this._gate1.notifyAll();
}
});
}
};
/**
* The size in bytes of the mutex.
*/
__publicField$6(_SharedMutex, "ByteLength", 4 * Int32Array.BYTES_PER_ELEMENT);
let SharedMutex = _SharedMutex;
class SharedTimedMutex extends SharedMutex {
async tryLockFor(timeout) {
return this.tryLockUntil(performance.now() + timeout);
}
async tryLockUntil(timestamp) {
if (this._isWriter || this._isReader) {
return false;
}
if (!await this._mutex.tryLockUntil(timestamp)) {
return false;
}
let notify = false;
try {
while (Atomics.or(this._mem, 0, WRITE_BIT) & WRITE_BIT) {
const res = await this._gate1.waitUntil(this._mutex, timestamp);
if (res === CV_TIMED_OUT) {
return false;
}
}
this._isWriter = true;
while (Atomics.load(this._mem, 0) & READ_BITS) {
const res = await this._gate2.waitUntil(this._mutex, timestamp);
if (res === CV_TIMED_OUT) {
notify = true;
Atomics.and(this._mem, 0, READ_BITS);
this._isWriter = false;
return false;
}
}
return true;
} finally {
await this._mutex.unlock();
if (notify) {
this._gate1.notifyAll();
}
}
}
async tryLockSharedFor(timeout) {
return this.tryLockSharedUntil(performance.now() + timeout);
}
async tryLockSharedUntil(timestamp) {
if (this._isReader || this._isWriter) {
return false;
}
if (!await this._mutex.tryLockUntil(timestamp)) {
return false;
}
try {
let state = Atomics.load(this._mem, 0);
while (state & WRITE_BIT || state === READ_BITS) {
const res = await this._gate1.waitUntil(this._mutex, timestamp);
if (res === CV_TIMED_OUT) {
return false;
}
state = Atomics.load(this._mem, 0);
}
Atomics.add(this._mem, 0, 1);
this._isReader = true;
return true;
} finally {
await this._mutex.unlock();
}
}
}
async function lock(...locks) {
const N = locks.length;
const lockErrors = [];
let numLocked = N;
for (let i = 0; i < N; ++i) {
try {
await locks[i].lock();
} catch (err) {
lockErrors.push([i, err]);
numLocked = i;
break;
}
}
if (numLocked === N) {
return;
}
const unlockErrors = [];
for (let i = numLocked - 1; i >= 0; --i) {
try {
await locks[i].unlock();
} catch (err) {
unlockErrors.push([i, err]);
}
}
throw new MultiLockError(locks, numLocked, lockErrors, unlockErrors);
}
async function tryLock(...locks) {
const N = locks.length;
const lockErrors = [];
let numLocked = N;
for (let i = 0; i < N; ++i) {
try {
if (!await locks[i].tryLock()) {
numLocked = i;
break;
}
} catch (err) {
lockErrors.push([i, err]);
numLocked = i;
break;
}
}
if (numLocked === N) {
return -1;
}
if (numLocked < 1 && lockErrors.length < 1) {
return numLocked;
}
const unlockErrors = [];
for (let i = numLocked - 1; i >= 0; --i) {
try {
await locks[i].unlock();
} catch (err) {
unlockErrors.push([i, err]);
}
}
if (lockErrors.length > 0) {
throw new MultiLockError(locks, numLocked, lockErrors, unlockErrors);
}
if (unlockErrors.length > 0) {
const numUnlocked = numLocked - unlockErrors.length;
throw new MultiUnlockError(locks, numUnlocked, unlockErrors);
}
return numLocked;
}
var __defProp$5 = Object.defineProperty;
var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$5 = (obj, key, value) => __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
class MultiLock {
/**
* @param mutexes - The basic lockables to associate.
*/
constructor(...mutexes) {
/**
* Indicates whether the current agent owns the lock.
*/
__publicField$5(this, "_isOwner");
/**
* The associated basic lockable.
*/
__publicField$5(this, "mutexes");
this._isOwner = false;
this.mutexes = mutexes;
}
get ownsLock() {
return this._isOwner;
}
async lock() {
await lock(...this.mutexes);
this._isOwner = true;
}
/**
* Exchange internal state
*/
swap(other) {
const tIsOwner = this._isOwner;
this._isOwner = other._isOwner;
other._isOwner = tIsOwner;
const tMutexes = this.mutexes;
this.mutexes = other.mutexes;
other.mutexes = tMutexes;
}
async tryLock() {
const res = await tryLock(...this.mutexes);
return this._isOwner = res < 0;
}
async unlock() {
const locks = this.mutexes;
const N = locks.length;
const unlockErrors = [];
for (let i = N - 1; i >= 0; --i) {
try {
await locks[i].unlock();
} catch (err) {
unlockErrors.push([i, err]);
}
}
this._isOwner = false;
if (unlockErrors.length > 0) {
const unlocked = N - unlockErrors.length;
throw new MultiUnlockError(Array.from(locks), unlocked, unlockErrors);
}
}
}
var __defProp$4 = Object.defineProperty;
var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$4 = (obj, key, value) => __defNormalProp$4(obj, key + "" , value);
class SharedLock {
/**
* @param mutex - The shared lockable to associate.
*/
constructor(mutex) {
/**
* The associated mutex.
*/
__publicField$4(this, "mutex");
this.mutex = mutex;
}
get ownsLock() {
return this.mutex?.ownsSharedLock ?? false;
}
lock() {
return this.mutex.lockShared();
}
/**
* Exchanges the internal states of the shared locks.
*/
swap(other) {
const temp = this.mutex;
this.mutex = other.mutex;
other.mutex = temp;
}
tryLock() {
return this.mutex.tryLockShared();
}
tryLockFor(timeout) {
return this.mutex.tryLockSharedFor(timeout);
}
tryLockUntil(timestamp) {
return this.mutex.tryLockSharedUntil(timestamp);
}
unlock() {
return this.mutex.unlockShared();
}
}
var __defProp$3 = Object.defineProperty;
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, key + "" , value);
class UniqueLock {
/**
* @param mutex - The basic lockable to associate.
*/
constructor(mutex) {
/**
* The associated basic lockable.
*/
__publicField$3(this, "mutex");
this.mutex = mutex;
}
get ownsLock() {
return this.mutex?.ownsLock ?? false;
}
lock() {
return this.mutex.lock();
}
lockSync() {
return this.mutex.lockSync();
}
/**
* Exchanges the internal states of the unique locks.
*/
swap(other) {
const temp = this.mutex;
this.mutex = other.mutex;
other.mutex = temp;
}
tryLock() {
return this.mutex.tryLock();
}
tryLockSync() {
return this.mutex.tryLockSync();
}
tryLockFor(timeout) {
return this.mutex.tryLockFor(timeout);
}
tryLockForSync(timeout) {
return this.mutex.tryLockForSync(timeout);
}
tryLockUntil(timestamp) {
return this.mutex.tryLockUntil(timestamp);
}
tryLockUntilSync(timestamp) {
return this.mutex.tryLockUntilSync(timestamp);
}
unlock() {
return this.mutex.unlock();
}
unlockSync() {
return this.mutex.unlockSync();
}
}
function callOnce(flag, callbackfn) {
return flag.set() ? callbackfn() : void 0;
}
var __defProp$2 = Object.defineProperty;
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
const _OnceFlag = class _OnceFlag {
constructor(sharedBuffer, byteOffset = 0, bitOffset = 0) {
/**
* The bit within the shared memory used to set the flag.
*/
__publicField$2(this, "_bit");
/**
* The offset for the bit within the 32-bit integer of the shared memory.
*/
__publicField$2(this, "_bitOffset");
/**
* The shared memory buffer used for the flag.
*/
__publicField$2(this, "_mem");
sharedBuffer ?? (sharedBuffer = new SharedArrayBuffer(_OnceFlag.ByteLength));
if (bitOffset < 0) {
throw new RangeError("Invalid bit offset", {
cause: `${bitOffset} < 0`
});
}
if (bitOffset >= 32) {
throw new RangeError("Invalid bit offset", {
cause: `${bitOffset} >= 32`
});
}
this._bit = 1 << bitOffset;
this._bitOffset = bitOffset;
this._mem = new Int32Array(sharedBuffer, byteOffset, 1);
}
get buffer() {
return this._mem.buffer;
}
get byteLength() {
return this._mem.byteLength;
}
get byteOffset() {
return this._mem.byteOffset;
}
/**
* The bit offset for the flag within shared memory, relative to `byteOffset`.
*/
get bitOffset() {
return this._bitOffset;
}
/**
* Resets the flag state to `false`.
*
* @returns `true` if the flag was previously set, `false` otherwise.
*/
clear() {
return (Atomics.and(this._mem, 0, ~this._bit) & this._bit) !== 0;
}
/**
* Checks if the flag is currently set.
*
* @returns `true` if the flag is set, `false` otherwise.
*/
isSet() {
return (Atomics.load(this._mem, 0) & this._bit) !== 0;
}
/**
* Sets the flag to `true`. This operation is atomic and thread-safe.
*
* @returns `true` if the flag was set, `false` if it was already set.
*/
set() {
return (Atomics.or(this._mem, 0, this._bit) & this._bit) === 0;
}
};
/**
* The size in bytes of the flag.
*/
__publicField$2(_OnceFlag, "ByteLength", Int32Array.BYTES_PER_ELEMENT);
let OnceFlag = _OnceFlag;
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
const _CountingSemaphore = class _CountingSemaphore {
constructor(sharedBuffer, byteOffset = 0) {
__publicField$1(this, "_gate");
__publicField$1(this, "_mem");
__publicField$1(this, "_mutex");
const bInt32 = Int32Array.BYTES_PER_ELEMENT;
if (sharedBuffer instanceof SharedArrayBuffer) {
this._mem = new Int32Array(sharedBuffer, byteOffset, 3);
byteOffset += bInt32;
this._mutex = new TimedMutex(sharedBuffer, byteOffset);
byteOffset += bInt32;
this._gate = new ConditionVariable(sharedBuffer, byteOffset);
return;
}
const desired = sharedBuffer;
if (desired < 0) {
throw new RangeError(ERR_NEGATIVE_VALUE, {
cause: `${desired} < 0`
});
}
if (desired > _CountingSemaphore.Max) {
throw new RangeError(ERR_OVERFLOW, {
cause: `${desired} > ${_CountingSemaphore.Max}`
});
}
sharedBuffer = new SharedArrayBuffer(_CountingSemaphore.ByteLength);
this._mem = new Int32Array(sharedBuffer, 0, 3);
byteOffset += bInt32;
this._mutex = new TimedMutex(sharedBuffer, byteOffset);
byteOffset += bInt32;
this._gate = new ConditionVariable(sharedBuffer, byteOffset);
this._mem[0] = desired;
}
get buffer() {
return this._mem.buffer;
}
get byteLength() {
return this._mem.byteLength;
}
get byteOffset() {
return this._mem.byteOffset;
}
/**
* Acquires the semaphore, blocking until it is available.
*
* @returns A promise that resolves when acquisition is successful.
*/
acquire() {
return lockGuard(this._mutex, async () => {
while (Atomics.load(this._mem, 0) <= 0) {
await this._gate.wait(this._mutex);
}
Atomics.sub(this._mem, 0, 1);
});
}
/**
* Attempts to acquire the semaphore.
*
* @returns A promise resolving to `true` if successful, otherwise `false`.
*/
tryAcquire() {
return lockGuard(this._mutex, () => {
if (Atomics.load(this._mem, 0) <= 0) {
return false;
}
Atomics.sub(this._mem, 0, 1);
return true;
});
}
/**
* Attempts to acquire the semaphore, blocking until either
* success or the specified timeout elapses.
*
* @param timeout The maximum duration in milliseconds to wait.
*
* @returns A promise resolving to `true` if successful, otherwise `false`.
*/
tryAcquireFor(timeout) {
return this.tryAcquireUntil(performance.now() + timeout);
}
/**
* Attempts to acquire the lock, blocking until either
* the lock is acquired or the specified point in time is reached.
*
* @param timestamp The absolute time in milliseconds to wait until.
*
* @returns A promise resolved to `true` if succesful, otherwise `false`.
*/
async tryAcquireUntil(timestamp) {
if (!await this._mutex.tryLockUntil(timestamp)) {
return false;
}
try {
while (Atomics.load(this._mem, 0) <= 0) {
const status = await this._gate.waitUntil(this._mutex, timestamp);
if (status === CV_TIMED_OUT) {
return false;
}
}
Atomics.sub(this._mem, 0, 1);
return true;
} finally {
await this._mutex.unlock();
}
}
/**
* Releases a specified number of units back to the semaphore.
*
* @param count The number of units to release. Defaults to 1.
*
* @throws {RangeError} If `count` is negative or would cause the semaphore to overflow.
*/
release(count = 1) {
if (count < 0) {
throw new RangeError(ERR_SEM_INPUT_NEG, {
cause: `${count} < 0`
});
}
return lockGuard(this._mutex, () => {
const state = Atomics.load(this._mem, 0);
if (count > _CountingSemaphore.Max - state) {
throw new RangeError(ERR_SEM_INPUT_OVERFLOW, {
cause: `${count} > ${_CountingSemaphore.Max - state}`
});
}
Atomics.add(this._mem, 0, count);
this._gate.notifyAll();
});
}
};
/**
* The size in bytes of the semaphore.
*/
__publicField$1(_CountingSemaphore, "ByteLength", 3 * Int32Array.BYTES_PER_ELEMENT);
/**
* The maximum possible value of the internal counter
*/
__publicField$1(_CountingSemaphore, "Max", MAX_INT32_VALUE);
let CountingSemaphore = _CountingSemaphore;
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
const _Latch = class _Latch {
constructor(sharedBuffer, byteOffset = 0) {
/**
* Condition variable to manage waiting agents.
*/
__publicField(this, "_gate");
/**
* The shared atomic memory for the internal counter.
*/
__publicField(this, "_mem");
/**
* Mutex to protect access to the internal counter.
*/
__publicField(this, "_mutex");
const bInt32 = Int32Array.BYTES_PER_ELEMENT;
if (sharedBuffer instanceof SharedArrayBuffer) {
this._mem = new Int32Array(sharedBuffer, byteOffset, 3);
byteOffset += bInt32;
this._mutex = new Mutex(sharedBuffer, byteOffset);
byteOffset += bInt32;
this._gate = new ConditionVariable(sharedBuffer, byteOffset);
return;
}
const expected = sharedBuffer;
if (expected < 0) {
throw new RangeError(ERR_NEGATIVE_VALUE, {
cause: `${expected} < 0`
});
}
if (expected > _Latch.Max) {
throw new RangeError(ERR_OVERFLOW, {
cause: `${expected} > ${_Latch.Max}`
});
}
sharedBuffer = new SharedArrayBuffer(_Latch.ByteLength);
this._mem = new Int32Array(sharedBuffer, 0, 3);
byteOffset += bInt32;
this._mutex = new Mutex(sharedBuffer, byteOffset);
byteOffset += bInt32;
this._gate = new ConditionVariable(sharedBuffer, byteOffset);
this._mem[0] = expected;
}
/**
* Decrements the counter by a specified amount.
*
* If the counter reaches zero, waiting agents are notified.
*
* @param n The amount to decrement the counter.
*
* @throws A {@link RangeError} If `n` is negative or exceeds the current count.
*/
async countDown(n = 1) {
if (n < 0) {
throw new RangeError(ERR_LATCH_INPUT_UNDERFLOW, {
cause: `${n} < 0`
});
}
await lockGuard(this._mutex, async () => {
const value = Atomics.load(this._mem, 0);
if (n > value) {
throw new RangeError(ERR_LATCH_INPUT_OVERFLOW, {
cause: `${n} > ${value}`
});
}
if (Atomics.sub(this._mem, 0, n) === n) {
this._gate.notifyAll();
}
});
}
/**
* Decrements the counter by a specified amount, then waits for it to reach zero.
*
* If the counter is decremented to zero, waiting agents are notified.
*
* @param n The amount to decrement the counter.
*
* @throws A {@link RangeError} If `n` is negative or exceeds the current count.
*
* @returns A promise that resolves once the internal count reaches zero,
* allowing the agent to proceed.
*/
async arriveAndWait(n = 1) {
if (n < 0) {
throw new RangeError(ERR_LATCH_INPUT_UNDERFLOW, {
cause: `${n} < 0`
});
}
await lockGuard(this._mutex, async () => {
const value = Atomics.load(this._mem, 0);
if (n > value) {
throw new RangeError(ERR_LATCH_INPUT_OVERFLOW, {
cause: `${n} > ${value}`
});
}
if (Atomics.sub(this._mem, 0, n) === n) {
this._gate.notifyAll();
return;
}
do {
await this._gate.wait(this._mutex);
} while (Atomics.load(this._mem, 0) !== 0);
});
}
/**
* Tests if the counter has reached zero.
*
* @returns `true` if the current count is zero, otherwise `false`.
*/
tryWait() {
return Atomics.load(this._mem, 0) === 0;
}
/**
* Wait until the counter reaches zero.
*
* @returns A promise that resolves once the internal count reaches zero,
* allowing the agent to proceed.
*/
async wait() {
await lockGuard(this._mutex, async () => {
while (Atomics.load(this._mem, 0) !== 0) {
await this._gate.wait(this._mutex);
}
});
}
};
/**
* The size in bytes of the latch.
*/
__publicField(_Latch, "ByteLength", 3 * Int32Array.BYTES_PER_ELEMENT);
/**
* The maximum possible value of the internal counter.
*/
__publicField(_Latch, "Max", MAX_INT32_VALUE);
let Latch = _Latch;
export { CV_OK, CV_TIMED_OUT, ConditionVariable, CountingSemaphore, Latch, LockError, MultiLock, MultiLockError, MultiUnlockError, Mutex, OnceFlag, OwnershipError, RecursiveMutex, RecursiveTimedMutex, RelockError, SharedLock, SharedMutex, SharedTimedMutex, TimedMutex, TimeoutError, UniqueLock, callOnce, lock, lockGuard, lockGuardSync, tryLock };
//# sourceMappingURL=semafy.mjs.map