Repository: denoland/deno
Pull request: fix(ext/node): update interaction between event emitters and domains

This PR updates the interaction between event emitters and domains. This enables `parallel/test-event-emitter-no-error-provided-to-error-event.js` test case.

towards #29595 

Changed files:
filename: ext/node/polyfills/domain.ts
patch: @@ -1,25 +1,30 @@
 // 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,
-  FunctionPrototypeBind,
+  ArrayPrototypeSplice,
   FunctionPrototypeCall,
   FunctionPrototypeApply,
+  ObjectDefineProperty,
+  ObjectPrototypeIsPrototypeOf,
+  ReflectApply,
 } = primordials;
 import { EventEmitter } from "node:events";
 
 function emitError(e) {
   this.emit("error", e);
 }
 
-// TODO(bartlomieju): maybe use this one
-// deno-lint-ignore prefer-const
 let stack = [];
-export const _stack = stack;
-export const active = null;
+export let _stack = stack;
+export let active = null;
 
 export function create() {
   return new Domain();
@@ -30,19 +35,44 @@ export function createDomain() {
 }
 
 export class Domain extends EventEmitter {
-  #handler;
+  members = [] as EventEmitter[];
 
   constructor() {
     super();
-    this.#handler = FunctionPrototypeBind(emitError, this);
+    patchEventEmitter();
   }
 
-  add(emitter) {
-    emitter.on("error", this.#handler);
+  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(emitter) {
-    emitter.off("error", this.#handler);
+  remove(ee) {
+    ee.domain = null;
+    const index = ArrayPrototypeIndexOf(this.members, ee);
+    if (index !== -1) {
+      ArrayPrototypeSplice(this.members, index, 1);
+    }
   }
 
   bind(fn) {
@@ -99,6 +129,123 @@ export class Domain extends EventEmitter {
     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,

filename: tests/node_compat/config.jsonc
patch: @@ -72,7 +72,6 @@
       "test-dsa-fips-invalid-key.js",
       "test-error-aggregateTwoErrors.js",
       "test-error-prepare-stack-trace.js",
-      "test-event-emitter-no-error-provided-to-error-event.js",
       "test-event-emitter-prepend.js",
       "test-events-on-async-iterator.js",
       "test-events-once.js",

filename: tests/node_compat/test/parallel/test-event-emitter-no-error-provided-to-error-event.js
patch: @@ -2,8 +2,8 @@
 // deno-lint-ignore-file
 
 // Copyright Joyent and Node contributors. All rights reserved. MIT license.
-// Taken from Node 16.13.0
-// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually
+// Taken from Node 23.9.0
+// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
 
 // Copyright Joyent, Inc. and other Node contributors.
 //
@@ -30,7 +30,6 @@
 const common = require('../common');
 const assert = require('assert');
 const events = require('events');
-/* TODO(uki00a): Uncomment this block when the 'domain' module is implemented.
 const domain = require('domain');
 
 {
@@ -62,4 +61,3 @@ for (const arg of [42, 'fortytwo', true]) {
   }));
   e.emit('error', arg);
 }
-*/