async-monitor
Version:
Provides a mechanism that synchronizes access to objects.
546 lines (528 loc) • 16.9 kB
JavaScript
'use strict';
var async_hooks = require('async_hooks');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol */
function __addDisposableResource(env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
}
var _SuppressedError = typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
function __disposeResources(env) {
function fail(e) {
env.error = env.hasError ? new _SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
function next() {
while (env.stack.length) {
var rec = env.stack.pop();
try {
var result = rec.dispose && rec.dispose.call(rec.value);
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
catch (e) {
fail(e);
}
}
if (env.hasError) throw env.error;
}
return next();
}
async function delay(timeout) {
await new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}
async function nextTick() {
await new Promise((resolve) => {
process.nextTick(resolve);
});
}
function expression(f) {
return f();
}
class Monitor {
pulse() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { done, value: one } = this.waitSet.entries().next();
if (!(done ?? false)) {
const [criticalSection, resolve] = one;
this.waitSet.delete(criticalSection);
resolve();
}
}
pulseAll() {
for (const [criticalSection, resolve] of this.waitSet) {
this.waitSet.delete(criticalSection);
resolve();
}
}
async wait(criticalSection, readyTimeout) {
criticalSection.throwIfAborted();
let reject, resolve;
const readyPromise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
const abortListener = () => {
reject(criticalSection.reason);
};
criticalSection.addEventListener("abort", abortListener);
const hasPulse = await expression(async () => {
switch (readyTimeout) {
case 0: {
return true;
}
case Infinity: {
this.waitSet.set(criticalSection, resolve);
await readyPromise;
return true;
}
default: {
this.waitSet.set(criticalSection, resolve);
return await Promise.race([
readyPromise.then(() => true),
delay(readyTimeout).then(() => {
this.waitSet.delete(criticalSection);
return false;
}),
]);
}
}
}).catch((e) => {
this.waitSet.delete(criticalSection);
throw e;
});
criticalSection.removeEventListener("abort", abortListener);
return hasPulse;
}
waitSet = new Map();
}
/* eslint-disable @typescript-eslint/no-empty-function */
class Lock {
_isLocked;
constructor(_isLocked = true) {
this._isLocked = _isLocked;
if (_isLocked) {
this.block = new Promise((resolve, reject) => {
this.unlock = resolve;
this.interrupt = reject;
}).finally(() => {
this._isLocked = false;
});
}
else {
this.block = Promise.resolve();
this.unlock = () => { };
this.interrupt = () => { };
}
}
get isLocked() {
return this._isLocked;
}
block;
interrupt;
unlock;
}
class SingleChannel {
hasValue;
value;
constructor(hasValue, value) {
this.hasValue = hasValue;
this.value = value;
if (hasValue) {
this.lock = new Lock(false);
}
else {
this.value = null;
this.lock = new Lock();
}
}
async acquire(abortSignal = null) {
this.assertClosed();
if (0 < this.acquireCount) {
await nextTick();
}
let leave;
const interruptPromise = new Promise((resolve, reject) => {
if (abortSignal) {
abortSignal.throwIfAborted();
abortSignal.addEventListener("abort", () => {
reject(abortSignal.reason);
});
}
leave = resolve;
});
this.acquireCount++;
try {
while (!this.hasValue) {
await Promise.race([this.lock.block, interruptPromise]);
}
}
finally {
this.acquireCount--;
leave();
}
this.lock = new Lock();
return this.removeValue();
}
assertClosed() {
if (this.isClosed) {
throw new ClosedError();
}
}
close(reason) {
if (this.isClosed) {
return;
}
this.removeValue();
if (0 !== this.acquireCount) {
this.lock.interrupt(reason);
}
this._isClosed = true;
}
getValue() {
this.assertClosed();
if (this.hasValue) {
return [true, this.value];
}
else {
return [false];
}
}
removeValue() {
const value = this.value;
this.value = null;
this.hasValue = false;
return value;
}
setValue(value) {
this.assertClosed();
if (this.hasValue) {
throw new ExistValueError();
}
else {
this.value = value;
this.lock.unlock();
this.hasValue = true;
}
}
get isClosed() {
return this._isClosed;
}
_isClosed = false;
acquireCount = 0;
lock;
}
class SingleChannelError extends Error {
}
class ExistValueError extends SingleChannelError {
}
class ClosedError extends SingleChannelError {
}
class AsyncMonitorError extends Error {
}
class SynchronizationLockError extends AsyncMonitorError {
}
class DeprecateCriticalSectionError extends AsyncMonitorError {
}
class InterruptedError extends AsyncMonitorError {
}
class MismatchCriticalSectionError extends AsyncMonitorError {
}
class InnerCriticalSection {
parent;
root;
constructor(monitor, parent, root) {
this.parent = parent;
this.root = root;
this.monitorChannel = new SingleChannel(true, monitor);
}
async acquireMonitor(abortSignal) {
if (null === this.parent) {
return await this.monitorChannel.acquire(abortSignal);
}
else if (this.parent.isExited) {
return await Promise.race([
this.monitorChannel.acquire(abortSignal),
this.root.top.acquireMonitor(abortSignal),
]);
}
else {
return await Promise.race([
this.monitorChannel.acquire(abortSignal),
this.parent.acquireMonitor(abortSignal),
]);
}
}
assertDeprecateCriticalSection() {
if (this.isExited) {
throw new DeprecateCriticalSectionError();
}
}
assertMismatchCriticalSectionError() {
if (this !== this.root.current) {
throw new MismatchCriticalSectionError();
}
}
static create(monitor, root) {
return new InnerCriticalSection(monitor, null, root);
}
pulse() {
this.assertDeprecateCriticalSection();
this.assertMismatchCriticalSectionError();
const [ok, monitor] = this.monitorChannel.getValue();
if (ok) {
monitor.pulse();
}
else {
throw new SynchronizationLockError();
}
}
pulseAll() {
this.assertDeprecateCriticalSection();
this.assertMismatchCriticalSectionError();
const [ok, monitor] = this.monitorChannel.getValue();
if (ok) {
monitor.pulseAll();
}
else {
throw new SynchronizationLockError();
}
}
async reenter(abortSignal = null) {
this.assertDeprecateCriticalSection();
this.assertMismatchCriticalSectionError();
const abortController = new AbortController();
if (abortSignal) {
abortSignal.throwIfAborted();
abortSignal.addEventListener("abort", () => {
abortController.abort(abortSignal.reason);
});
}
return await this.root.tryEnter(async () => {
const monitor = await this.acquireMonitor(abortController.signal).finally(() => {
abortController.abort();
});
return new InnerCriticalSection(monitor, this, this.root);
});
}
releaseMonitor(monitor) {
if (this.parent.isExited) {
this.root.top.monitorChannel.setValue(monitor);
}
else {
this.parent.monitorChannel.setValue(monitor);
}
}
[Symbol.dispose]() {
if (this.isExited) {
return;
}
for (let cs = this.root.current; this !== cs; cs = cs.parent) {
if (null === cs || undefined === cs) {
throw new MismatchCriticalSectionError();
}
}
const [ok, monitor] = this.monitorChannel.getValue();
if (ok) {
this.monitorChannel.close();
this.releaseMonitor(monitor);
}
else {
this.monitorChannel.close(new InterruptedError());
}
this.root.exit(() => {
this.exitController.abort(new InterruptedError());
return this.parent;
});
}
async wait(readyTimeout = Infinity) {
this.assertDeprecateCriticalSection();
this.assertMismatchCriticalSectionError();
const [ok, monitor] = this.monitorChannel.getValue();
if (ok) {
void this.monitorChannel.acquire();
this.releaseMonitor(monitor);
const hasPulse = await monitor.wait(this.exitController.signal, readyTimeout);
const abortController = new AbortController();
{
const monitor = await this.acquireMonitor(abortController.signal).finally(() => {
abortController.abort();
});
this.monitorChannel.setValue(monitor);
}
return hasPulse;
}
else {
throw new SynchronizationLockError();
}
}
get hasMonitor() {
const [ok] = this.monitorChannel.getValue();
return ok;
}
get isExited() {
return this.exitController.signal.aborted;
}
exitController = new AbortController();
monitorChannel;
}
class CriticalSectionRoot {
key;
constructor(key) {
this.key = key;
this.top = InnerCriticalSection.create(new Monitor(), this);
}
static async enter(key, scope) {
return await this.tryEnter(key, Infinity, scope);
}
exit(exit) {
const parent = exit();
this.localStorage.getStore().current = parent;
this.referenceCount--;
if (0 === this.referenceCount) {
CriticalSectionRoot.map.delete(this.key);
}
}
static async tryEnter(key, timeout, scope) {
const criticalSection = await this.tryEnterWithoutScope(key, timeout);
if (criticalSection) {
return await criticalSection.root.localStorage.run({ current: criticalSection }, async () => {
const env_1 = { stack: [], error: void 0, hasError: false };
try {
const _ = __addDisposableResource(env_1, criticalSection, false);
return await scope(criticalSection);
}
catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
}
finally {
__disposeResources(env_1);
}
});
}
else {
return await scope(null);
}
}
async tryEnter(enter) {
this.referenceCount++;
const criticalSection = await enter().catch((e) => {
this.referenceCount--;
throw e;
});
// this.localStorage.enterWith({ current: criticalSection });
return criticalSection;
}
static async tryEnterWithoutScope(key, timeout) {
const criticalSectionRoot = this.map.get(key);
if (criticalSectionRoot) {
const parent = criticalSectionRoot.current;
switch (timeout) {
case 0: {
if (parent.hasMonitor) {
return await parent.reenter();
}
else {
return null;
}
}
case Infinity: {
return await parent.reenter();
}
default: {
try {
return await parent.reenter(AbortSignal.timeout(timeout));
}
catch (e) {
if (e instanceof DOMException && "TimeoutError" === e.name) {
return null;
}
else {
throw e;
}
}
}
}
}
else {
const criticalSectionRoot = new CriticalSectionRoot(key);
this.map.set(key, criticalSectionRoot);
return await criticalSectionRoot.top.reenter();
}
}
static tryGet(key) {
const criticalSectionRoot = this.map.get(key);
if (criticalSectionRoot) {
const criticalSection = criticalSectionRoot.current;
if (criticalSection.hasMonitor) {
return criticalSection;
}
else {
return null;
}
}
else {
return null;
}
}
get current() {
return this.localStorage.getStore()?.current ?? this.top;
}
localStorage = new async_hooks.AsyncLocalStorage();
static map = new Map();
referenceCount = 0;
top;
}
/**
* Acquires an exclusive lock on the specified key.
*/
async function enter(key, scope) {
return await CriticalSectionRoot.enter(key, scope);
}
/**
* Attempts, for the specified amount of time, to acquire an exclusive lock on the specified key.
*/
async function tryEnter(key, timeout, scope) {
return await CriticalSectionRoot.tryEnter(key, timeout, scope);
}
/**
* Attempts to get the current CriticalSection
*/
function tryGet(key) {
return CriticalSectionRoot.tryGet(key);
}
exports.enter = enter;
exports.tryEnter = tryEnter;
exports.tryGet = tryGet;