Summary
An indirect subclass of a native base inherits nothing. The base's method surface is only installed when the class literally names the base in its extends clause, so one level of indirection loses it entirely.
Confirmed on clean origin/main.
Repro
import { EventEmitter } from "node:events";
class B extends EventEmitter {}
class D extends B {} // one hop away from the native base
const d = new D();
console.log("typeof d.on:", typeof d.on, "typeof d.emit:", typeof d.emit);
|
output |
| node 26 |
typeof d.on: function typeof d.emit: function |
perry main |
typeof d.on: undefined typeof d.emit: undefined |
new D() is a usable EventEmitter in Node; in perry it has no emitter surface at all. No override is involved — plain two-level inheritance.
Root cause
js_event_emitter_subclass_init only fires when extends_name is literally EventEmitter. A class whose parent is itself an EventEmitter subclass never runs the init, so the method surface is never installed.
Fix direction
This is the EventEmitter mirror of the class-chain walk that PR #6311 added for EventTarget (which reserved CLASS_ID_EVENT_TARGET and wired the registry parent edge so inheritance resolves at any depth). The init needs to trigger on "the class chain reaches EventEmitter", not on a literal extends name match.
Surfaced while fixing #6316 (PR #6322), which deliberately documents this exclusion in its fixture rather than silently passing it. Two-level subclassing of EventEmitter is a very common Node pattern.
Summary
An indirect subclass of a native base inherits nothing. The base's method surface is only installed when the class literally names the base in its
extendsclause, so one level of indirection loses it entirely.Confirmed on clean
origin/main.Repro
typeof d.on: function typeof d.emit: functionmaintypeof d.on: undefined typeof d.emit: undefinednew D()is a usable EventEmitter in Node; in perry it has no emitter surface at all. No override is involved — plain two-level inheritance.Root cause
js_event_emitter_subclass_initonly fires whenextends_nameis literallyEventEmitter. A class whose parent is itself an EventEmitter subclass never runs the init, so the method surface is never installed.Fix direction
This is the
EventEmittermirror of the class-chain walk that PR #6311 added forEventTarget(which reservedCLASS_ID_EVENT_TARGETand wired the registry parent edge so inheritance resolves at any depth). The init needs to trigger on "the class chain reaches EventEmitter", not on a literalextendsname match.Surfaced while fixing #6316 (PR #6322), which deliberately documents this exclusion in its fixture rather than silently passing it. Two-level subclassing of
EventEmitteris a very common Node pattern.