292 lines
11 KiB
JavaScript
292 lines
11 KiB
JavaScript
const E_TIMEOUT = new Error('timeout while waiting for mutex to become available');
|
|
const E_ALREADY_LOCKED = new Error('mutex already locked');
|
|
const E_CANCELED = new Error('request for lock canceled');
|
|
|
|
var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
class Semaphore {
|
|
constructor(_value, _cancelError = E_CANCELED) {
|
|
this._value = _value;
|
|
this._cancelError = _cancelError;
|
|
this._queue = [];
|
|
this._weightedWaiters = [];
|
|
}
|
|
acquire(weight = 1, priority = 0) {
|
|
if (weight <= 0)
|
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
return new Promise((resolve, reject) => {
|
|
const task = { resolve, reject, weight, priority };
|
|
const i = findIndexFromEnd(this._queue, (other) => priority <= other.priority);
|
|
if (i === -1 && weight <= this._value) {
|
|
// Needs immediate dispatch, skip the queue
|
|
this._dispatchItem(task);
|
|
}
|
|
else {
|
|
this._queue.splice(i + 1, 0, task);
|
|
}
|
|
});
|
|
}
|
|
runExclusive(callback_1) {
|
|
return __awaiter$2(this, arguments, void 0, function* (callback, weight = 1, priority = 0) {
|
|
const [value, release] = yield this.acquire(weight, priority);
|
|
try {
|
|
return yield callback(value);
|
|
}
|
|
finally {
|
|
release();
|
|
}
|
|
});
|
|
}
|
|
waitForUnlock(weight = 1, priority = 0) {
|
|
if (weight <= 0)
|
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
if (this._couldLockImmediately(weight, priority)) {
|
|
return Promise.resolve();
|
|
}
|
|
else {
|
|
return new Promise((resolve) => {
|
|
if (!this._weightedWaiters[weight - 1])
|
|
this._weightedWaiters[weight - 1] = [];
|
|
insertSorted(this._weightedWaiters[weight - 1], { resolve, priority });
|
|
});
|
|
}
|
|
}
|
|
isLocked() {
|
|
return this._value <= 0;
|
|
}
|
|
getValue() {
|
|
return this._value;
|
|
}
|
|
setValue(value) {
|
|
this._value = value;
|
|
this._dispatchQueue();
|
|
}
|
|
release(weight = 1) {
|
|
if (weight <= 0)
|
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
this._value += weight;
|
|
this._dispatchQueue();
|
|
}
|
|
cancel() {
|
|
this._queue.forEach((entry) => entry.reject(this._cancelError));
|
|
this._queue = [];
|
|
}
|
|
_dispatchQueue() {
|
|
this._drainUnlockWaiters();
|
|
while (this._queue.length > 0 && this._queue[0].weight <= this._value) {
|
|
this._dispatchItem(this._queue.shift());
|
|
this._drainUnlockWaiters();
|
|
}
|
|
}
|
|
_dispatchItem(item) {
|
|
const previousValue = this._value;
|
|
this._value -= item.weight;
|
|
item.resolve([previousValue, this._newReleaser(item.weight)]);
|
|
}
|
|
_newReleaser(weight) {
|
|
let called = false;
|
|
return () => {
|
|
if (called)
|
|
return;
|
|
called = true;
|
|
this.release(weight);
|
|
};
|
|
}
|
|
_drainUnlockWaiters() {
|
|
if (this._queue.length === 0) {
|
|
for (let weight = this._value; weight > 0; weight--) {
|
|
const waiters = this._weightedWaiters[weight - 1];
|
|
if (!waiters)
|
|
continue;
|
|
waiters.forEach((waiter) => waiter.resolve());
|
|
this._weightedWaiters[weight - 1] = [];
|
|
}
|
|
}
|
|
else {
|
|
const queuedPriority = this._queue[0].priority;
|
|
for (let weight = this._value; weight > 0; weight--) {
|
|
const waiters = this._weightedWaiters[weight - 1];
|
|
if (!waiters)
|
|
continue;
|
|
const i = waiters.findIndex((waiter) => waiter.priority <= queuedPriority);
|
|
(i === -1 ? waiters : waiters.splice(0, i))
|
|
.forEach((waiter => waiter.resolve()));
|
|
}
|
|
}
|
|
}
|
|
_couldLockImmediately(weight, priority) {
|
|
return (this._queue.length === 0 || this._queue[0].priority < priority) &&
|
|
weight <= this._value;
|
|
}
|
|
}
|
|
function insertSorted(a, v) {
|
|
const i = findIndexFromEnd(a, (other) => v.priority <= other.priority);
|
|
a.splice(i + 1, 0, v);
|
|
}
|
|
function findIndexFromEnd(a, predicate) {
|
|
for (let i = a.length - 1; i >= 0; i--) {
|
|
if (predicate(a[i])) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
class Mutex {
|
|
constructor(cancelError) {
|
|
this._semaphore = new Semaphore(1, cancelError);
|
|
}
|
|
acquire() {
|
|
return __awaiter$1(this, arguments, void 0, function* (priority = 0) {
|
|
const [, releaser] = yield this._semaphore.acquire(1, priority);
|
|
return releaser;
|
|
});
|
|
}
|
|
runExclusive(callback, priority = 0) {
|
|
return this._semaphore.runExclusive(() => callback(), 1, priority);
|
|
}
|
|
isLocked() {
|
|
return this._semaphore.isLocked();
|
|
}
|
|
waitForUnlock(priority = 0) {
|
|
return this._semaphore.waitForUnlock(1, priority);
|
|
}
|
|
release() {
|
|
if (this._semaphore.isLocked())
|
|
this._semaphore.release();
|
|
}
|
|
cancel() {
|
|
return this._semaphore.cancel();
|
|
}
|
|
}
|
|
|
|
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
function withTimeout(sync, timeout, timeoutError = E_TIMEOUT) {
|
|
return {
|
|
acquire: (weightOrPriority, priority) => {
|
|
let weight;
|
|
if (isSemaphore(sync)) {
|
|
weight = weightOrPriority;
|
|
}
|
|
else {
|
|
weight = undefined;
|
|
priority = weightOrPriority;
|
|
}
|
|
if (weight !== undefined && weight <= 0) {
|
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
}
|
|
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
let isTimeout = false;
|
|
const handle = setTimeout(() => {
|
|
isTimeout = true;
|
|
reject(timeoutError);
|
|
}, timeout);
|
|
try {
|
|
const ticket = yield (isSemaphore(sync)
|
|
? sync.acquire(weight, priority)
|
|
: sync.acquire(priority));
|
|
if (isTimeout) {
|
|
const release = Array.isArray(ticket) ? ticket[1] : ticket;
|
|
release();
|
|
}
|
|
else {
|
|
clearTimeout(handle);
|
|
resolve(ticket);
|
|
}
|
|
}
|
|
catch (e) {
|
|
if (!isTimeout) {
|
|
clearTimeout(handle);
|
|
reject(e);
|
|
}
|
|
}
|
|
}));
|
|
},
|
|
runExclusive(callback, weight, priority) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
let release = () => undefined;
|
|
try {
|
|
const ticket = yield this.acquire(weight, priority);
|
|
if (Array.isArray(ticket)) {
|
|
release = ticket[1];
|
|
return yield callback(ticket[0]);
|
|
}
|
|
else {
|
|
release = ticket;
|
|
return yield callback();
|
|
}
|
|
}
|
|
finally {
|
|
release();
|
|
}
|
|
});
|
|
},
|
|
release(weight) {
|
|
sync.release(weight);
|
|
},
|
|
cancel() {
|
|
return sync.cancel();
|
|
},
|
|
waitForUnlock: (weightOrPriority, priority) => {
|
|
let weight;
|
|
if (isSemaphore(sync)) {
|
|
weight = weightOrPriority;
|
|
}
|
|
else {
|
|
weight = undefined;
|
|
priority = weightOrPriority;
|
|
}
|
|
if (weight !== undefined && weight <= 0) {
|
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const handle = setTimeout(() => reject(timeoutError), timeout);
|
|
(isSemaphore(sync)
|
|
? sync.waitForUnlock(weight, priority)
|
|
: sync.waitForUnlock(priority)).then(() => {
|
|
clearTimeout(handle);
|
|
resolve();
|
|
});
|
|
});
|
|
},
|
|
isLocked: () => sync.isLocked(),
|
|
getValue: () => sync.getValue(),
|
|
setValue: (value) => sync.setValue(value),
|
|
};
|
|
}
|
|
function isSemaphore(sync) {
|
|
return sync.getValue !== undefined;
|
|
}
|
|
|
|
// eslint-disable-next-lisne @typescript-eslint/explicit-module-boundary-types
|
|
function tryAcquire(sync, alreadyAcquiredError = E_ALREADY_LOCKED) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return withTimeout(sync, 0, alreadyAcquiredError);
|
|
}
|
|
|
|
export { E_ALREADY_LOCKED, E_CANCELED, E_TIMEOUT, Mutex, Semaphore, tryAcquire, withTimeout };
|