composable-locks
Version:
Composable concurrency locks for Javascript.
220 lines (188 loc) • 5.01 kB
JavaScript
// 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