web-locks
Version:
Web Locks API
176 lines (153 loc) • 4.09 kB
JavaScript
'use strict';
const threads = require('worker_threads');
const { isMainThread, parentPort } = threads;
const isWorkerThread = !isMainThread;
const abort = global.AbortController ? global : require('./abort.js');
const { AbortController, AbortSignal, AbortError } = abort;
const LOCKED = 0;
const UNLOCKED = 1;
let locks = null; // LockManager instance
class Lock {
constructor(name, mode = 'exclusive', buffer = null) {
this.name = name;
this.mode = mode; // 'exclusive' or 'shared'
this.queue = [];
this.owner = false;
this.trying = false;
this.buffer = buffer ? buffer : new SharedArrayBuffer(4);
this.flag = new Int32Array(this.buffer, 0, 1);
if (!buffer) Atomics.store(this.flag, 0, UNLOCKED);
}
enter(handler) {
return new Promise((resolve, reject) => {
this.queue.push({ handler, resolve, reject });
this.trying = true;
setTimeout(() => {
this.tryEnter();
}, 0);
});
}
tryEnter() {
if (this.queue.length === 0) return;
const prev = Atomics.exchange(this.flag, 0, LOCKED);
if (prev === LOCKED) return;
this.owner = true;
this.trying = false;
const { handler, resolve, reject } = this.queue.shift();
handler(this)
.then(() => {
this.leave();
resolve();
})
.catch((error) => {
this.leave();
reject(error);
});
}
leave() {
if (!this.owner) return;
Atomics.store(this.flag, 0, UNLOCKED);
this.owner = false;
const message = { webLocks: true, kind: 'leave', name: this.name };
locks.send(message);
this.tryEnter();
}
}
class LockManagerSnapshot {
constructor(resources) {
const held = [];
const pending = [];
this.held = held;
this.pending = pending;
for (const lock of resources) {
if (lock.queue.length > 0) {
pending.push(...lock.queue);
}
if (lock.owner) {
held.push(lock);
}
}
}
}
class LockManager {
constructor() {
this.collection = new Map();
this.workers = new Set();
if (isWorkerThread) {
parentPort.on('message', (message) => {
this.receive(message);
});
}
}
async request(name, options, handler) {
if (typeof options === 'function') {
handler = options;
options = {};
}
const { mode = 'exclusive', signal = null } = options;
let lock = this.collection.get(name);
if (!lock) {
lock = new Lock(name, mode);
this.collection.set(name, lock);
const { buffer } = lock;
const message = { webLocks: true, kind: 'create', name, mode, buffer };
locks.send(message);
}
const finished = lock.enter(handler);
let aborted = null;
if (signal) {
aborted = new Promise((resolve, reject) => {
signal.on('abort', reject);
});
await Promise.race([finished, aborted]);
} else {
await finished;
}
setTimeout(() => {
lock.tryEnter();
}, 0);
return undefined;
}
query() {
const snapshot = new LockManagerSnapshot();
return Promise.resolve(snapshot);
}
attach(worker) {
this.workers.add(worker);
worker.on('message', (message) => {
for (const peer of this.workers) {
if (peer !== worker) {
peer.postMessage(message);
}
}
this.receive(message);
});
}
send(message) {
if (isWorkerThread) {
parentPort.postMessage(message);
return;
}
for (const worker of this.workers) {
worker.postMessage(message);
}
}
receive(message) {
if (!message.webLocks) return;
const { kind, name, mode, buffer } = message;
if (kind === 'create') {
const lock = new Lock(name, mode, buffer);
this.collection.set(name, lock);
return;
}
if (kind === 'leave') {
for (const lock of this.collection.values()) {
if (lock.name === name && lock.trying) {
lock.tryEnter();
}
}
}
}
}
locks = new LockManager();
module.exports = { locks, AbortController, AbortSignal, AbortError };