@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
175 lines (153 loc) • 4.57 kB
text/typescript
/**
* A mutual exclusion mechanism for concurrent operations and protecting shared
* data.
* @module
*/
import BiMap from "./collections/BiMap.ts";
if (typeof Symbol.dispose === "undefined") {
Object.defineProperty(Symbol, "dispose", { value: Symbol("Symbol.dispose") });
}
const _queue = Symbol.for("queue");
const _value = Symbol.for("value");
const _mutex = Symbol.for("mutex");
const _unlocked = Symbol.for("unlocked");
/**
* Mutual Exclusion prevents multiple coroutines from accessing the same shared
* resource simultaneously.
*
* **NOTE:**
* Currently, the Mutex instance can not be used across multiple threads, but is
* considering adding support for `parallel` threads.
*
* @example
* ```ts
* import { Mutex } from "@ayonli/jsext/lock";
* import { random } from "@ayonli/jsext/number";
* import { sleep } from "@ayonli/jsext/async";
*
* const mutex = new Mutex(1);
*
* async function concurrentOperation() {
* using shared = await mutex.lock();
* const value1 = shared.value;
*
* await otherAsyncOperations();
*
* shared.value += 1
* const value2 = shared.value;
*
* // Without mutex lock, the shared value may have been modified by other
* // calls during `await otherAsyncOperation()`, and the following
* // assertion will fail.
* console.assert(value1 + 1 === value2);
* }
*
* async function otherAsyncOperations() {
* await sleep(100 * random(1, 10));
* }
*
* await Promise.all([
* concurrentOperation(),
* concurrentOperation(),
* concurrentOperation(),
* concurrentOperation(),
* ]);
* ```
*/
export class Mutex<T> {
private [_queue]: (() => void)[] = [];
private [_value]: T;
/**
* @param value The data associated to the mutex instance.
*/
constructor(value: T) {
this[_value] = value;
}
/**
* Acquires the lock of the mutex, optionally for modifying the shared
* resource.
*/
async lock(): Promise<Mutex.Lock<T>> {
await new Promise<void>(resolve => {
if (this[_queue].length) {
this[_queue].push(resolve);
} else {
this[_queue].push(resolve);
resolve();
}
});
const lock = Object.create(Mutex.Lock.prototype) as Mutex.Lock<T>;
lock[_mutex] = this;
return lock;
}
}
export namespace Mutex {
export abstract class Lock<T> {
private [_mutex]: Mutex<T>;
private [_unlocked] = false;
constructor(mutex: Mutex<T>) {
this[_mutex] = mutex;
}
/** Accesses the data associated to the mutex instance. */
get value(): T {
if (this[_unlocked]) {
throw new ReferenceError("trying to access data after unlocked");
}
return this[_mutex][_value];
}
set value(v: T) {
if (this[_unlocked]) {
throw new ReferenceError("trying to access data after unlocked");
}
this[_mutex][_value] = v;
}
/** Releases the current lock of the mutex. */
unlock() {
this[_unlocked] = true;
const queue = this[_mutex][_queue];
queue.shift();
const next = queue[0];
if (next) {
next();
} else if (registry.hasValue(this[_mutex] as any)) {
registry.deleteValue(this[_mutex] as any);
}
}
[Symbol.dispose]() {
this.unlock();
}
}
}
const registry = new BiMap<any, Mutex<undefined>>();
/**
* Acquires a mutex lock for the given key in order to perform concurrent
* operations and prevent conflicts.
*
* If the key is currently being locked by other coroutines, this function will
* block until the lock becomes available again.
*
* @example
* ```ts
* import lock from "@ayonli/jsext/lock";
*
* const key = "lock_key";
*
* export async function concurrentOperation() {
* using ctx = await lock(key);
* void ctx;
*
* // This block will never be run if there are other coroutines holding
* // the lock.
* //
* // Other coroutines trying to lock the same key will also never be run
* // before this function completes.
* }
* ```
*/
export default async function lock(key: any): Promise<Mutex.Lock<undefined>> {
let mutex = registry.get(key);
if (!mutex) {
registry.set(key, mutex = new Mutex(void 0));
}
return await mutex.lock();
}