Files
risuai/src/ts/mutex.ts

88 lines
2.4 KiB
TypeScript

/**
* A lock for synchronizing async operations.
* Use this to protect a critical section
* from getting modified by multiple async operations
* at the same time.
*/
export class Mutex {
/**
* When multiple operations attempt to acquire the lock,
* this queue remembers the order of operations.
*/
private _queue: {
resolve: (release: ReleaseFunction) => void
}[] = []
private _isLocked = false
/**
* Wait until the lock is acquired.
* @returns A function that releases the acquired lock.
*/
acquire() {
return new Promise<ReleaseFunction>((resolve) => {
this._queue.push({resolve})
this._dispatch()
});
}
/**
* Enqueue a function to be run serially.
*
* This ensures no other functions will start running
* until `callback` finishes running.
* @param callback Function to be run exclusively.
* @returns The return value of `callback`.
*/
async runExclusive<T>(callback: () => Promise<T>) {
const release = await this.acquire()
try {
return await callback()
} finally {
release()
}
}
/**
* Check the availability of the resource
* and provide access to the next operation in the queue.
*
* _dispatch is called whenever availability changes,
* such as after lock acquire request or lock release.
*/
private _dispatch() {
if (this._isLocked) {
// The resource is still locked.
// Wait until next time.
return
}
const nextEntry = this._queue.shift()
if (!nextEntry) {
// There is nothing in the queue.
// Do nothing until next dispatch.
return
}
// The resource is available.
this._isLocked = true
// and give access to the next operation
// in the queue.
nextEntry.resolve(this._buildRelease())
}
/**
* Build a release function for each operation
* so that it can release the lock after
* the operation is complete.
*/
private _buildRelease(): ReleaseFunction {
return () => {
// Each release function make
// the resource available again
this._isLocked = false
// and call dispatch.
this._dispatch()
}
}
}
type ReleaseFunction = () => void