UNPKG

igir

Version:

🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.

137 lines (136 loc) • 4.13 kB
import { Mutex } from "async-mutex"; import ArrayUtil from "../utils/arrayUtil.js"; class KeyedMutex { keyMutexes = /* @__PURE__ */ new Map(); keyMutexesMutex = new Mutex(); // ES2015/ES6 maps are required to maintain insertion order, making them inexpensive for LRU keyMutexesLru = /* @__PURE__ */ new Map(); maxSize; constructor(maxSize) { this.maxSize = maxSize; } /** * Run a {@link runnable} exclusively across all keys. */ async runExclusiveGlobally(runnable) { return await this.keyMutexesMutex.runExclusive(runnable); } /** * Acquire a lock for the given key. */ async acquire(key) { await this.acquireMultiple([key]); } /** * Acquire a lock for every given key. */ async acquireMultiple(keys) { if (keys.length === 0) { return; } const uniqueKeys = keys.reduce(ArrayUtil.reduceUnique(), []).toSorted((a, b) => a.localeCompare(b)); let entries; if (uniqueKeys.every((key) => this.keyMutexes.has(key))) { for (const key of uniqueKeys) { this.keyMutexesLru.delete(key); this.keyMutexesLru.set(key, void 0); } entries = uniqueKeys.map((key) => { const entry = this.keyMutexes.get(key); entry.pendingLocks += 1; return entry; }); } else { const uniqueKeySet = new Set(uniqueKeys); entries = await this.keyMutexesMutex.runExclusive(() => { for (const key of uniqueKeys) { if (!this.keyMutexes.has(key)) { this.keyMutexes.set(key, { mutex: new Mutex(), pendingLocks: 0 }); } } if (this.maxSize !== void 0 && this.keyMutexes.size > this.maxSize) { let keysToEvict = this.keyMutexes.size - this.maxSize; for (const lruKey of this.keyMutexesLru.keys()) { if (keysToEvict <= 0) { break; } const lruEntry = this.keyMutexes.get(lruKey); if (!uniqueKeySet.has(lruKey) && lruEntry !== void 0 && !lruEntry.mutex.isLocked() && lruEntry.pendingLocks === 0) { lruEntry.mutex.release(); this.keyMutexes.delete(lruKey); this.keyMutexesLru.delete(lruKey); keysToEvict--; } } } for (const key of uniqueKeys) { this.keyMutexesLru.delete(key); this.keyMutexesLru.set(key, void 0); } return uniqueKeys.map((key) => { const entry = this.keyMutexes.get(key); entry.pendingLocks += 1; return entry; }); }); } let nextToAcquire = 0; try { for (; nextToAcquire < entries.length; nextToAcquire += 1) { await entries[nextToAcquire].mutex.acquire(); entries[nextToAcquire].pendingLocks -= 1; } } catch (error) { for (let i = nextToAcquire; i < entries.length; i += 1) { entries[i].pendingLocks -= 1; } throw error; } } /** * Release any held lock for the given key. */ release(key) { this.releaseMultiple([key]); } /** * Release any held lock for the given keys. */ releaseMultiple(keys) { if (keys.length === 0) { return; } const uniqueKeys = keys.reduce(ArrayUtil.reduceUnique(), []); for (const key of uniqueKeys) { this.keyMutexes.get(key)?.mutex.release(); } } /** * Run a {@link runnable} exclusively for the given {@link key}. */ async runExclusiveForKey(key, runnable) { await this.acquire(key); try { return await runnable(); } finally { this.release(key); } } /** * Run a {@link runnable} exclusively for the given {@link keys}. Keys are acquired in canonical * sorted order and released after {@link runnable} completes, so concurrent callers cannot * deadlock regardless of the order in which they pass their keys. */ async runExclusiveForKeys(keys, runnable) { await this.acquireMultiple(keys); try { return await runnable(); } finally { this.releaseMultiple(keys); } } } export { KeyedMutex as default }; //# sourceMappingURL=keyedMutex.js.map