Share defaultMaxListeners with the typed EventEmitter, link instanceof alone, and clean up a pipe once

- EventEmitter.defaultMaxListeners on the untyped constructor is Node's
  accessor over the default the typed EventEmitter reads: assignments
  validate as validateNumber(arg, 'defaultMaxListeners', 0) does, and
  getMaxListeners() of emitters without their own limit answers the new
  value.
- dyn.builtinInstanceOf and dyn.functionInstanceOf link the
  constructor-function unit and the units it calls (dyn-invoke, inspect,
  symbol, emitter), so a program whose only use is `this instanceof F`
  links.
- The legacy pipe's cleanup runs once: when the destination emits
  'close' from end(), the cleanup still queued on the source's 'end'
  finds the listeners removed and emit('end') answers true.
This commit is contained in:
Chris Tate
2026-10-20 11:28:28 -05:00
parent 25780d9231
commit 447f56e5c3
10 changed files with 150 additions and 11 deletions
@@ -236,7 +236,12 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
"scr_util_callable.c",
),
unit(
(u) => u.opts.dynBuiltinClass && u.opts.inspect && u.opts.dynInvoke && u.opts.symbol,
(u) =>
u.opts.dynBuiltinClass &&
u.opts.inspect &&
u.opts.dynInvoke &&
u.opts.symbol &&
(u.opts.emitter || u.opts.workers),
"scr_dyn_builtin_class.c",
),
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
+16 -5
View File
@@ -234,10 +234,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** libCall name prefixes that link the child-process unit. */
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
/** The entry points scr_dyn_builtin_class.c defines. The unit calls
* listeners through the dyn-invoke unit, renders unhandled 'error'
* payloads through the inspect unit, creates EventEmitter's private
* symbols through the symbol unit and shares defaultMaxListeners with the
* emitter unit, so each of them links all five. */
const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
"dyn.builtinClass",
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
];
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// EventEmitter's kShapeMode and kCapture properties
"dyn.builtinClass",
...BUILTIN_CLASS_LIB_FNS,
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
@@ -511,11 +522,11 @@ const RUNTIME_FEATURE_RULES = {
* unit, render unhandled 'error' payloads through the inspect unit and
* create EventEmitter's private symbols through the symbol unit. */
dynBuiltinClass: rule({
fns: ["dyn.builtinClass", "dyn.builtinInstanceOf", "dyn.functionInstanceOf"],
fns: [...BUILTIN_CLASS_LIB_FNS],
}),
dynInvoke: rule({
initial: (mod) => mod.workers === true,
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, "dyn.builtinClass"],
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
// Constant-key reads of Object.prototype member names answer the
// inherited member through scr_dyn_invoke.c.
expr: (node, _tables) =>
@@ -532,7 +543,7 @@ const RUNTIME_FEATURE_RULES = {
emitter: rule({
initial: (mod) =>
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
fns: [...STREAM_LIB_FNS],
fns: [...STREAM_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
}),
/** Subclasses of the runtime EventTarget/Event bases tear down and box
@@ -567,7 +578,7 @@ const RUNTIME_FEATURE_RULES = {
"global.native",
"util.styleText",
"crypto.cipherNew",
"dyn.builtinClass",
...BUILTIN_CLASS_LIB_FNS,
...UTIL_CALLABLE_LIB_FNS,
],
prefixes: ["insp.", "console.", "test."],
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5817-constructor-function-instanceof.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5818-constructor-function-emitter-defaults.cjs"
],
"diags": []
}
@@ -91,7 +91,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
// Untyped EventEmitter and stream constructor values call listeners
// through the dyn-invoke unit, inspect unhandled 'error' payloads and
// create EventEmitter's private symbols.
["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol")],
["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol", "emitter")],
["scr_stream.c", "stream"],
// The zlib stream classes reference both the zlib engine and the stream unit.
["scr_zlib_stream.c", all("zlib", "stream")],
+45 -4
View File
@@ -263,9 +263,7 @@ static double bc_max_listeners(ScrDyn *self) {
if (!max) return -1;
double value;
if (bc_undefined(max)) {
ScrDyn *fallback = bc_get(bc_class(BC_EMITTER), "defaultMaxListeners");
value = fallback ? bc_number(fallback) : 10;
if (fallback) scr_dyn_release(fallback);
value = scr_emitter_get_default_max();
} else {
value = bc_number(max);
}
@@ -1017,6 +1015,42 @@ static ScrDyn *bc_set_max(ScrClosure *closure, ScrDyn *const *args, size_t argc)
return self;
}
/* EventEmitter.defaultMaxListeners, Node's accessor over the one default
* the typed EventEmitter reads too (scr_events_emitter.c). The setter
* validates as Node's validateNumber(arg, 'defaultMaxListeners', 0). */
static ScrDyn *bc_default_max_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
(void)args;
(void)argc;
return scr_dyn_new_num(scr_emitter_get_default_max());
}
static ScrDyn *bc_default_max_set(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
static const char name[] = "defaultMaxListeners";
ScrStr *key = scr_str_new(name, sizeof name - 1);
scr_emitter_set_default_max_chk(argc ? args[0] : scr_dyn_undefined(), key);
scr_str_release(key);
if (scr_exc_pending()) return NULL;
return scr_dyn_retain(scr_dyn_undefined());
}
static void bc_define_default_max(ScrDyn *ctor) {
ScrDyn *descriptor = scr_dyn_new_obj();
scr_dyn_obj_set(descriptor, "get", 3,
scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_get, 0, "native:events", "get"));
scr_dyn_obj_set(descriptor, "set", 3,
scr_dyn_new_func(scr_closure_new(NULL, 0), bc_default_max_set, 1, "native:events", "set"));
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true));
ScrStr *name = scr_str_new("defaultMaxListeners", 19);
ScrDyn *key = scr_dyn_new_str(name);
scr_str_release(name);
ScrDyn *defined = scr_dyn_define_property(ctor, key, descriptor);
if (defined) scr_dyn_release(defined);
scr_dyn_release(key);
scr_dyn_release(descriptor);
}
static ScrDyn *bc_get_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
(void)args;
@@ -1070,6 +1104,13 @@ static bool bc_pipe_on(ScrDyn *emitter, const char *event, ScrClosure *state, in
}
static bool bc_pipe_cleanup(ScrClosure *state) {
/* A second run (dest emitting 'close' from end(), with cleanup still
* queued on the source's 'end') finds the listeners removed, as Node's
* repeated removeListener calls do. */
ScrDyn *handlers = bc_cap(state, BC_PIPE_HANDLERS);
bool cleaned = handlers->kind == SCR_DYN_ARR && scr_dyn_arr_len(handlers) == 0;
scr_dyn_release(handlers);
if (cleaned) return true;
ScrDyn *source = bc_cap(state, BC_PIPE_SOURCE);
ScrDyn *dest = bc_cap(state, BC_PIPE_DEST);
bool ok = bc_pipe_remove(source, "data", state, BC_ONDATA) &&
@@ -1367,7 +1408,7 @@ static ScrDyn *bc_new_class(int kind, ScrDyn *parent) {
bc_method(prototype, "listenerCount", bc_listener_count, 2);
bc_method(prototype, "eventNames", bc_event_names, 0);
bc_set(ctor, "EventEmitter", scr_dyn_retain(ctor));
bc_set(ctor, "defaultMaxListeners", scr_dyn_new_num(10));
bc_define_default_max(ctor);
ScrDyn *count = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_static_listener_count, 2, "native:events", "listenerCount");
bc_set(ctor, "listenerCount", count);
} else if (kind == BC_STREAM) {
@@ -0,0 +1,15 @@
// `this instanceof F` in a constructor function, in a program that uses no
// EventEmitter or stream constructor value: the call-without-new idiom
// returns a new instance, and instanceof follows the function's prototype.
"use strict";
function Glob(pattern) {
if (!(this instanceof Glob)) return new Glob(pattern);
this.pattern = pattern;
}
Glob.prototype.describe = function () {
return `glob ${this.pattern}`;
};
const called = Glob("*.ts");
const constructed = new Glob("*.js");
console.log(called.describe(), constructed.describe());
console.log(called instanceof Glob, {} instanceof Glob, Object.create(Glob.prototype) instanceof Glob);
@@ -0,0 +1,53 @@
// EventEmitter.defaultMaxListeners read and written through the untyped
// EventEmitter constructor (Node's accessor and its validation), and a
// legacy pipe whose destination emits 'close' synchronously from end().
"use strict";
const util = require("util");
const EventEmitter = require("events");
const Stream = require("stream");
function Bus() {
EventEmitter.call(this);
}
util.inherits(Bus, EventEmitter);
console.log(EventEmitter.defaultMaxListeners);
console.log(new Bus().getMaxListeners());
EventEmitter.defaultMaxListeners = 3;
console.log(EventEmitter.defaultMaxListeners, new Bus().getMaxListeners());
for (const value of [-1, "4", Number.NaN]) {
try {
EventEmitter.defaultMaxListeners = value;
} catch (error) {
console.log(error.name, error.code, error.message);
}
}
console.log(EventEmitter.defaultMaxListeners);
EventEmitter.defaultMaxListeners = 10;
function Source() {
Stream.call(this);
this.readable = true;
}
util.inherits(Source, Stream);
function Sink() {
Stream.call(this);
this.writable = true;
}
util.inherits(Sink, Stream);
Sink.prototype.write = function (chunk) {
console.log("write", chunk);
return true;
};
Sink.prototype.end = function () {
console.log("sink end");
this.emit("close");
};
const source = new Source();
const sink = new Sink();
source.pipe(sink);
source.emit("data", "a");
console.log("emit end", source.emit("end"));
console.log("listeners", source.listenerCount("end"), source.listenerCount("data"), sink.listenerCount("close"));
@@ -0,0 +1 @@
{ "plain": 2655, "san": 7944 }
@@ -0,0 +1 @@
{ "plain": 2746, "san": 51663 }