mortice
Version:
Isomorphic read/write lock that works in single processes, node clusters and web workers
213 lines • 6.17 kB
JavaScript
/**
* @packageDocumentation
*
* - Reads occur concurrently
* - Writes occur one at a time
* - No reads occur while a write operation is in progress
* - Locks can be created with different names
* - Reads/writes can time out
*
* ## Usage
*
* ```javascript
* import mortice from 'mortice'
* import delay from 'delay'
*
* // the lock name & options objects are both optional
* const mutex = mortice('my-lock', {
*
* // how long before write locks time out (default: 24 hours)
* timeout: 30000,
*
* // control how many read operations are executed concurrently (default: Infinity)
* concurrency: 5,
*
* // by default the the lock will be held on the main thread, set this to true if the
* // a lock should reside on each worker (default: false)
* singleProcess: false
* })
*
* Promise.all([
* (async () => {
* const release = await mutex.readLock()
*
* try {
* console.info('read 1')
* } finally {
* release()
* }
* })(),
* (async () => {
* const release = await mutex.readLock()
*
* try {
* console.info('read 2')
* } finally {
* release()
* }
* })(),
* (async () => {
* const release = await mutex.writeLock()
*
* try {
* await delay(1000)
*
* console.info('write 1')
* } finally {
* release()
* }
* })(),
* (async () => {
* const release = await mutex.readLock()
*
* try {
* console.info('read 3')
* } finally {
* release()
* }
* })()
* ])
* ```
*
* read 1
* read 2
* <small pause>
* write 1
* read 3
*
* ## Browser
*
* Because there's no global way to evesdrop on messages sent by Web Workers, please pass all created Web Workers to the [`observable-webworkers`](https://npmjs.org/package/observable-webworkers) module:
*
* ```javascript
* // main.js
* import mortice from 'mortice'
* import observe from 'observable-webworkers'
*
* // create our lock on the main thread, it will be held here
* const mutex = mortice()
*
* const worker = new Worker('worker.js')
*
* observe(worker)
* ```
*
* ```javascript
* // worker.js
* import mortice from 'mortice'
* import delay from 'delay'
*
* const mutex = mortice()
*
* let release = await mutex.readLock()
* // read something
* release()
*
* release = await mutex.writeLock()
* // write something
* release()
* ```
*/
import PQueue from 'p-queue';
import pTimeout from 'p-timeout';
import impl from './node.js';
const mutexes = {};
let implementation;
async function createReleaseable(queue, options) {
let res;
const p = new Promise((resolve) => {
res = resolve;
});
void queue.add(async () => pTimeout((async () => {
await new Promise((resolve) => {
res(() => {
resolve();
});
});
})(), {
milliseconds: options.timeout
}));
return p;
}
const createMutex = (name, options) => {
if (implementation.isWorker === true) {
return {
readLock: implementation.readLock(name, options),
writeLock: implementation.writeLock(name, options)
};
}
const masterQueue = new PQueue({ concurrency: 1 });
let readQueue;
return {
async readLock() {
// If there's already a read queue, just add the task to it
if (readQueue != null) {
return createReleaseable(readQueue, options);
}
// Create a new read queue
readQueue = new PQueue({
concurrency: options.concurrency,
autoStart: false
});
const localReadQueue = readQueue;
// Add the task to the read queue
const readPromise = createReleaseable(readQueue, options);
void masterQueue.add(async () => {
// Start the task only once the master queue has completed processing
// any previous tasks
localReadQueue.start();
// Once all the tasks in the read queue have completed, remove it so
// that the next read lock will occur after any write locks that were
// started in the interim
await localReadQueue.onIdle()
.then(() => {
if (readQueue === localReadQueue) {
readQueue = null;
}
});
});
return readPromise;
},
async writeLock() {
// Remove the read queue reference, so that any later read locks will be
// added to a new queue that starts after this write lock has been
// released
readQueue = null;
return createReleaseable(masterQueue, options);
}
};
};
const defaultOptions = {
name: 'lock',
concurrency: Infinity,
timeout: 84600000,
singleProcess: false
};
export default function createMortice(options) {
const opts = Object.assign({}, defaultOptions, options);
if (implementation == null) {
implementation = impl(opts);
if (implementation.isWorker !== true) {
// we are master, set up worker requests
implementation.addEventListener('requestReadLock', (event) => {
if (mutexes[event.data.name] == null) {
return;
}
void mutexes[event.data.name].readLock()
.then(async (release) => event.data.handler().finally(() => { release(); }));
});
implementation.addEventListener('requestWriteLock', async (event) => {
if (mutexes[event.data.name] == null) {
return;
}
void mutexes[event.data.name].writeLock()
.then(async (release) => event.data.handler().finally(() => { release(); }));
});
}
}
if (mutexes[opts.name] == null) {
mutexes[opts.name] = createMutex(opts.name, opts);
}
return mutexes[opts.name];
}
//# sourceMappingURL=index.js.map