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
JavaScript
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