deno/ext/node/polyfills/domain.ts

255 lines
6.2 KiB
TypeScript

// Copyright 2018-2025 the Deno authors. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// This code has been inspired by https://github.com/bevry/domain-browser/commit/8bce7f4a093966ca850da75b024239ad5d0b33c6
// deno-lint-ignore-file no-process-global
import { primordials } from "ext:core/mod.js";
import { ERR_UNHANDLED_ERROR } from "ext:deno_node/internal/errors.ts";
const {
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
FunctionPrototypeCall,
FunctionPrototypeApply,
ObjectDefineProperty,
ObjectPrototypeIsPrototypeOf,
ReflectApply,
} = primordials;
import { EventEmitter } from "node:events";
function emitError(e) {
this.emit("error", e);
}
let stack = [];
export let _stack = stack;
export let active = null;
export function create() {
return new Domain();
}
export function createDomain() {
return new Domain();
}
export class Domain extends EventEmitter {
members = [] as EventEmitter[];
constructor() {
super();
patchEventEmitter();
}
add(ee) {
if (ee.domain === this) {
return;
}
if (ee.domain) {
ee.domain.remove(ee);
}
if (this.domain && (ObjectPrototypeIsPrototypeOf(Domain.prototype, ee))) {
for (let d = this.domain; d; d = d.domain) {
if (ee === d) return;
}
}
ObjectDefineProperty(ee, "domain", {
__proto__: null,
configurable: true,
enumerable: false,
value: this,
writable: true,
});
ArrayPrototypePush(this.members, ee);
}
remove(ee) {
ee.domain = null;
const index = ArrayPrototypeIndexOf(this.members, ee);
if (index !== -1) {
ArrayPrototypeSplice(this.members, index, 1);
}
}
bind(fn) {
// deno-lint-ignore no-this-alias
const self = this;
return function () {
try {
return FunctionPrototypeApply(fn, null, ArrayPrototypeSlice(arguments));
} catch (e) {
FunctionPrototypeCall(emitError, self, e);
}
};
}
intercept(fn) {
// deno-lint-ignore no-this-alias
const self = this;
return function (e) {
if (e) {
FunctionPrototypeCall(emitError, self, e);
} else {
try {
return FunctionPrototypeApply(
fn,
null,
ArrayPrototypeSlice(arguments, 1),
);
} catch (e) {
FunctionPrototypeCall(emitError, self, e);
}
}
};
}
run(fn) {
try {
return fn();
} catch (e) {
FunctionPrototypeCall(emitError, this, e);
}
return this;
}
dispose() {
this.removeAllListeners();
return this;
}
enter() {
return this;
}
exit() {
return this;
}
}
function updateExceptionCapture() {
// TODO(kt3k): implement this
}
let patched = false;
/** Patches EventEmitter method to make it domain-aware.
* This happens at top-level of domain module in Node. That works because
* Node uses cjs for internal modules. We do this patching at constructor
* of Domain class to best approximate that behavior. */
function patchEventEmitter() {
if (patched) return;
patched = true;
EventEmitter.usingDomains = true;
const eventInit = EventEmitter.init;
EventEmitter.init = function (opts) {
ObjectDefineProperty(this, "domain", {
__proto__: null,
configurable: true,
enumerable: false,
value: null,
writable: true,
});
if (active && !ObjectPrototypeIsPrototypeOf(Domain.prototype, this)) {
this.domain = active;
}
return FunctionPrototypeCall(eventInit, this, opts);
};
const eventEmit = EventEmitter.prototype.emit;
EventEmitter.prototype.emit = function emit(...args) {
const domain = this.domain;
const type = args[0];
const shouldEmitError = type === "error" &&
this.listenerCount(type) > 0;
// Just call original `emit` if current EE instance has `error`
// handler, there's no active domain or this is process
if (
shouldEmitError || domain === null || domain === undefined ||
this === process
) {
return ReflectApply(eventEmit, this, args);
}
if (type === "error") {
const er = args.length > 1 && args[1]
? args[1]
: new ERR_UNHANDLED_ERROR();
if (typeof er === "object") {
er.domainEmitter = this;
ObjectDefineProperty(er, "domain", {
__proto__: null,
configurable: true,
enumerable: false,
value: domain,
writable: true,
});
er.domainThrown = false;
}
// Remove the current domain (and its duplicates) from the domains stack and
// set the active domain to its parent (if any) so that the domain's error
// handler doesn't run in its own context. This prevents any event emitter
// created or any exception thrown in that error handler from recursively
// executing that error handler.
const origDomainsStack = ArrayPrototypeSlice(stack);
const origActiveDomain = process.domain;
// Travel the domains stack from top to bottom to find the first domain
// instance that is not a duplicate of the current active domain.
let idx = stack.length - 1;
while (idx > -1 && process.domain === stack[idx]) {
--idx;
}
// Change the stack to not contain the current active domain, and only the
// domains above it on the stack.
if (idx < 0) {
stack.length = 0;
} else {
ArrayPrototypeSplice(stack, idx + 1);
}
// Change the current active domain
if (stack.length > 0) {
active = process.domain = stack[stack.length - 1];
} else {
active = process.domain = null;
}
updateExceptionCapture();
domain.emit("error", er);
// Now that the domain's error handler has completed, restore the domains
// stack and the active domain to their original values.
_stack = stack = origDomainsStack;
active = process.domain = origActiveDomain;
updateExceptionCapture();
return false;
}
domain.enter();
const ret = ReflectApply(eventEmit, this, args);
domain.exit();
return ret;
};
}
export default {
_stack,
create,
active,
createDomain,
Domain,
};