Launch Week Day 5: Introducing Reachability for PHP.Learn More
Socket
Book a DemoSign in
Socket

mortice

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mortice - npm Package Compare versions

Comparing version
2.0.1
to
3.0.0
+4
dist/src/browser.d.ts
import type { MorticeImplementation, MorticeOptions } from './index.js';
declare const _default: (options: Required<MorticeOptions>) => MorticeImplementation | EventTarget;
export default _default;
//# sourceMappingURL=browser.d.ts.map
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAW,MAAM,YAAY,CAAA;kCAiGvD,SAAS,cAAc,CAAC,KAAG,qBAAqB,GAAG,WAAW;AAAvF,wBAkBC"}
import { nanoid } from 'nanoid';
import { WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK } from './constants.js';
import observer from 'observable-webworkers';
const handleWorkerLockRequest = (emitter, masterEvent, requestType, releaseType, grantType) => {
return (worker, event) => {
if (event.data.type !== requestType) {
return;
}
const requestEvent = {
type: event.data.type,
name: event.data.name,
identifier: event.data.identifier
};
emitter.dispatchEvent(new MessageEvent(masterEvent, {
data: {
name: requestEvent.name,
handler: async () => {
// grant lock to worker
worker.postMessage({
type: grantType,
name: requestEvent.name,
identifier: requestEvent.identifier
});
// wait for worker to finish
return await new Promise((resolve) => {
const releaseEventListener = (event) => {
if (event == null || event.data == null) {
return;
}
const releaseEvent = {
type: event.data.type,
name: event.data.name,
identifier: event.data.identifier
};
if (releaseEvent.type === releaseType && releaseEvent.identifier === requestEvent.identifier) {
worker.removeEventListener('message', releaseEventListener);
resolve();
}
};
worker.addEventListener('message', releaseEventListener);
});
}
}
}));
};
};
const makeWorkerLockRequest = (name, requestType, grantType, releaseType) => {
return async () => {
const id = nanoid();
globalThis.postMessage({
type: requestType,
identifier: id,
name
});
return await new Promise((resolve) => {
const listener = (event) => {
if (event == null || event.data == null) {
return;
}
const responseEvent = {
type: event.data.type,
identifier: event.data.identifier
};
if (responseEvent.type === grantType && responseEvent.identifier === id) {
globalThis.removeEventListener('message', listener);
// grant lock
resolve(() => {
// release lock
globalThis.postMessage({
type: releaseType,
identifier: id,
name
});
});
}
};
globalThis.addEventListener('message', listener);
});
};
};
const defaultOptions = {
singleProcess: false
};
export default (options) => {
options = Object.assign({}, defaultOptions, options);
const isPrimary = Boolean(globalThis.document) || options.singleProcess;
if (isPrimary) {
const emitter = new EventTarget();
observer.addEventListener('message', handleWorkerLockRequest(emitter, 'requestReadLock', WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK));
observer.addEventListener('message', handleWorkerLockRequest(emitter, 'requestWriteLock', WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK));
return emitter;
}
return {
isWorker: true,
readLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_RELEASE_READ_LOCK),
writeLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK)
};
};
//# sourceMappingURL=browser.js.map
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"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"}
export declare const WORKER_REQUEST_READ_LOCK = "lock:worker:request-read";
export declare const WORKER_RELEASE_READ_LOCK = "lock:worker:release-read";
export declare const MASTER_GRANT_READ_LOCK = "lock:master:grant-read";
export declare const WORKER_REQUEST_WRITE_LOCK = "lock:worker:request-write";
export declare const WORKER_RELEASE_WRITE_LOCK = "lock:worker:release-write";
export declare const MASTER_GRANT_WRITE_LOCK = "lock:master:grant-write";
//# sourceMappingURL=constants.d.ts.map
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,wBAAwB,6BAA6B,CAAA;AAClE,eAAO,MAAM,wBAAwB,6BAA6B,CAAA;AAClE,eAAO,MAAM,sBAAsB,2BAA2B,CAAA;AAE9D,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AACpE,eAAO,MAAM,yBAAyB,8BAA8B,CAAA;AACpE,eAAO,MAAM,uBAAuB,4BAA4B,CAAA"}
export const WORKER_REQUEST_READ_LOCK = 'lock:worker:request-read';
export const WORKER_RELEASE_READ_LOCK = 'lock:worker:release-read';
export const MASTER_GRANT_READ_LOCK = 'lock:master:grant-read';
export const WORKER_REQUEST_WRITE_LOCK = 'lock:worker:request-write';
export const WORKER_RELEASE_WRITE_LOCK = 'lock:worker:release-write';
export const MASTER_GRANT_WRITE_LOCK = 'lock:master:grant-write';
//# sourceMappingURL=constants.js.map
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAA;AAClE,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAA;AAClE,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAA;AAE9D,MAAM,CAAC,MAAM,yBAAyB,GAAG,2BAA2B,CAAA;AACpE,MAAM,CAAC,MAAM,yBAAyB,GAAG,2BAA2B,CAAA;AACpE,MAAM,CAAC,MAAM,uBAAuB,GAAG,yBAAyB,CAAA"}
export interface MorticeOptions {
name?: string;
timeout?: number;
concurrency?: number;
singleProcess?: boolean;
global?: typeof globalThis;
}
export interface Mortice {
readLock: () => Promise<Release>;
writeLock: () => Promise<Release>;
}
export interface Release {
(): void;
}
export interface MorticeImplementation {
isWorker: boolean;
readLock: (name: string, options: MorticeOptions) => Mortice['readLock'];
writeLock: (name: string, options: MorticeOptions) => Mortice['writeLock'];
}
export default function createMortice(options?: MorticeOptions): Mortice;
//# sourceMappingURL=index.d.ts.map
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,OAAO,UAAU,CAAA;CAC3B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAChC,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,IAAI,CAAA;CACT;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;IACxE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;CAC3E;AA6FD,MAAM,CAAC,OAAO,UAAU,aAAa,CAAE,OAAO,CAAC,EAAE,cAAc,WAiC9D"}
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 () => await pTimeout((async () => {
return await new Promise((resolve) => {
res(() => {
resolve();
});
});
})(), options.timeout));
return await 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 await 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
return await localReadQueue.onIdle()
.then(() => {
if (readQueue === localReadQueue) {
readQueue = null;
}
});
});
return await 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 await createReleaseable(masterQueue, options);
}
};
};
const defaultOptions = {
name: 'lock',
concurrency: Infinity,
timeout: 84600000,
global: globalThis,
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) => await 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) => await 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
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
import type { MorticeImplementation, MorticeOptions } from './index.js';
declare const _default: (options: Required<MorticeOptions>) => MorticeImplementation | EventTarget | undefined;
export default _default;
//# sourceMappingURL=node.d.ts.map
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/node.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAW,MAAM,YAAY,CAAA;kCAkFvD,SAAS,cAAc,CAAC,KAAG,qBAAqB,GAAG,WAAW,GAAG,SAAS;AAAnG,wBAeC"}
import { nanoid } from 'nanoid';
import { WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK } from './constants.js';
import cluster from 'cluster';
const handleWorkerLockRequest = (emitter, masterEvent, requestType, releaseType, grantType) => {
return (worker, requestEvent) => {
if (requestEvent != null && requestEvent.type === requestType) {
emitter.dispatchEvent(new MessageEvent(masterEvent, {
data: {
name: requestEvent.name,
handler: async () => {
// grant lock to worker
worker.send({
type: grantType,
name: requestEvent.name,
identifier: requestEvent.identifier
});
// wait for worker to finish
return await new Promise((resolve) => {
const releaseEventListener = (releaseEvent) => {
if (releaseEvent.type === releaseType && releaseEvent.identifier === requestEvent.identifier) {
worker.removeListener('message', releaseEventListener);
resolve();
}
};
worker.on('message', releaseEventListener);
});
}
}
}));
}
};
};
const makeWorkerLockRequest = (name, requestType, grantType, releaseType) => {
return async () => {
const id = nanoid();
if (process.send == null) {
throw new Error('No send method on process - are we a cluster worker?');
}
process.send({
type: requestType,
identifier: id,
name
});
return await new Promise((resolve) => {
const listener = (event) => {
if (event.type === grantType && event.identifier === id) {
process.removeListener('message', listener);
// grant lock
resolve(() => {
if (process.send == null) {
throw new Error('No send method on process - are we a cluster worker?');
}
// release lock
process.send({
type: releaseType,
identifier: id,
name
});
});
}
};
process.on('message', listener);
});
};
};
export default (options) => {
if (cluster.isPrimary || options.singleProcess) {
const emitter = new EventTarget();
cluster.on('message', handleWorkerLockRequest(emitter, 'requestReadLock', WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK));
cluster.on('message', handleWorkerLockRequest(emitter, 'requestWriteLock', WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK));
return emitter;
}
return {
isWorker: true,
readLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_RELEASE_READ_LOCK),
writeLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK)
};
};
//# sourceMappingURL=node.js.map
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/node.ts"],"names":[],"mappings":"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"}
This project is dual licensed under MIT and Apache-2.0.
MIT: https://www.opensource.org/licenses/mit
Apache-2.0: https://www.apache.org/licenses/license-2.0
import { nanoid } from 'nanoid'
import {
WORKER_REQUEST_READ_LOCK,
WORKER_RELEASE_READ_LOCK,
MASTER_GRANT_READ_LOCK,
WORKER_REQUEST_WRITE_LOCK,
WORKER_RELEASE_WRITE_LOCK,
MASTER_GRANT_WRITE_LOCK
} from './constants.js'
import observer from 'observable-webworkers'
import type { MorticeImplementation, MorticeOptions, Release } from './index.js'
const handleWorkerLockRequest = (emitter: EventTarget, masterEvent: string, requestType: string, releaseType: string, grantType: string) => {
return (worker: Worker, event: MessageEvent) => {
if (event.data.type !== requestType) {
return
}
const requestEvent = {
type: event.data.type,
name: event.data.name,
identifier: event.data.identifier
}
emitter.dispatchEvent(new MessageEvent(masterEvent, {
data: {
name: requestEvent.name,
handler: async (): Promise<void> => {
// grant lock to worker
worker.postMessage({
type: grantType,
name: requestEvent.name,
identifier: requestEvent.identifier
})
// wait for worker to finish
return await new Promise<void>((resolve) => {
const releaseEventListener = (event: MessageEvent) => {
if (event == null || event.data == null) {
return
}
const releaseEvent = {
type: event.data.type,
name: event.data.name,
identifier: event.data.identifier
}
if (releaseEvent.type === releaseType && releaseEvent.identifier === requestEvent.identifier) {
worker.removeEventListener('message', releaseEventListener)
resolve()
}
}
worker.addEventListener('message', releaseEventListener)
})
}
}
}))
}
}
const makeWorkerLockRequest = (name: string, requestType: string, grantType: string, releaseType: string) => {
return async () => {
const id = nanoid()
globalThis.postMessage({
type: requestType,
identifier: id,
name
})
return await new Promise<Release>((resolve) => {
const listener = (event: MessageEvent) => {
if (event == null || event.data == null) {
return
}
const responseEvent = {
type: event.data.type,
identifier: event.data.identifier
}
if (responseEvent.type === grantType && responseEvent.identifier === id) {
globalThis.removeEventListener('message', listener)
// grant lock
resolve(() => {
// release lock
globalThis.postMessage({
type: releaseType,
identifier: id,
name
})
})
}
}
globalThis.addEventListener('message', listener)
})
}
}
const defaultOptions = {
singleProcess: false
}
export default (options: Required<MorticeOptions>): MorticeImplementation | EventTarget => {
options = Object.assign({}, defaultOptions, options)
const isPrimary = Boolean(globalThis.document) || options.singleProcess
if (isPrimary) {
const emitter = new EventTarget()
observer.addEventListener('message', handleWorkerLockRequest(emitter, 'requestReadLock', WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK))
observer.addEventListener('message', handleWorkerLockRequest(emitter, 'requestWriteLock', WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK))
return emitter
}
return {
isWorker: true,
readLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_RELEASE_READ_LOCK),
writeLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK)
}
}
export const WORKER_REQUEST_READ_LOCK = 'lock:worker:request-read'
export const WORKER_RELEASE_READ_LOCK = 'lock:worker:release-read'
export const MASTER_GRANT_READ_LOCK = 'lock:master:grant-read'
export const WORKER_REQUEST_WRITE_LOCK = 'lock:worker:request-write'
export const WORKER_RELEASE_WRITE_LOCK = 'lock:worker:release-write'
export const MASTER_GRANT_WRITE_LOCK = 'lock:master:grant-write'
import PQueue from 'p-queue'
import pTimeout from 'p-timeout'
import impl from './node.js'
export interface MorticeOptions {
name?: string
timeout?: number
concurrency?: number
singleProcess?: boolean
global?: typeof globalThis
}
export interface Mortice {
readLock: () => Promise<Release>
writeLock: () => Promise<Release>
}
export interface Release {
(): void
}
export interface MorticeImplementation {
isWorker: boolean
readLock: (name: string, options: MorticeOptions) => Mortice['readLock']
writeLock: (name: string, options: MorticeOptions) => Mortice['writeLock']
}
const mutexes: Record<string, Mortice> = {}
let implementation: any
async function createReleaseable (queue: PQueue, options: Required<MorticeOptions>): Promise<Release> {
let res: (release: Release) => void
const p = new Promise<Release>((resolve) => {
res = resolve
})
void queue.add(async () => await pTimeout((async () => {
return await new Promise<void>((resolve) => {
res(() => {
resolve()
})
})
})(), options.timeout))
return await p
}
const createMutex = (name: string, options: Required<MorticeOptions>): Mortice => {
if (implementation.isWorker === true) {
return {
readLock: implementation.readLock(name, options),
writeLock: implementation.writeLock(name, options)
}
}
const masterQueue = new PQueue({ concurrency: 1 })
let readQueue: PQueue | null
return {
async readLock () {
// If there's already a read queue, just add the task to it
if (readQueue != null) {
return await 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
return await localReadQueue.onIdle()
.then(() => {
if (readQueue === localReadQueue) {
readQueue = null
}
})
})
return await 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 await createReleaseable(masterQueue, options)
}
}
}
const defaultOptions = {
name: 'lock',
concurrency: Infinity,
timeout: 84600000,
global: globalThis,
singleProcess: false
}
interface EventData {
name: string
handler: () => Promise<void>
}
export default function createMortice (options?: MorticeOptions) {
const opts: Required<MorticeOptions> = 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: MessageEvent<EventData>) => {
if (mutexes[event.data.name] == null) {
return
}
void mutexes[event.data.name].readLock()
.then(async release => await event.data.handler().finally(() => release()))
})
implementation.addEventListener('requestWriteLock', async (event: MessageEvent<EventData>) => {
if (mutexes[event.data.name] == null) {
return
}
void mutexes[event.data.name].writeLock()
.then(async release => await event.data.handler().finally(() => release()))
})
}
}
if (mutexes[opts.name] == null) {
mutexes[opts.name] = createMutex(opts.name, opts)
}
return mutexes[opts.name]
}
import { nanoid } from 'nanoid'
import {
WORKER_REQUEST_READ_LOCK,
WORKER_RELEASE_READ_LOCK,
MASTER_GRANT_READ_LOCK,
WORKER_REQUEST_WRITE_LOCK,
WORKER_RELEASE_WRITE_LOCK,
MASTER_GRANT_WRITE_LOCK
} from './constants.js'
import type { MorticeImplementation, MorticeOptions, Release } from './index.js'
import cluster from 'cluster'
import type { Worker } from 'cluster'
interface RequestEvent {
type: string
identifier: string
name: string
}
const handleWorkerLockRequest = (emitter: EventTarget, masterEvent: string, requestType: string, releaseType: string, grantType: string) => {
return (worker: Worker, requestEvent: RequestEvent) => {
if (requestEvent != null && requestEvent.type === requestType) {
emitter.dispatchEvent(new MessageEvent(masterEvent, {
data: {
name: requestEvent.name,
handler: async () => {
// grant lock to worker
worker.send({
type: grantType,
name: requestEvent.name,
identifier: requestEvent.identifier
})
// wait for worker to finish
return await new Promise<void>((resolve) => {
const releaseEventListener = (releaseEvent: RequestEvent) => {
if (releaseEvent.type === releaseType && releaseEvent.identifier === requestEvent.identifier) {
worker.removeListener('message', releaseEventListener)
resolve()
}
}
worker.on('message', releaseEventListener)
})
}
}
}))
}
}
}
const makeWorkerLockRequest = (name: string, requestType: string, grantType: string, releaseType: string) => {
return async () => {
const id = nanoid()
if (process.send == null) {
throw new Error('No send method on process - are we a cluster worker?')
}
process.send({
type: requestType,
identifier: id,
name
})
return await new Promise<Release>((resolve) => {
const listener = (event: RequestEvent) => {
if (event.type === grantType && event.identifier === id) {
process.removeListener('message', listener)
// grant lock
resolve(() => {
if (process.send == null) {
throw new Error('No send method on process - are we a cluster worker?')
}
// release lock
process.send({
type: releaseType,
identifier: id,
name
})
})
}
}
process.on('message', listener)
})
}
}
export default (options: Required<MorticeOptions>): MorticeImplementation | EventTarget | undefined => {
if (cluster.isPrimary || options.singleProcess) {
const emitter = new EventTarget()
cluster.on('message', handleWorkerLockRequest(emitter, 'requestReadLock', WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK))
cluster.on('message', handleWorkerLockRequest(emitter, 'requestWriteLock', WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK))
return emitter
}
return {
isWorker: true,
readLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_RELEASE_READ_LOCK),
writeLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK)
}
}
+135
-31
{
"name": "mortice",
"version": "2.0.1",
"version": "3.0.0",
"description": "Isomorphic read/write lock that works in single processes, node clusters and web workers",
"main": "lib/index.js",
"scripts": {
"test": "ava",
"lint": "standard"
},
"keywords": [
"read-write",
"cluster",
"mutex",
"lock",
"await",
"async"
],
"author": "Alex Potsides <alex@achingbrain.net>",
"license": "ISC",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/achingbrain/mortice#readme",
"repository": {

@@ -27,26 +15,142 @@ "type": "git",

},
"homepage": "https://github.com/achingbrain/mortice",
"devDependencies": {
"ava": "^3.15.0",
"browserify": "^17.0.0",
"delay": "^5.0.0",
"execa": "^5.0.0",
"run-headless": "^2.0.1",
"standard": "^16.0.3",
"webworkify": "^1.5.0"
"keywords": [
"async",
"await",
"cluster",
"lock",
"mutex",
"read-write"
],
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
},
"ava": {
"files": [
"test/*.test.js"
"type": "module",
"types": "./dist/src/index.d.ts",
"files": [
"src",
"dist/src",
"!dist/test",
"!**/*.tsbuildinfo"
],
"exports": {
".": {
"import": "./dist/src/index.js"
}
},
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
}
},
"release": {
"branches": [
"master"
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{
"breaking": true,
"release": "major"
},
{
"revert": true,
"release": "patch"
},
{
"type": "feat",
"release": "minor"
},
{
"type": "fix",
"release": "patch"
},
{
"type": "chore",
"release": "patch"
},
{
"type": "docs",
"release": "patch"
},
{
"type": "test",
"release": "patch"
},
{
"scope": "no-release",
"release": false
}
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "chore",
"section": "Trivial Changes"
},
{
"type": "docs",
"section": "Trivial Changes"
},
{
"type": "test",
"section": "Tests"
}
]
}
}
],
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
"@semantic-release/git"
]
},
"scripts": {
"lint": "aegir lint",
"pretest": "npm run build",
"test": "aegir test",
"test:node": "npm run test -- -f dist/test/node.js -t node --cov",
"test:chrome": "npm run test -- -f dist/test/browser.js -t browser --cov",
"test:firefox": "npm run test -- -f dist/test/browser.js -t browser -- --browser firefox",
"test:electron-main": "npm run test -- -f dist/test/node.js -t electron-main",
"build": "tsc && node esbuild.js",
"release": "semantic-release"
},
"dependencies": {
"nanoid": "^3.1.20",
"observable-webworkers": "^1.0.0",
"p-queue": "^6.0.0",
"promise-timeout": "^1.3.0"
"observable-webworkers": "^2.0.1",
"p-queue": "^7.2.0",
"p-timeout": "^5.0.2"
},
"devDependencies": {
"aegir": "^36.1.3",
"delay": "^5.0.0",
"esbuild": "^0.14.21",
"execa": "^6.0.0"
},
"browser": {
"cluster": false
"cluster": false,
"./dist/src/node.js": "./dist/src/browser.js",
"./src/node.js": "./src/browser.js"
}
}
+8
-20
# mortice
[![Build status](https://travis-ci.org/achingbrain/mortice.svg?branch=master)](https://travis-ci.org/achingbrain/mortice)
[![Build Status](https://github.com/achingbrain/mortice/actions/workflows/js-test-and-release.yml/badge.svg?branch=main)](https://github.com/achingbrain/mortice/actions/workflows/js-test-and-release.yml)
Isomorphic read/write lock that works in single processes, node clusters and web workers.
> Isomorphic read/write lock that works in single processes, node clusters and web workers.

@@ -24,4 +24,4 @@ ## Features

```javascript
const mortice = require('mortice')
const delay = require('delay')
import mortice from 'mortice'
import delay from 'delay'

@@ -98,4 +98,4 @@ // the lock name & options objects are both optional

// main.js
const mortice = require('mortice')
const observe = require('observable-webworkers')
import mortice from 'mortice'
import observe from 'observable-webworkers'

@@ -112,4 +112,4 @@ // create our lock on the main thread, it will be held here

// worker.js
const mortice = require('mortice')
const delay = require('delay')
import mortice from 'mortice'
import delay from 'delay'

@@ -126,13 +126,1 @@ const mutex = mortice()

```
Alternatively you can use the bundled `mortice.Worker` to create web workers and save yourself an extra dependency.
```javascript
const mortice = require('mortice')
const Worker = mortice.Worker
// create our lock on the main thread, it will be held here
const mutex = mortice()
const worker = new Worker('worker.js')
```
sudo: required
language: node_js
addons:
chrome: stable
node_js:
- '10'
- '12'
const EventEmitter = require('events').EventEmitter
const { nanoid } = require('nanoid')
const {
WORKER_REQUEST_READ_LOCK,
WORKER_RELEASE_READ_LOCK,
MASTER_GRANT_READ_LOCK,
WORKER_REQUEST_WRITE_LOCK,
WORKER_RELEASE_WRITE_LOCK,
MASTER_GRANT_WRITE_LOCK
} = require('./constants')
const observer = require('observable-webworkers')
const handleWorkerLockRequest = (emitter, masterEvent, requestType, releaseType, grantType) => {
return (worker, event) => {
if (!event || !event.data || event.data.type !== requestType) {
return
}
const requestEvent = {
type: event.data.type,
name: event.data.name,
identifier: event.data.identifier
}
emitter.emit(masterEvent, requestEvent.name, () => {
// grant lock to worker
worker.postMessage({
type: grantType,
name: requestEvent.name,
identifier: requestEvent.identifier
})
// wait for worker to finish
return new Promise((resolve) => {
const releaseEventListener = (event) => {
if (!event || !event.data) {
return
}
const releaseEvent = {
type: event.data.type,
name: event.data.name,
identifier: event.data.identifier
}
if (releaseEvent && releaseEvent.type === releaseType && releaseEvent.identifier === requestEvent.identifier) {
worker.removeEventListener('message', releaseEventListener)
resolve()
}
}
worker.addEventListener('message', releaseEventListener)
})
})
}
}
const makeWorkerLockRequest = (name, requestType, grantType, releaseType) => {
return () => {
const id = nanoid()
globalThis.postMessage({
type: requestType,
identifier: id,
name
})
return new Promise((resolve) => {
const listener = (event) => {
if (!event || !event.data) {
return
}
const responseEvent = {
type: event.data.type,
identifier: event.data.identifier
}
if (responseEvent && responseEvent.type === grantType && responseEvent.identifier === id) {
globalThis.removeEventListener('message', listener)
// grant lock
resolve(() => {
// release lock
globalThis.postMessage({
type: releaseType,
identifier: id,
name
})
})
}
}
globalThis.addEventListener('message', listener)
})
}
}
const defaultOptions = {
singleProcess: false
}
module.exports = (options) => {
options = Object.assign({}, defaultOptions, options)
const isMaster = !!globalThis.document || options.singleProcess
if (isMaster) {
const emitter = new EventEmitter()
observer.addEventListener('message', handleWorkerLockRequest(emitter, 'requestReadLock', WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK))
observer.addEventListener('message', handleWorkerLockRequest(emitter, 'requestWriteLock', WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK))
return emitter
}
return {
isWorker: true,
readLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_RELEASE_READ_LOCK),
writeLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK)
}
}
module.exports = {
WORKER_REQUEST_READ_LOCK: 'lock:worker:request-read',
WORKER_RELEASE_READ_LOCK: 'lock:worker:release-read',
MASTER_GRANT_READ_LOCK: 'lock:master:grant-read',
WORKER_REQUEST_WRITE_LOCK: 'lock:worker:request-write',
WORKER_RELEASE_WRITE_LOCK: 'lock:worker:release-write',
MASTER_GRANT_WRITE_LOCK: 'lock:master:grant-write'
}
const node = require('./node')
const browser = require('./browser')
const { default: Queue } = require('p-queue')
const { timeout } = require('promise-timeout')
const observe = require('observable-webworkers')
const mutexes = {}
let implementation
function createReleaseable (queue, options) {
let res
const p = new Promise((resolve) => {
res = resolve
})
queue.add(() => timeout((() => {
return new Promise((resolve) => {
res(() => {
resolve()
})
})
})(), options.timeout))
return p
}
const createMutex = (name, options) => {
if (implementation.isWorker) {
return {
readLock: implementation.readLock(name, options),
writeLock: implementation.writeLock(name, options)
}
}
const masterQueue = new Queue({ concurrency: 1 })
let readQueue = null
return {
readLock: () => {
// If there's already a read queue, just add the task to it
if (readQueue) {
return createReleaseable(readQueue, options)
}
// Create a new read queue
readQueue = new Queue({
concurrency: options.concurrency,
autoStart: false
})
const localReadQueue = readQueue
// Add the task to the read queue
const readPromise = createReleaseable(readQueue, options)
masterQueue.add(() => {
// 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
return localReadQueue.onIdle()
.then(() => {
if (readQueue === localReadQueue) {
readQueue = null
}
})
})
return readPromise
},
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 = {
concurrency: Infinity,
timeout: 84600000,
global: global,
singleProcess: false
}
module.exports = (name, options) => {
if (!options) {
options = {}
}
if (typeof name === 'object') {
options = name
name = 'lock'
}
if (!name) {
name = 'lock'
}
options = Object.assign({}, defaultOptions, options)
if (!implementation) {
implementation = node(options) || browser(options)
if (!implementation.isWorker) {
// we are master, set up worker requests
implementation.on('requestReadLock', (name, fn) => {
if (!mutexes[name]) {
return
}
mutexes[name].readLock()
.then(release => fn().finally(() => release()))
})
implementation.on('requestWriteLock', async (name, fn) => {
if (!mutexes[name]) {
return
}
mutexes[name].writeLock()
.then(release => fn().finally(() => release()))
})
}
}
if (!mutexes[name]) {
mutexes[name] = createMutex(name, options)
}
return mutexes[name]
}
module.exports.Worker = function (script, Impl) {
Impl = Impl || global.Worker
let worker
try {
worker = new Impl(script)
} catch (error) {
if (error.message.includes('not a constructor')) {
worker = Impl(script)
}
}
if (!worker) {
throw new Error('Could not create Worker from', Impl)
}
observe(worker)
return worker
}
const EventEmitter = require('events').EventEmitter
const { nanoid } = require('nanoid')
const {
WORKER_REQUEST_READ_LOCK,
WORKER_RELEASE_READ_LOCK,
MASTER_GRANT_READ_LOCK,
WORKER_REQUEST_WRITE_LOCK,
WORKER_RELEASE_WRITE_LOCK,
MASTER_GRANT_WRITE_LOCK
} = require('./constants')
let cluster
const handleWorkerLockRequest = (emitter, masterEvent, requestType, releaseType, grantType) => {
return (worker, requestEvent) => {
if (requestEvent && requestEvent.type === requestType) {
emitter.emit(masterEvent, requestEvent.name, () => {
// grant lock to worker
worker.send({
type: grantType,
name: requestEvent.name,
identifier: requestEvent.identifier
})
// wait for worker to finish
return new Promise((resolve) => {
const releaseEventListener = (releaseEvent) => {
if (releaseEvent && releaseEvent.type === releaseType && releaseEvent.identifier === requestEvent.identifier) {
worker.removeListener('message', releaseEventListener)
resolve()
}
}
worker.on('message', releaseEventListener)
})
})
}
}
}
const makeWorkerLockRequest = (name, requestType, grantType, releaseType) => {
return () => {
const id = nanoid()
process.send({
type: requestType,
identifier: id,
name
})
return new Promise((resolve) => {
const listener = (event) => {
if (event && event.type === grantType && event.identifier === id) {
process.removeListener('message', listener)
// grant lock
resolve(() => {
// release lock
process.send({
type: releaseType,
identifier: id,
name
})
})
}
}
process.on('message', listener)
})
}
}
module.exports = (options) => {
try {
cluster = require('cluster')
if (!Object.keys(cluster).length) {
return
}
} catch (_) {
return
}
if (cluster.isMaster || options.singleProcess) {
const emitter = new EventEmitter()
cluster.on('message', handleWorkerLockRequest(emitter, 'requestReadLock', WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK))
cluster.on('message', handleWorkerLockRequest(emitter, 'requestWriteLock', WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK))
return emitter
}
return {
isWorker: true,
readLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_RELEASE_READ_LOCK),
writeLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK)
}
}
const test = require('ava')
const exec = require('execa')
const path = require('path')
const browserify = require('browserify')
test('executes locks in correct order', async (t) => {
const result = await exec('run-headless', {
input: browserify([path.join(__dirname, 'fixtures', 'browser.js')]).bundle()
})
t.is(result.stdout, `write 1 waiting
read 1 waiting
read 2 waiting
read 3 waiting
write 2 waiting
read 4 waiting
write 1 start
write 1 complete
read 1 start
read 1 complete
read 2 start
read 2 complete
read 3 start
read 3 complete
write 2 start
write 2 complete
read 4 start
read 4 complete`)
})
const test = require('ava')
const exec = require('execa')
const path = require('path')
test('executes locks in correct order', async (t) => {
const result = await exec('node', [path.join(__dirname, 'fixtures', 'cluster.js')])
t.is(result.stdout, `write 1 waiting
read 1 waiting
read 2 waiting
read 3 waiting
write 2 waiting
read 4 waiting
write 1 start
write 1 complete
read 1 start
read 1 complete
read 2 start
read 2 complete
read 3 start
read 3 complete
write 2 start
write 2 complete
read 4 start
read 4 complete`)
})
test('executes locks in correct order on a single process', async (t) => {
const result = await exec('node', [path.join(__dirname, 'fixtures', 'cluster-single-thread.js')])
t.is(result.stdout, `write 1 waiting
read 1 waiting
read 2 waiting
read 3 waiting
write 2 waiting
read 4 waiting
write 1 start
write 1 complete
read 1 start
read 1 complete
read 2 start
read 2 complete
read 3 start
read 3 complete
write 2 start
write 2 complete
read 4 start
read 4 complete`)
})
const mortice = require('../../')
const delay = require('delay')
const counts = {
read: 0,
write: 0
}
async function lock (type, muxex, timeout = 0) {
counts[type]++
const index = counts[type]
console.info(`${type} ${index} waiting`)
const release = await muxex[`${type}Lock`]()
console.info(`${type} ${index} start`)
if (timeout) {
await delay(timeout)
}
console.info(`${type} ${index} complete`)
release()
if (type === 'read' && index === 4) {
global.__close__()
}
}
async function run () {
const mutex = mortice()
// queue up read/write requests, the third read should block the second write
lock('write', mutex)
lock('read', mutex)
lock('read', mutex)
lock('read', mutex, 500)
lock('write', mutex)
lock('read', mutex)
}
run()
const cluster = require('cluster')
const mortice = require('../../')
const delay = require('delay')
const counts = {
read: 0,
write: 0
}
async function lock (type, muxex, timeout = 0) {
counts[type]++
const index = counts[type]
console.info(`${type} ${index} waiting`)
const release = await muxex[`${type}Lock`]()
console.info(`${type} ${index} start`)
if (timeout) {
await delay(timeout)
}
console.info(`${type} ${index} complete`)
release()
if (type === 'read' && index === 4) {
process.send('done')
}
}
async function run () {
const mutex = mortice({
singleProcess: true
})
if (cluster.isMaster) {
cluster.on('message', (worker, message) => {
if (message === 'done') {
worker.kill()
}
})
cluster.fork()
} else {
// queue up read/write requests, the third read should block the second write
lock('write', mutex)
lock('read', mutex)
lock('read', mutex)
lock('read', mutex, 500)
lock('write', mutex)
lock('read', mutex)
}
}
run()
.then(() => {})
const cluster = require('cluster')
const mortice = require('../../')
const delay = require('delay')
const counts = {
read: 0,
write: 0
}
async function lock (type, muxex, timeout = 0) {
counts[type]++
const index = counts[type]
console.info(`${type} ${index} waiting`)
const release = await muxex[`${type}Lock`]()
console.info(`${type} ${index} start`)
if (timeout) {
await delay(timeout)
}
console.info(`${type} ${index} complete`)
release()
if (type === 'read' && index === 4) {
process.send('done')
}
}
async function run () {
const mutex = mortice()
if (cluster.isMaster) {
cluster.on('message', (worker, message) => {
if (message === 'done') {
worker.kill()
}
})
cluster.fork()
} else {
// queue up read/write requests, the third read should block the second write
lock('write', mutex)
lock('read', mutex)
lock('read', mutex)
lock('read', mutex, 500)
lock('write', mutex)
lock('read', mutex)
}
}
run()
.then(() => {})
const work = require('webworkify')
const observe = require('observable-webworkers')
const mortice = require('../../')
const Worker = mortice.Worker
mortice()
new Worker(require('./worker.js'), work) // eslint-disable-line
observe.addEventListener('message', (worker, event) => {
if (event.data) {
if (event.data.type === 'log') {
console.info(event.data.message)
}
if (event.data.type === 'done') {
worker.terminate()
return global.__close__()
}
}
})
const mortice = require('../../')
async function read (muxex) {
const release = await muxex.readLock()
try {
console.info('read 1')
throw new Error('err')
} finally {
release()
}
}
async function write (muxex) {
const release = await muxex.writeLock()
await new Promise((resolve) => {
console.info('write 1')
resolve()
})
release()
}
async function run () {
const mutex = mortice()
read(mutex)
.catch(() => {})
write(mutex)
}
run()
.then(() => {})
const mortice = require('../../')
const delay = require('delay')
const counts = {
read: 0,
write: 0
}
async function lock (type, muxex, timeout = 0) {
counts[type]++
const index = counts[type]
console.info(`${type} ${index} waiting`)
const release = await muxex[`${type}Lock`]()
console.info(`${type} ${index} start`)
if (timeout) {
await delay(timeout)
}
console.info(`${type} ${index} complete`)
release()
}
async function run () {
const mutex = mortice()
// read should complete before write
lock('read', mutex, 500)
lock('write', mutex)
}
run()
.then(() => {})
const mortice = require('../../')
const delay = require('delay')
const counts = {
read: 0,
write: 0
}
async function lock (type, muxex, timeout = 0) {
counts[type]++
const index = counts[type]
console.info(`${type} ${index} waiting`)
const release = await muxex[`${type}Lock`]()
console.info(`${type} ${index} start`)
if (timeout) {
await delay(timeout)
}
console.info(`${type} ${index} complete`)
release()
}
async function run () {
const mutex = mortice()
// queue up read/write requests, the third read should block the second write
lock('write', mutex)
lock('read', mutex)
lock('read', mutex)
lock('read', mutex, 500)
lock('write', mutex)
lock('read', mutex)
}
run()
.then(() => {})
const work = require('webworkify')
const mortice = require('../../')
mortice()
const observe = require('observable-webworkers')
const worker = work(require('./worker-single-thread.js'))
observe(worker)
observe.addEventListener('message', (worker, event) => {
if (event.data) {
if (event.data.type === 'log') {
console.info(event.data.message)
}
if (event.data.type === 'done') {
worker.terminate()
return global.__close__()
}
}
})
const work = require('webworkify')
const mortice = require('../../')
mortice()
const observe = require('observable-webworkers')
const worker = work(require('./worker.js'))
observe(worker)
observe.addEventListener('message', (worker, event) => {
if (event.data) {
if (event.data.type === 'log') {
console.info(event.data.message)
}
if (event.data.type === 'done') {
worker.terminate()
return global.__close__()
}
}
})
const mortice = require('../../')
const delay = require('delay')
const counts = {
read: 0,
write: 0
}
async function lock (type, muxex, timeout = 0) {
counts[type]++
const index = counts[type]
globalThis.postMessage({
type: 'log',
message: `${type} ${index} waiting`
})
const release = await muxex[`${type}Lock`]()
globalThis.postMessage({
type: 'log',
message: `${type} ${index} start`
})
if (timeout) {
await delay(timeout)
}
globalThis.postMessage({
type: 'log',
message: `${type} ${index} complete`
})
release()
if (type === 'read' && index === 4) {
globalThis.postMessage({
type: 'done'
})
}
}
module.exports = () => {
const mutex = mortice({
singleProcess: true
})
lock('write', mutex)
lock('read', mutex)
lock('read', mutex)
lock('read', mutex, 500)
lock('write', mutex)
lock('read', mutex)
}
const mortice = require('../../')
const delay = require('delay')
const counts = {
read: 0,
write: 0
}
async function lock (type, muxex, timeout = 0) {
counts[type]++
const index = counts[type]
globalThis.postMessage({
type: 'log',
message: `${type} ${index} waiting`
})
const release = await muxex[`${type}Lock`]()
globalThis.postMessage({
type: 'log',
message: `${type} ${index} start`
})
if (timeout) {
await delay(timeout)
}
globalThis.postMessage({
type: 'log',
message: `${type} ${index} complete`
})
release()
if (type === 'read' && index === 4) {
globalThis.postMessage({
type: 'done'
})
}
}
module.exports = () => {
const mutex = mortice()
lock('write', mutex)
lock('read', mutex)
lock('read', mutex)
lock('read', mutex, 500)
lock('write', mutex)
lock('read', mutex)
}
const test = require('ava')
const exec = require('execa')
const path = require('path')
const browserify = require('browserify')
test('execute locks in correct order', async (t) => {
const result = await exec('run-headless', {
input: browserify([path.join(__dirname, 'fixtures', 'mortice-workers.js')]).bundle()
})
t.is(result.stdout, `write 1 waiting
read 1 waiting
read 2 waiting
read 3 waiting
write 2 waiting
read 4 waiting
write 1 start
write 1 complete
read 1 start
read 1 complete
read 2 start
read 2 complete
read 3 start
read 3 complete
write 2 start
write 2 complete
read 4 start
read 4 complete`)
})
const test = require('ava')
const exec = require( 'execa')
const path = require('path')
test('executes locks in correct order', async (t) => {
const result = await exec('node', [path.join(__dirname, 'fixtures', 'process.js')])
t.is(result.stdout, `write 1 waiting
read 1 waiting
read 2 waiting
read 3 waiting
write 2 waiting
read 4 waiting
write 1 start
write 1 complete
read 1 start
read 1 complete
read 2 start
read 2 complete
read 3 start
read 3 complete
write 2 start
write 2 complete
read 4 start
read 4 complete`)
})
test('executes read then waits to start write', async (t) => {
const result = await exec('node', [path.join(__dirname, 'fixtures', 'process-read-then-write.js')])
t.is(result.stdout, `read 1 waiting
write 1 waiting
read 1 start
read 1 complete
write 1 start
write 1 complete`)
})
test('continues processing after error', async (t) => {
const result = await exec('node', [path.join(__dirname, 'fixtures', 'process-error-handling.js')])
t.is(result.stdout, `read 1
write 1`)
})
const test = require('ava')
const exec = require('execa')
const path = require('path')
const browserify = require('browserify')
test('execute locks in correct order', async (t) => {
const result = await exec('run-headless', {
input: browserify([path.join(__dirname, 'fixtures', 'web-workers.js')]).bundle()
})
t.is(result.stdout, `write 1 waiting
read 1 waiting
read 2 waiting
read 3 waiting
write 2 waiting
read 4 waiting
write 1 start
write 1 complete
read 1 start
read 1 complete
read 2 start
read 2 complete
read 3 start
read 3 complete
write 2 start
write 2 complete
read 4 start
read 4 complete`)
})
test('execute locks in correct order on single thread', async (t) => {
const result = await exec('run-headless', {
input: browserify([path.join(__dirname, 'fixtures', 'web-workers-single-thread.js')]).bundle()
})
t.is(result.stdout, `write 1 waiting
read 1 waiting
read 2 waiting
read 3 waiting
write 2 waiting
read 4 waiting
write 1 start
write 1 complete
read 1 start
read 1 complete
read 2 start
read 2 complete
read 3 start
read 3 complete
write 2 start
write 2 complete
read 4 start
read 4 complete`)
})