UNPKG

composable-locks

Version:

Composable concurrency locks for Javascript.

220 lines (188 loc) 5.01 kB
// inspired from https://github.com/mgtitimoli/await-mutex, with some tweaks for typescript class Mutex { constructor() { this._locking = Promise.resolve(); } acquire() { let unlockNext; const willLock = new Promise(resolve => { unlockNext = () => resolve(); }); const willUnlock = this._locking.then(() => unlockNext); this._locking = this._locking.then(() => willLock); return willUnlock; } } /** * A keyed lock, for mapping strings to a lock type */ class KeyedMutex { /** * A keyed lock, for mapping strings to a lock type * @param newLock A function to create a new lock interface * @param resolver A function to transform a key into a normalized form. * Useful for resolving paths. */ constructor(newLock, resolver) { this.newLock = void 0; this.locks = {}; this.resolver = void 0; this.newLock = newLock; this.resolver = resolver != null ? resolver : key => key; } getOrCreateLock(key) { const record = this.locks[key]; if (record) return record; const newRecord = { count: 0, lock: this.newLock() }; this.locks[key] = newRecord; return newRecord; } async acquire(key, ...args) { const resolved = this.resolver(key); const record = this.getOrCreateLock(resolved); record.count++; const release = await record.lock.acquire(...args); // ensure idempotence let released = false; return () => { release(); if (released) return; record.count--; if (record.count === 0) { delete this.locks[resolved]; } released = true; }; } } /** * A re-entrant Mutex. * * Usage: * ``` * const lock = new ReentrantMutex() * const domain = Symbol() * * const release1 = await lock.acquire(domain) * const release2 = await lock.acquire(domain) * release1() * release2() * ``` */ class ReentrantMutex { constructor(newLock, greedy = true) { this.greedy = void 0; this.latest = null; this.lockMap = {}; this.lock = void 0; this.greedy = greedy; this.lock = newLock(); } /** * Acquire the lock * @param id The domain identifier. * @returns A function to release the lock. A domain *must* call all releasers before exiting. */ async acquire(id, ...args) { const queued = this.getQueued(id, ...args); const releaser = await queued.releaser; let released = false; return () => { if (released) return; released = true; this.release(queued, releaser); }; } /** * Get a queued domain object * @param id The domain to get the queued information for * @param args Passed to the underlying mutex * @returns The queued information, and a boolean that is true if the domain existed. */ getQueued(id, ...args) { if (this.greedy) { return this.getQueuedGreedy(id, ...args); } else { return this.getQueuedUngreedy(id, ...args); } } getQueuedGreedy(id, ...args) { const existing = this.lockMap[id]; if (existing) { return existing; } else { const queued = this.createQueued(id, ...args); this.lockMap[id] = queued; return queued; } } getQueuedUngreedy(id, ...args) { if (!this.latest || this.latest.id !== id) { const queued = this.createQueued(id, ...args); this.latest = queued; return queued; } else { const queued = this.latest; queued.reentrants++; return queued; } } createQueued(id, ...args) { return { id, reentrants: 1, releaser: this.lock.acquire(...args) }; } cleanup(queued) { if (this.greedy) { delete this.lockMap[queued.id]; } else if (this.latest === queued) { this.latest = null; } } release(queued, releaser) { queued.reentrants--; if (queued.reentrants === 0) { this.cleanup(queued); releaser(); } } } class RWMutex { constructor(newLock, preferRead = false) { this.readerDomain = Symbol(); this.base = void 0; this.base = new ReentrantMutex(newLock, preferRead); } acquire(type, ...args) { switch (type) { case "read": return this.base.acquire(this.readerDomain, ...args); case "write": return this.base.acquire(Symbol(), ...args); } } } /** * Execute an async function with permissions * @param permssions An array of promises that will resolve to release functions to release permissions * @param f The function to execute with permissions * @returns The return value of f */ const withPermissions = async (permssions, f) => { const releasers = await Promise.all(permssions); try { return await f(); } finally { releasers.forEach(release => release()); } }; exports.KeyedMutex = KeyedMutex; exports.Mutex = Mutex; exports.RWMutex = RWMutex; exports.ReentrantMutex = ReentrantMutex; exports.withPermissions = withPermissions; //# sourceMappingURL=index.js.map