diff --git a/changelog.d/11809-eventemitter-shared-prototype.md b/changelog.d/11809-eventemitter-shared-prototype.md new file mode 100644 index 0000000000..5ec101ea3b --- /dev/null +++ b/changelog.d/11809-eventemitter-shared-prototype.md @@ -0,0 +1,34 @@ +**perf/parity: EventEmitter methods live on one shared prototype (Refs #10508)** + +`class X extends EventEmitter`, `util.inherits` + `EventEmitter.call(this)`, +`Object.create(EventEmitter.prototype)` and `setPrototypeOf` mixins no longer +get the 15 emitter methods stamped onto every instance as own closures. As in +node, `super()` / `EventEmitter.call(this)` run `EventEmitter.init`: the +instance owns `_events` (a null-prototype object), `_eventsCount` and +`_maxListeners`, and the methods are inherited from `EventEmitter.prototype`, +which carries node's defaults (`_events: undefined`, `_eventsCount: 0`, +`_maxListeners: undefined`) and its aliases (`addListener === on`, +`off === removeListener`). + +- `Object.keys(new Sub())` is `["_events","_eventsCount","_maxListeners"]`, + `hasOwnProperty('on')` is false and `sub.on === EventEmitter.prototype.on`, + matching node. +- The listener store is node's: `_events[type]` holds one function or an + array; a `once` listener is a wrapper with `.listener`; `rawListeners`, + `listeners`, `eventNames`, `newListener` / `removeListener` and + `removeAllListeners` follow `lib/events.js`, so code that reads or edits + `_events` directly sees the same state. +- A subclass override still wins, and `super.emit()` / `super.on()` reach the + base through the prototype chain (the #6316 behavior, without the + per-instance stash). +- `super({ captureRejections: true })` now works for subclasses: an async + listener's rejection reaches `[Symbol.for('nodejs.rejection')]` or + `emit('error')`; a non-boolean value throws `ERR_INVALID_ARG_TYPE`. + +Instructions per operation (t508, `class Q extends EventEmitter` with 12 +fields): `new` 151.3k -> 77.1k (RSS 108 MB -> 47 MB), `emit` 51.1k -> 34.2k, +`this.m()` 190 -> 190. Commander `parse_argv` 13.39G -> 10.43G instructions +(-22.1%); tsc and Zod unchanged. + +Plain `new EventEmitter()` is still a handle (its own keys are still `[]`). +That is the next step. diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index 37f5925292..c7cc7a3fd6 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -915,28 +915,35 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); } // #5137: `class X extends EventEmitter` (node:events) — - // `super()` installs the bare EventEmitter listener/emit - // surface onto `this` (see `lower_event_emitter_subclass_init`). - // `super(opts)` takes an optional options bag in Node; we lower - // the args for side effects but the bare emitter seeds no state. + // `super(opts)` runs node's `EventEmitter.init(opts)` on + // `this` (see `lower_event_emitter_subclass_init`); the + // methods are inherited from the shared + // `EventEmitter.prototype`. Every argument is lowered (in + // order, rooted across the later ones); the first is the + // options bag (`captureRejections`). if parent_name.as_str() == "EventEmitter" { - for a in super_args { - let _ = lower_expr(ctx, a)?; - } - let this_box = match ctx.this_stack.last().cloned() { - Some(slot) => ctx.block().load(DOUBLE, &slot), - None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), - }; - lower_event_emitter_subclass_init(ctx, &this_box); - bind_derived_this_after_super(ctx); + let operands: Vec<_> = super_args.iter().collect(); let current_class_name = ctx.class_stack.last().cloned().unwrap_or_default(); - crate::lower_call::apply_field_initializers_recursive( - ctx, - ¤t_class_name, - crate::lower_call::FieldInitMode::SelfOnly, - )?; - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + return rooting::with_operands_rooted(ctx, &operands, |ctx, lowered| { + let options = lowered.first().cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + let this_box = match ctx.this_stack.last().cloned() { + Some(slot) => ctx.block().load(DOUBLE, &slot), + None => { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + } + }; + lower_event_emitter_subclass_init(ctx, &this_box, &options); + bind_derived_this_after_super(ctx); + crate::lower_call::apply_field_initializers_recursive( + ctx, + ¤t_class_name, + crate::lower_call::FieldInitMode::SelfOnly, + )?; + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + }); } if parent_name.as_str() == "EventEmitterAsyncResource" { let operands: Vec<_> = super_args.iter().collect(); diff --git a/crates/perry-codegen/src/expr/write_barrier.rs b/crates/perry-codegen/src/expr/write_barrier.rs index 3dc77d5654..28982a3ba7 100644 --- a/crates/perry-codegen/src/expr/write_barrier.rs +++ b/crates/perry-codegen/src/expr/write_barrier.rs @@ -1135,21 +1135,25 @@ pub(crate) fn lower_array_super_init(ctx: &mut FnCtx<'_>, super_args: &[Expr]) - Ok(undef_lit) } -/// #5137: install the bare EventEmitter listener/emit surface onto `this_box` -/// for a source-compiled `class X extends EventEmitter` (node:events). Shared -/// by the explicit-`super()` arm (`expr/this_super_call.rs`) and the +/// #5137: node's `EventEmitter.init` on `this_box` for a source-compiled +/// `class X extends EventEmitter` (node:events). Shared by the +/// explicit-`super()` arm (`expr/this_super_call.rs`) and the /// no-own-constructor `new` path (`lower_call/new.rs`). The runtime helper -/// reuses the generic `ns_*` emitter closures (they key all state off the -/// receiver), so a plain object that never went through a stream constructor -/// gets working `.on`/`.emit`/`.once`/…. Reached when an EventEmitter +/// gives `this` its own `_events`/`_eventsCount`/`_maxListeners`; the +/// `.on`/`.emit`/`.once`/… methods are inherited from the shared +/// `EventEmitter.prototype`, as in node. Reached when an EventEmitter /// subclass's real npm source is compiled — e.g. commander's `Command` under /// `perry.compilePackages`, where the `new Command()` → `js_commander_*` /// native-shim path is intentionally off. -pub(crate) fn lower_event_emitter_subclass_init(ctx: &mut FnCtx<'_>, this_box: &str) { +pub(crate) fn lower_event_emitter_subclass_init( + ctx: &mut FnCtx<'_>, + this_box: &str, + options_box: &str, +) { ctx.block().call( DOUBLE, "js_event_emitter_subclass_init", - &[(DOUBLE, this_box)], + &[(DOUBLE, this_box), (DOUBLE, options_box)], ); } diff --git a/crates/perry-codegen/src/lower_call/new_helpers.rs b/crates/perry-codegen/src/lower_call/new_helpers.rs index 25aef536ca..bb812b6c80 100644 --- a/crates/perry-codegen/src/lower_call/new_helpers.rs +++ b/crates/perry-codegen/src/lower_call/new_helpers.rs @@ -227,9 +227,13 @@ pub(crate) fn emit_native_instance_base_init( let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); match base { NativeInstanceBase::EventEmitter => { - // The bare emitter seeds no state from its options bag, so the args - // (already lowered for their side effects) are not forwarded. - crate::expr::lower_event_emitter_subclass_init(ctx, this_box); + // node's `EventEmitter.init(opts)`: the first argument is the + // options bag (`captureRejections`). + let options = lowered_args + .first() + .cloned() + .unwrap_or_else(|| undef.clone()); + crate::expr::lower_event_emitter_subclass_init(ctx, this_box, &options); } NativeInstanceBase::Array => { let n = lowered_args.len(); diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs index cad9c94814..5c18f75391 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs @@ -7,7 +7,7 @@ use crate::types::{DOUBLE, I32, I64, PTR}; pub(crate) fn declare_streams_events(module: &mut LlModule) { // ========== node:stream stubs (issue #631) ========== - module.declare_function("js_event_emitter_subclass_init", DOUBLE, &[DOUBLE]); // #5137 EE subclass init + module.declare_function("js_event_emitter_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // #5137 EE subclass init (this, options) module.declare_function( "js_event_emitter_async_resource_subclass_init", DOUBLE, diff --git a/crates/perry-codegen/src/wasm32/runtime_abi.tsv b/crates/perry-codegen/src/wasm32/runtime_abi.tsv index 026d1f972e..9ae07690e7 100644 --- a/crates/perry-codegen/src/wasm32/runtime_abi.tsv +++ b/crates/perry-codegen/src/wasm32/runtime_abi.tsv @@ -1148,7 +1148,7 @@ js_event_emitter_remove_all_listeners i64 i64,ptr js_event_emitter_remove_listener i64 i64,i64,i64 js_event_emitter_set_domain i32s i64,i64 js_event_emitter_set_max_listeners i64 i64,f64 -js_event_emitter_subclass_init f64 f64 +js_event_emitter_subclass_init f64 f64,f64 js_event_loop_check_phase i32s js_event_loop_host_driven i32s js_event_loop_poll_callbacks i32s diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index 959dce5d6a..ae88e46da5 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -119,9 +119,9 @@ unsafe fn native_call_value_this_impl( // the direct `class X extends EventEmitter` form (#5137) — never fires, and // `js_register_class_parent_dynamic` early-returns for bound native parents. // The dynamic super lowering (expr/this_super_call.rs) dispatches the parent - // VALUE here with `this` = the fresh subclass instance. Install - // the EventEmitter listener/emit methods onto that instance, exactly as the - // direct form does, so `this.setMaxListeners(…)`/`.on`/`.emit` resolve. + // VALUE here with `this` = the fresh subclass instance. Run node's + // `EventEmitter.init` on that instance, exactly as the direct form does; + // `this.setMaxListeners(…)`/`.on`/`.emit` resolve on the shared prototype. // Routed through the armed ops table (see `nm_namespace_hooks`): the // probe can only match a bound native callable, which exists only once // `callable_exports` minted one (arming the table). diff --git a/crates/perry-runtime/src/node_stream.rs b/crates/perry-runtime/src/node_stream.rs index 7e9ea2ab9a..0e7de0533d 100644 --- a/crates/perry-runtime/src/node_stream.rs +++ b/crates/perry-runtime/src/node_stream.rs @@ -62,6 +62,10 @@ pub(crate) fn has_stream_listeners(stream: f64, event: &[u8]) -> bool { // #3049 — `process.setMaxListeners` reuses the EventEmitter setter // validation (TypeError/RangeError + fractional/Infinity storage). pub(crate) use event_emitter::validate_max_listeners; +// node EventEmitter instance state (EventEmitter.init) and the prototype defaults. +pub(crate) use event_emitter::{ + init_event_emitter_capture, init_event_emitter_state, install_event_emitter_prototype_state, +}; pub use event_emitter::{ js_node_stream_method_event_names, js_node_stream_method_get_max_listeners, js_node_stream_method_listener_count, js_node_stream_method_listeners, @@ -101,8 +105,9 @@ const STREAM_READABLE_SCHEDULED_KEY: &[u8] = b"__perryStreamReadableScheduled"; const STREAM_END_SCHEDULED_KEY: &[u8] = b"__perryStreamEndScheduled"; const STREAM_END_EMITTED_KEY: &[u8] = b"__perryStreamEndEmitted"; const STREAM_ENDED_KEY: &[u8] = b"__perryStreamEnded"; -const STREAM_MAX_LISTENERS_KEY: &[u8] = b"__perryStreamMaxListeners"; -const STREAM_CAPTURE_REJECTIONS_KEY: &[u8] = b"__perryStreamCaptureRejections"; +/// An emitter's `captureRejections` flag (node's `this[kCapture]`). Internal: +/// hidden from own-key enumeration (`is_internal_runtime_key_bytes`). +pub(crate) const STREAM_CAPTURE_REJECTIONS_KEY: &[u8] = b"__perryStreamCaptureRejections"; const EVENT_EMITTER_ASYNC_RESOURCE_KEY: &[u8] = b"__perryEventEmitterAsyncResource"; const WRITABLE_WRITE_KEY: &[u8] = b"__perryWritableWrite"; const WRITABLE_FINISH_SCHEDULED_KEY: &[u8] = b"__perryWritableFinishScheduled"; diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index d70f51da13..71ee224f94 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -66,10 +66,10 @@ pub extern "C" fn js_node_stream_readable_subclass_init(this: f64, opts: f64) -> } /// #5137: `super()` for a source-compiled `class X extends EventEmitter` -/// (from `node:events`). Installs the bare EventEmitter listener/emit -/// methods directly onto `this` — the same generic `ns_*` closures the -/// stream subclasses use — so `.on`/`.emit`/`.once`/… resolve as the -/// instance's own bound methods. This is the EventEmitter analog of +/// (from `node:events`), and `EventEmitter.call(this)`. Gives `this` node's +/// instance state; `.on`/`.emit`/`.once`/… are inherited from the shared +/// `EventEmitter.prototype` (`install_event_emitter_prototype`), as in node. +/// This is the EventEmitter analog of /// `js_node_stream_readable_subclass_init`; commander's `Command extends /// EventEmitter` reaches it when its real npm source is compiled (the /// package is in `perry.compilePackages`, so the `new Command()` → native @@ -77,7 +77,7 @@ pub extern "C" fn js_node_stream_readable_subclass_init(this: f64, opts: f64) -> /// inits there is no option-driven state to seed — a plain EventEmitter /// has no `_read`/`highWaterMark`/etc. #[no_mangle] -pub extern "C" fn js_event_emitter_subclass_init(this: f64) -> f64 { +pub extern "C" fn js_event_emitter_subclass_init(this: f64, options: f64) -> f64 { let raw = raw_ptr_from_value(this); if raw == 0 { return this; @@ -85,10 +85,17 @@ pub extern "C" fn js_event_emitter_subclass_init(this: f64) -> f64 { if unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { return this; } - let obj = raw as *mut ObjectHeader; - let methods = emitter_methods(); - install_methods_on_existing_object(obj, this, &methods, &[]); - this + // node's `EventEmitter.init`: own `_events`/`_eventsCount`/`_maxListeners` + // only. The methods are inherited from the shared `EventEmitter.prototype` + // (a subclass override on its own prototype shadows them there, and + // `super.m()` finds the base through the chain), so nothing is installed + // per instance (#10508). + let scope = crate::gc::RuntimeHandleScope::new(); + let this = scope.root_nanbox_f64(this); + let options = scope.root_nanbox_f64(options); + init_event_emitter_state(this.get_nanbox_f64()); + init_event_emitter_capture(this.get_nanbox_f64(), options.get_nanbox_f64()); + this.get_nanbox_f64() } /// #10798: install the legacy `node:stream` `Stream` base surface onto @@ -132,7 +139,14 @@ pub extern "C" fn js_event_emitter_async_resource_subclass_init(this: f64, optio let scope = crate::gc::RuntimeHandleScope::new(); let this_handle = scope.root_nanbox_f64(this); let options_handle = scope.root_nanbox_f64(options); - js_event_emitter_subclass_init(this_handle.get_nanbox_f64()); + // A string `options` is the resource name; only an options object carries + // `captureRejections` for the EventEmitter half. + let emitter_options = if JSValue::from_bits(options.to_bits()).is_any_string() { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + options_handle.get_nanbox_f64() + }; + js_event_emitter_subclass_init(this_handle.get_nanbox_f64(), emitter_options); let this = this_handle.get_nanbox_f64(); let raw = raw_ptr_from_value(this); diff --git a/crates/perry-runtime/src/node_stream_dispatch.rs b/crates/perry-runtime/src/node_stream_dispatch.rs index 11875afef0..9379adbc67 100644 --- a/crates/perry-runtime/src/node_stream_dispatch.rs +++ b/crates/perry-runtime/src/node_stream_dispatch.rs @@ -72,8 +72,10 @@ pub(super) fn build_object(methods: &[(&str, StubFn)], shape_id: u32) -> *mut Ob /// subclass override. /// /// The native bases perry models by stamping their method surface onto the -/// instance (`EventEmitter`, every `node:stream` class) install those methods as -/// ORDINARY OWN PROPERTIES. Own properties legitimately shadow class methods, so +/// instance (every `node:stream` class; `EventEmitter`'s methods live on the +/// shared `EventEmitter.prototype` instead, see +/// `inherited_event_emitter_method`) install those methods as ORDINARY OWN +/// PROPERTIES. Own properties legitimately shadow class methods, so /// perry's own-property-override probe (issue #620, /// `perry-codegen/src/lower_call/method_override.rs`) selected the native /// closure in preference to the user's `class Bus extends EventEmitter { emit() @@ -124,11 +126,58 @@ pub(crate) fn displaced_native_base_method(this_value: f64, name: &str) -> Optio if JSValue::from_bits(val.to_bits()).is_pointer() { Some(val) } else { - None + inherited_event_emitter_method(this_value, name) } } } +/// `super.` for a class whose chain bottoms out in `EventEmitter`: its +/// methods are not stamped onto the instance but live on the shared +/// `EventEmitter.prototype`, so walk the receiver's prototype chain to the +/// first value of `name` that IS one of the native EventEmitter method bodies. +/// The user's own overrides on the class prototypes in between are skipped by +/// that identity test, so `super.emit` can never re-enter the override. +fn inherited_event_emitter_method(this_value: f64, name: &str) -> Option { + let infos: Vec = super::emitter_methods() + .iter() + .filter(|(method, _)| *method == name) + .map(|(_, info)| *info) + .collect(); + if infos.is_empty() { + return None; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let mut current = scope.root_nanbox_f64(crate::object::js_object_get_prototype_of(this_value)); + for _ in 0..64 { + let proto = current.get_nanbox_f64(); + if object_ptr_from_value(proto).is_none() { + return None; + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = unsafe { + crate::object::js_object_get_property_key( + current.get_nanbox_f64(), + f64::from_bits(JSValue::string_ptr(key).bits()), + ) + }; + let raw = raw_ptr_from_value(value); + if raw >= 0x10000 { + let func = + crate::closure::get_valid_func_ptr(raw as *const crate::closure::ClosureHeader); + if !func.is_null() { + let info = unsafe { (*(raw as *const crate::closure::ClosureHeader)).info }; + if infos.iter().any(|candidate| std::ptr::eq(*candidate, info)) { + return Some(value); + } + } + } + current = scope.root_nanbox_f64(crate::object::js_object_get_prototype_of( + current.get_nanbox_f64(), + )); + } + None +} + /// True when the receiver's class chain declares `name` as a real class method — /// i.e. the user OVERRODE this native base method (#6316). The class registry is /// populated at module init, long before any `new`, so the vtable is always @@ -249,6 +298,86 @@ pub(crate) fn install_event_emitter_prototype_methods(proto: *mut ObjectHeader) } } +/// node's `EventEmitter.prototype`: the instance-state defaults +/// (`_events`/`_eventsCount`/`_maxListeners`) then the methods, in the order +/// `lib/events.js` assigns them, with node's two aliases (`addListener` IS +/// `on`, `off` IS `removeListener`). Every closure reads its receiver from the +/// call-site `this` (slot 0 holds `TAG_UNDEFINED`), so ONE set of closures +/// serves every emitter: no instance carries its own copy (#10508). +pub(crate) fn install_event_emitter_prototype(proto: *mut ObjectHeader) { + let scope = crate::gc::RuntimeHandleScope::new(); + let proto = scope.root_raw_mut_ptr(proto); + let proto_value = proto.with_const_ptr::(|proto| { + f64::from_bits(JSValue::pointer(proto as *const u8).bits()) + }); + super::install_event_emitter_prototype_state(proto_value); + let methods = super::emitter_methods(); + let body = |name: &str| { + methods + .iter() + .find(|(method, _)| *method == name) + .map(|(_, info)| *info) + }; + let mut values: Vec<(&str, crate::gc::RuntimeHandle<'_>)> = Vec::new(); + for name in [ + "setMaxListeners", + "getMaxListeners", + "emit", + "on", + "prependListener", + "once", + "prependOnceListener", + "removeListener", + "removeAllListeners", + "listeners", + "rawListeners", + "listenerCount", + "eventNames", + ] { + let Some(info) = body(name) else { continue }; + let closure = js_closure_alloc(info, 1); + crate::closure::js_closure_set_capture_ptr(closure, 0, crate::value::TAG_UNDEFINED as i64); + let value = scope.root_nanbox_f64(f64::from_bits( + JSValue::pointer(closure as *const u8).bits(), + )); + values.push((name, value)); + } + let value_of = |name: &str| { + values + .iter() + .find(|(method, _)| *method == name) + .map(|(_, value)| value.get_nanbox_f64()) + }; + for (name, source) in [ + ("setMaxListeners", "setMaxListeners"), + ("getMaxListeners", "getMaxListeners"), + ("emit", "emit"), + ("addListener", "on"), + ("on", "on"), + ("prependListener", "prependListener"), + ("once", "once"), + ("prependOnceListener", "prependOnceListener"), + ("removeListener", "removeListener"), + ("off", "removeListener"), + ("removeAllListeners", "removeAllListeners"), + ("listeners", "listeners"), + ("rawListeners", "rawListeners"), + ("listenerCount", "listenerCount"), + ("eventNames", "eventNames"), + ] { + let Some(value) = value_of(source) else { + continue; + }; + let value = scope.root_nanbox_f64(value); + let key = scope.root_string_ptr(hidden_key(name.as_bytes())); + proto.with_mut_ptr::(|proto| { + key.with_const_ptr::(|key| { + js_object_set_field_by_name(proto, key, value.get_nanbox_f64()) + }) + }); + } +} + enum EventEmitterAsyncResourceBacking { ExternalEmitter(i64), RuntimeResource(i64), diff --git a/crates/perry-runtime/src/node_stream_event_emitter.rs b/crates/perry-runtime/src/node_stream_event_emitter.rs index 624cd8d136..8b5b0bdda3 100644 --- a/crates/perry-runtime/src/node_stream_event_emitter.rs +++ b/crates/perry-runtime/src/node_stream_event_emitter.rs @@ -3,10 +3,6 @@ use crate::closure::{ }; use crate::value::JSValue; -const STREAM_EVENT_NAMES_KEY: &[u8] = b"__perryStreamEventNames"; -const STREAM_LISTENERS_PREFIX: &[u8] = b"__perryStreamListeners:"; -const STREAM_ONCE_PREFIX: &[u8] = b"__perryStreamOnce:"; - pub(super) extern "C" fn ns_set_max_listeners( closure: *const ClosureHeader, this: crate::closure::JsThis, @@ -22,8 +18,10 @@ pub extern "C" fn js_node_stream_method_set_max_listeners(stream_handle: i64, va fn set_stream_max_listeners(stream: f64, value: f64) -> f64 { let value = validate_max_listeners(value); - super::set_hidden_value(stream, super::hidden_max_listeners_key(), value); - stream + let scope = crate::gc::RuntimeHandleScope::new(); + let stream = scope.root_nanbox_f64(stream); + set_named(stream.get_nanbox_f64(), MAX_LISTENERS_KEY, value); + stream.get_nanbox_f64() } fn format_max_listeners_received(n: f64) -> String { @@ -90,7 +88,13 @@ pub extern "C" fn js_node_stream_method_get_max_listeners(stream_handle: i64) -> } fn stream_max_listeners(stream: f64) -> f64 { - super::get_hidden_value(stream, super::hidden_max_listeners_key()).unwrap_or(10.0) + // node: `_maxListeners === undefined ? defaultMaxListeners : _maxListeners`. + let max = get_named(stream, MAX_LISTENERS_KEY); + if is_undefined(max) { + DEFAULT_MAX_LISTENERS + } else { + max + } } pub(super) extern "C" fn ns_on2( @@ -315,394 +319,643 @@ pub(super) fn add_stream_listener_for_event(stream: f64, event: f64, cb: f64) { add_stream_listener_for_event_with_options(stream, event, cb, false, false); } -fn add_stream_listener_for_event_with_options( - stream: f64, - event: f64, - cb: f64, - once: bool, - prepend: bool, -) { - if event_identity_bytes(event).is_none() { - return; - } - if !is_callable_value(cb) { - throw_invalid_listener_type(); - } - add_stream_listener(stream, event, cb, once, prepend); - if super::string_value_eq(event, b"data") { - super::readable_data_listener_added(stream); - } else if super::string_value_eq(event, b"readable") { - super::readable_listener_added(stream); - } +// ───────────────────────────────────────────────────────────────── +// Listener state is node's own (`lib/events.js`): `this._events` is a +// null-prototype object mapping each event key to ONE listener function or an +// array of them, `this._eventsCount` counts its keys, and a `once` listener is +// stored as a wrapper function whose `.listener` is the original. Every read +// and write goes through ordinary property access on the receiver, so code +// that inspects or edits `_events` directly (readable-stream's +// `prependListener`, ee-first, user code) sees and changes the same state the +// methods use, and the methods themselves live on one shared prototype +// rather than on every instance. +// ───────────────────────────────────────────────────────────────── + +const EVENTS_KEY: &[u8] = b"_events"; +const EVENTS_COUNT_KEY: &[u8] = b"_eventsCount"; +const MAX_LISTENERS_KEY: &[u8] = b"_maxListeners"; +const DEFAULT_MAX_LISTENERS: f64 = 10.0; + +fn undefined_value() -> f64 { + f64::from_bits(super::TAG_UNDEFINED) } -#[cold] -fn throw_invalid_listener_type() -> ! { - let msg = b"The \"listener\" argument must be of type function"; - let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); - crate::node_submodules::register_error_code(s, "ERR_INVALID_ARG_TYPE"); - let err = crate::error::js_typeerror_new(s); - let bits = JSValue::pointer(err as *const u8).bits(); - crate::exception::js_throw(f64::from_bits(bits)) +fn is_undefined(value: f64) -> bool { + value.to_bits() == super::TAG_UNDEFINED } -fn string_bytes(value: f64) -> Option> { - let jsval = JSValue::from_bits(value.to_bits()); - if !jsval.is_any_string() { - return None; - } - let ptr = crate::value::js_get_string_pointer_unified(value) as *const crate::StringHeader; - if ptr.is_null() || (ptr as usize) < 0x1000 { - return None; - } +/// `target[key]`: the full [[Get]] (prototype chain, accessors, proxies). +fn get_key(target: f64, key: f64) -> f64 { + unsafe { crate::object::js_object_get_property_key(target, key) } +} + +/// `target[key] = value`: the full [[Set]]. +fn set_key(target: f64, key: f64, value: f64) { unsafe { - let len = (*ptr).byte_len as usize; - let data = (ptr as *const u8).add(std::mem::size_of::()); - Some(std::slice::from_raw_parts(data, len).to_vec()) + crate::object::js_object_set_property_key(target, key, value); } } -fn event_identity_bytes(event: f64) -> Option> { - if unsafe { crate::symbol::js_is_symbol(event) } != 0 { - let mut out = b"sym:".to_vec(); - out.extend_from_slice(super::raw_ptr_from_value(event).to_string().as_bytes()); - return Some(out); - } - let mut out = b"str:".to_vec(); - out.extend_from_slice(&string_bytes(event)?); - Some(out) +/// A runtime-owned property name, interned: allocates only on the first use +/// per thread, then a hash probe returning the canonical (rooted) string. +fn name_key(name: &[u8]) -> f64 { + f64::from_bits(JSValue::string_ptr(crate::string::intern_ascii_literal(name) as *mut _).bits()) } -fn event_key(prefix: &[u8], event: f64) -> Option<*mut crate::string::StringHeader> { - let mut key = prefix.to_vec(); - key.extend_from_slice(&event_identity_bytes(event)?); - Some(super::hidden_key(&key)) +fn get_named(target: f64, name: &[u8]) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let key = name_key(name); + get_key(target.get_nanbox_f64(), key) } -fn hidden_event_names_key() -> *mut crate::string::StringHeader { - super::hidden_key(STREAM_EVENT_NAMES_KEY) +fn set_named(target: f64, name: &[u8], value: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let value = scope.root_nanbox_f64(value); + let key = name_key(name); + set_key(target.get_nanbox_f64(), key, value.get_nanbox_f64()); } -fn event_names_value(stream: f64) -> f64 { - super::get_hidden_value(stream, hidden_event_names_key()).unwrap_or_else(|| { - let arr = crate::array::js_array_alloc(0); - let value = super::box_pointer(arr as *const u8); - super::set_hidden_value(stream, hidden_event_names_key(), value); - value - }) +fn is_object_value(value: f64) -> bool { + super::object_ptr_from_value(value).is_some() } -fn event_names_snapshot(stream: f64) -> Vec { - let names = event_names_value(stream); - if !super::is_array_like_value(names) { - return Vec::new(); - } - let arr = super::raw_ptr_from_value(names) as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(arr); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - out.push(crate::array::js_array_get_f64(arr, i)); - } - out +fn is_array_value(value: f64) -> bool { + crate::array::js_array_is_array(value).to_bits() == super::TAG_TRUE } -fn event_name_index(stream: f64, event: f64) -> Option { - let wanted = event_identity_bytes(event)?; - let names = event_names_value(stream); - if !super::is_array_like_value(names) { - return None; - } - let arr = super::raw_ptr_from_value(names) as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(arr); - for i in 0..len { - let existing = crate::array::js_array_get_f64(arr, i); - if event_identity_bytes(existing).is_some_and(|bytes| bytes == wanted) { - return Some(i); - } +fn number_of(value: f64) -> f64 { + let js = JSValue::from_bits(value.to_bits()); + if js.is_int32() { + js.as_int32() as f64 + } else if js.is_number() { + value + } else { + f64::NAN } - None } -fn note_event_name(stream: f64, event: f64) { - if event_name_index(stream, event).is_some() { - return; - } - let names = event_names_value(stream); - if !super::is_array_like_value(names) { - return; - } - let arr = super::raw_ptr_from_value(names) as *mut crate::array::ArrayHeader; - let arr = crate::array::js_array_push_f64(arr, event); - super::set_hidden_value( - stream, - hidden_event_names_key(), - super::box_pointer(arr as *const u8), - ); +/// `{ __proto__: null }`, node's empty `_events`. +fn new_events_object() -> f64 { + crate::object::js_object_create(f64::from_bits(crate::value::TAG_NULL)) } -fn listener_storage(stream: f64, event: f64) -> Option<(f64, f64)> { - let listeners = super::get_hidden_value(stream, event_key(STREAM_LISTENERS_PREFIX, event)?)?; - let once = super::get_hidden_value(stream, event_key(STREAM_ONCE_PREFIX, event)?)?; - Some((listeners, once)) +fn reset_events(target: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let events = new_events_object(); + set_named(target.get_nanbox_f64(), EVENTS_KEY, events); + set_named(target.get_nanbox_f64(), EVENTS_COUNT_KEY, 0.0); } -fn ensure_listener_storage(stream: f64, event: f64) -> Option<(f64, f64)> { - let listener_key = event_key(STREAM_LISTENERS_PREFIX, event)?; - let once_key = event_key(STREAM_ONCE_PREFIX, event)?; - let listeners = super::get_hidden_value(stream, listener_key).unwrap_or_else(|| { - let arr = crate::array::js_array_alloc(0); - let value = super::box_pointer(arr as *const u8); - super::set_hidden_value(stream, listener_key, value); - value - }); - let once = super::get_hidden_value(stream, once_key).unwrap_or_else(|| { - let arr = crate::array::js_array_alloc(0); - let value = super::box_pointer(arr as *const u8); - super::set_hidden_value(stream, once_key, value); - value - }); - Some((listeners, once)) +fn adjust_events_count(target: f64, delta: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let count = number_of(get_named(target.get_nanbox_f64(), EVENTS_COUNT_KEY)) + delta; + set_named(target.get_nanbox_f64(), EVENTS_COUNT_KEY, count); + count } -fn set_listener_storage(stream: f64, event: f64, listeners: f64, once: f64) { - if let Some(listener_key) = event_key(STREAM_LISTENERS_PREFIX, event) { - super::set_hidden_value(stream, listener_key, listeners); - } - if let Some(once_key) = event_key(STREAM_ONCE_PREFIX, event) { - super::set_hidden_value(stream, once_key, once); - } +/// The receiver's `_events` when it is an object. +fn events_of(target: f64) -> Option { + let events = get_named(target, EVENTS_KEY); + is_object_value(events).then_some(events) } -fn add_stream_listener(stream: f64, event: f64, cb: f64, once: bool, prepend: bool) { - emit_meta_event(stream, b"newListener", &[event, cb]); - note_event_name(stream, event); - let Some((listeners, once_flags)) = ensure_listener_storage(stream, event) else { +/// node's `EventEmitter.init`: give the receiver its own `_events` (unless it +/// already has one that is not merely inherited), `_eventsCount` and +/// `_maxListeners`. Run by `super()` of a class extending EventEmitter and by +/// `EventEmitter.call(this)`. +pub(crate) fn init_event_emitter_state(target: f64) { + if !is_object_value(target) { return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + // node: reset when `this._events === undefined || this._events === + // ObjectGetPrototypeOf(this)._events`. Without an own `_events` the read + // IS the prototype's, so both arms hold and the (slow, inherited) reads + // are skipped; only an own `_events` needs comparing. + let has_own = crate::object::js_object_has_own(target.get_nanbox_f64(), name_key(EVENTS_KEY)) + .to_bits() + == super::TAG_TRUE; + let reset = !has_own || { + let events = scope.root_nanbox_f64(get_named(target.get_nanbox_f64(), EVENTS_KEY)); + is_undefined(events.get_nanbox_f64()) || { + let proto = crate::object::js_object_get_prototype_of(target.get_nanbox_f64()); + is_object_value(proto) + && get_named(proto, EVENTS_KEY).to_bits() == events.get_nanbox_f64().to_bits() + } }; - if !super::is_array_like_value(listeners) || !super::is_array_like_value(once_flags) { - return; + if reset { + reset_events(target.get_nanbox_f64()); } - let listeners_arr = super::raw_ptr_from_value(listeners) as *const crate::array::ArrayHeader; - let once_arr = super::raw_ptr_from_value(once_flags) as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(listeners_arr); - let mut out_listeners = crate::array::js_array_alloc(len + 1); - let mut out_once = crate::array::js_array_alloc(len + 1); - if prepend { - out_listeners = crate::array::js_array_push_f64(out_listeners, cb); - out_once = crate::array::js_array_push_f64(out_once, bool_value(once)); + let max = get_named(target.get_nanbox_f64(), MAX_LISTENERS_KEY); + let max = if crate::value::js_is_truthy(max) != 0 { + max + } else { + undefined_value() + }; + set_named(target.get_nanbox_f64(), MAX_LISTENERS_KEY, max); +} + +/// The data properties node's `EventEmitter.prototype` carries ahead of its +/// methods: `_events: undefined`, `_eventsCount: 0`, `_maxListeners: undefined`. +pub(crate) fn install_event_emitter_prototype_state(proto: f64) { + set_named(proto, EVENTS_KEY, undefined_value()); + set_named(proto, EVENTS_COUNT_KEY, 0.0); + set_named(proto, MAX_LISTENERS_KEY, undefined_value()); +} + +/// Call `target[name](...args)` with `this = target`, as node's emitter does +/// for `this.emit('newListener', …)`, `this.removeListener(…)` and +/// `this.removeAllListeners(…)`, so a subclass override sees those calls. +/// `None` when the receiver has no callable `name`. +fn call_method(target: f64, name: &[u8], args: &[f64]) -> Option { + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let arg_handles = scope.root_nanbox_f64_slice(args); + let method = get_named(target.get_nanbox_f64(), name); + if !is_callable_value(method) { + return None; } - for i in 0..len { - out_listeners = crate::array::js_array_push_f64( - out_listeners, - crate::array::js_array_get_f64(listeners_arr, i), - ); - out_once = - crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_arr, i)); - } - if !prepend { - out_listeners = crate::array::js_array_push_f64(out_listeners, cb); - out_once = crate::array::js_array_push_f64(out_once, bool_value(once)); - } - set_listener_storage( - stream, - event, - super::box_pointer(out_listeners as *const u8), - super::box_pointer(out_once as *const u8), - ); + let live_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + Some(unsafe { + crate::closure::native_call_value_this( + method, + crate::closure::JsThis::from_f64(target.get_nanbox_f64()), + live_args.as_ptr(), + live_args.len(), + ) + }) } -fn bool_value(value: bool) -> f64 { - f64::from_bits(if value { - super::TAG_TRUE +fn emit_via_method(target: f64, args: &[f64]) { + if call_method(target, b"emit", args).is_none() { + if let Some((event, rest)) = args.split_first() { + let _ = emit_stream_event(target, *event, rest); + } + } +} + +/// `listener.listener ?? listener` — a once wrapper's original. +fn unwrap_listener(listener: f64) -> f64 { + if !is_callable_value(listener) { + return listener; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let listener = scope.root_nanbox_f64(listener); + let inner = get_named(listener.get_nanbox_f64(), b"listener"); + if is_undefined(inner) || inner.to_bits() == crate::value::TAG_NULL { + listener.get_nanbox_f64() } else { - super::TAG_FALSE - }) + inner + } } -fn listener_snapshot(stream: f64, event: f64) -> Vec<(f64, bool)> { - let Some((listeners, once_flags)) = listener_storage(stream, event) else { - return Vec::new(); +/// The body of node's `onceWrapper`: captures `[target, type, listener, +/// fired]`; the first call removes the wrapper and forwards to the listener. +extern "C" fn ns_once_wrapper( + closure: *const ClosureHeader, + _this: crate::closure::JsThis, + rest: f64, +) -> f64 { + if closure.is_null() { + return undefined_value(); + } + if crate::value::js_is_truthy(js_closure_get_capture_f64(closure, 3)) != 0 { + return undefined_value(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let wrapper = scope.root_nanbox_f64(super::box_pointer(closure as *const u8)); + let target = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 0)); + let event = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 1)); + let listener = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 2)); + let args = { + let arr = super::raw_ptr_from_value(rest) as *const crate::array::ArrayHeader; + let len = if arr.is_null() || !is_array_value(rest) { + 0 + } else { + crate::array::js_array_length(arr) + }; + (0..len) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect::>() }; - if !super::is_array_like_value(listeners) || !super::is_array_like_value(once_flags) { - return Vec::new(); + let arg_handles = scope.root_nanbox_f64_slice(&args); + let removal = [event.get_nanbox_f64(), wrapper.get_nanbox_f64()]; + if call_method(target.get_nanbox_f64(), b"removeListener", &removal).is_none() { + remove_stream_listener_for_event( + target.get_nanbox_f64(), + event.get_nanbox_f64(), + wrapper.get_nanbox_f64(), + ); } - let listeners_arr = super::raw_ptr_from_value(listeners) as *const crate::array::ArrayHeader; - let once_arr = super::raw_ptr_from_value(once_flags) as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(listeners_arr); - let mut out = Vec::with_capacity(len as usize); - for i in 0..len { - out.push(( - crate::array::js_array_get_f64(listeners_arr, i), - crate::value::js_is_truthy(crate::array::js_array_get_f64(once_arr, i)) != 0, - )); + let wrapper_ptr = super::raw_ptr_from_value(wrapper.get_nanbox_f64()) as *mut ClosureHeader; + js_closure_set_capture_f64(wrapper_ptr, 3, f64::from_bits(super::TAG_TRUE)); + let live_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + if !is_callable_value(listener.get_nanbox_f64()) { + return undefined_value(); + } + unsafe { + crate::closure::native_call_value_this( + listener.get_nanbox_f64(), + crate::closure::JsThis::from_f64(target.get_nanbox_f64()), + live_args.as_ptr(), + live_args.len(), + ) } - out } -fn remove_event_name(stream: f64, event: f64) { - let Some(remove_idx) = event_name_index(stream, event) else { - return; +/// node's `_onceWrap(target, type, listener)`. +fn once_wrap(target: f64, event: f64, listener: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let event = scope.root_nanbox_f64(event); + let listener = scope.root_nanbox_f64(listener); + let wrapper = js_closure_alloc(crate::fn_info!(ns_once_wrapper, 1; with_rest(0)), 4); + js_closure_set_capture_f64(wrapper, 0, target.get_nanbox_f64()); + js_closure_set_capture_f64(wrapper, 1, event.get_nanbox_f64()); + js_closure_set_capture_f64(wrapper, 2, listener.get_nanbox_f64()); + js_closure_set_capture_f64(wrapper, 3, f64::from_bits(super::TAG_FALSE)); + let wrapper = scope.root_nanbox_f64(super::box_pointer(wrapper as *const u8)); + set_named( + wrapper.get_nanbox_f64(), + b"listener", + listener.get_nanbox_f64(), + ); + wrapper.get_nanbox_f64() +} + +fn add_stream_listener_for_event_with_options( + stream: f64, + event: f64, + cb: f64, + once: bool, + prepend: bool, +) { + if !is_callable_value(cb) { + throw_invalid_listener_type(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(stream); + let event = scope.root_nanbox_f64(event); + let cb = scope.root_nanbox_f64(cb); + let stored = if once { + once_wrap( + target.get_nanbox_f64(), + event.get_nanbox_f64(), + cb.get_nanbox_f64(), + ) + } else { + cb.get_nanbox_f64() + }; + let stored = scope.root_nanbox_f64(stored); + + let events = match events_of(target.get_nanbox_f64()) { + None => { + reset_events(target.get_nanbox_f64()); + events_of(target.get_nanbox_f64()) + } + Some(events) => { + if !is_undefined(get_named(events, b"newListener")) { + let announced = if once { + cb.get_nanbox_f64() + } else { + unwrap_listener(cb.get_nanbox_f64()) + }; + let meta = name_key(b"newListener"); + emit_via_method( + target.get_nanbox_f64(), + &[meta, event.get_nanbox_f64(), announced], + ); + // A `newListener` listener may have replaced `_events`. + events_of(target.get_nanbox_f64()) + } else { + Some(events) + } + } }; - let names = event_names_value(stream); - if !super::is_array_like_value(names) { + let Some(events) = events else { return; - } - let arr = super::raw_ptr_from_value(names) as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(arr); - let mut out = crate::array::js_array_alloc(len.saturating_sub(1)); - for i in 0..len { - if i != remove_idx { - out = crate::array::js_array_push_f64(out, crate::array::js_array_get_f64(arr, i)); + }; + let events = scope.root_nanbox_f64(events); + let existing = scope.root_nanbox_f64(get_key(events.get_nanbox_f64(), event.get_nanbox_f64())); + if is_undefined(existing.get_nanbox_f64()) { + set_key( + events.get_nanbox_f64(), + event.get_nanbox_f64(), + stored.get_nanbox_f64(), + ); + adjust_events_count(target.get_nanbox_f64(), 1.0); + } else if is_array_value(existing.get_nanbox_f64()) { + let arr = + super::raw_ptr_from_value(existing.get_nanbox_f64()) as *mut crate::array::ArrayHeader; + let grown = if prepend { + crate::array::js_array_unshift_f64(arr, stored.get_nanbox_f64()) + } else { + crate::array::js_array_push_f64(arr, stored.get_nanbox_f64()) + }; + if grown as usize != arr as usize { + set_key( + events.get_nanbox_f64(), + event.get_nanbox_f64(), + super::box_pointer(grown as *const u8), + ); } + } else { + let mut pair = crate::array::js_array_alloc(2); + let (first, second) = if prepend { + (stored.get_nanbox_f64(), existing.get_nanbox_f64()) + } else { + (existing.get_nanbox_f64(), stored.get_nanbox_f64()) + }; + pair = crate::array::js_array_push_f64(pair, first); + pair = crate::array::js_array_push_f64(pair, second); + set_key( + events.get_nanbox_f64(), + event.get_nanbox_f64(), + super::box_pointer(pair as *const u8), + ); } - super::set_hidden_value( - stream, - hidden_event_names_key(), - super::box_pointer(out as *const u8), - ); + + if super::string_value_eq(event.get_nanbox_f64(), b"data") { + super::readable_data_listener_added(target.get_nanbox_f64()); + } else if super::string_value_eq(event.get_nanbox_f64(), b"readable") { + super::readable_listener_added(target.get_nanbox_f64()); + } +} + +#[cold] +fn throw_invalid_listener_type() -> ! { + let msg = b"The \"listener\" argument must be of type function"; + let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + crate::node_submodules::register_error_code(s, "ERR_INVALID_ARG_TYPE"); + let err = crate::error::js_typeerror_new(s); + let bits = JSValue::pointer(err as *const u8).bits(); + crate::exception::js_throw(f64::from_bits(bits)) } -fn prune_event_if_empty(stream: f64, event: f64) { - if stream_listener_count_for_event(stream, event) == 0 { - remove_event_name(stream, event); +fn emit_remove_listener_if_watched(target: f64, events: f64, event: f64, listener: f64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let event = scope.root_nanbox_f64(event); + let listener = scope.root_nanbox_f64(listener); + if is_undefined(get_named(events, b"removeListener")) { + return; } + let meta = name_key(b"removeListener"); + emit_via_method( + target.get_nanbox_f64(), + &[meta, event.get_nanbox_f64(), listener.get_nanbox_f64()], + ); } +/// node's `removeListener(type, listener)`. True when a listener was removed. pub(super) fn remove_stream_listener_for_event(stream: f64, event: f64, cb: f64) -> bool { - let Some((listeners, once_flags)) = listener_storage(stream, event) else { + if !is_callable_value(cb) { + throw_invalid_listener_type(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(stream); + let event = scope.root_nanbox_f64(event); + let cb = scope.root_nanbox_f64(cb); + let Some(events) = events_of(target.get_nanbox_f64()) else { return false; }; - if !super::is_array_like_value(listeners) || !super::is_array_like_value(once_flags) { + let events = scope.root_nanbox_f64(events); + let list = scope.root_nanbox_f64(get_key(events.get_nanbox_f64(), event.get_nanbox_f64())); + if is_undefined(list.get_nanbox_f64()) { + return false; + } + let cb_bits = cb.get_nanbox_f64().to_bits(); + if is_callable_value(list.get_nanbox_f64()) { + let inner = unwrap_listener(list.get_nanbox_f64()); + if list.get_nanbox_f64().to_bits() != cb_bits && inner.to_bits() != cb_bits { + return false; + } + let announced = scope.root_nanbox_f64(inner); + if adjust_events_count(target.get_nanbox_f64(), -1.0) == 0.0 { + reset_events(target.get_nanbox_f64()); + } else { + let _ = crate::object::js_object_delete_dynamic_value( + events.get_nanbox_f64(), + event.get_nanbox_f64(), + ); + emit_remove_listener_if_watched( + target.get_nanbox_f64(), + events.get_nanbox_f64(), + event.get_nanbox_f64(), + announced.get_nanbox_f64(), + ); + } + return true; + } + if !is_array_value(list.get_nanbox_f64()) { return false; } - let listeners_arr = super::raw_ptr_from_value(listeners) as *const crate::array::ArrayHeader; - let once_arr = super::raw_ptr_from_value(once_flags) as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(listeners_arr); - let mut remove_idx = None; + let arr = super::raw_ptr_from_value(list.get_nanbox_f64()) as *const crate::array::ArrayHeader; + let len = crate::array::js_array_length(arr); + let mut position = None; for i in (0..len).rev() { - let listener = crate::array::js_array_get_f64(listeners_arr, i); - if listener.to_bits() == cb.to_bits() { - remove_idx = Some(i); + let arr = + super::raw_ptr_from_value(list.get_nanbox_f64()) as *const crate::array::ArrayHeader; + let item = crate::array::js_array_get_f64(arr, i); + if item.to_bits() == cb_bits || unwrap_listener(item).to_bits() == cb_bits { + position = Some(i); break; } } - let Some(remove_idx) = remove_idx else { + let Some(position) = position else { return false; }; - let mut out_listeners = crate::array::js_array_alloc(len); - let mut out_once = crate::array::js_array_alloc(len); - for i in 0..len { - let listener = crate::array::js_array_get_f64(listeners_arr, i); - if i == remove_idx { - continue; - } - out_listeners = crate::array::js_array_push_f64(out_listeners, listener); - out_once = - crate::array::js_array_push_f64(out_once, crate::array::js_array_get_f64(once_arr, i)); - } - set_listener_storage( - stream, - event, - super::box_pointer(out_listeners as *const u8), - super::box_pointer(out_once as *const u8), + let arr = super::raw_ptr_from_value(list.get_nanbox_f64()) as *mut crate::array::ArrayHeader; + // `js_array_splice` returns the DELETED elements and reports the edited + // (possibly reallocated) receiver through its out-parameter. + let mut kept: *mut crate::array::ArrayHeader = std::ptr::null_mut(); + let _deleted = + crate::array::js_array_splice(arr, position as i32, 1, std::ptr::null(), 0, &mut kept); + let kept_value = scope.root_nanbox_f64(super::box_pointer(kept as *const u8)); + let kept = + super::raw_ptr_from_value(kept_value.get_nanbox_f64()) as *const crate::array::ArrayHeader; + if crate::array::js_array_length(kept) == 1 { + let only = crate::array::js_array_get_f64(kept, 0); + set_key(events.get_nanbox_f64(), event.get_nanbox_f64(), only); + } else if kept as usize != arr as usize { + set_key( + events.get_nanbox_f64(), + event.get_nanbox_f64(), + kept_value.get_nanbox_f64(), + ); + } + emit_remove_listener_if_watched( + target.get_nanbox_f64(), + events.get_nanbox_f64(), + event.get_nanbox_f64(), + cb.get_nanbox_f64(), ); - prune_event_if_empty(stream, event); - emit_meta_event(stream, b"removeListener", &[event, cb]); true } +/// node's `removeAllListeners([type])`; `undefined` stands for "no argument". fn remove_all_stream_listeners_for_event(stream: f64, event: f64) { - if event_identity_bytes(event).is_none() { - for name in event_names_snapshot(stream) { - remove_all_stream_listeners_for_event(stream, name); + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(stream); + let event = scope.root_nanbox_f64(event); + let Some(events) = events_of(target.get_nanbox_f64()) else { + return; + }; + let events = scope.root_nanbox_f64(events); + let all = is_undefined(event.get_nanbox_f64()); + if is_undefined(get_named(events.get_nanbox_f64(), b"removeListener")) { + if all { + reset_events(target.get_nanbox_f64()); + } else if !is_undefined(get_key(events.get_nanbox_f64(), event.get_nanbox_f64())) { + if adjust_events_count(target.get_nanbox_f64(), -1.0) == 0.0 { + reset_events(target.get_nanbox_f64()); + } else { + let _ = crate::object::js_object_delete_dynamic_value( + events.get_nanbox_f64(), + event.get_nanbox_f64(), + ); + } } - super::set_hidden_value( - stream, - hidden_event_names_key(), - super::box_pointer(crate::array::js_array_alloc(0) as *const u8), - ); return; } - let removed = listener_snapshot(stream, event); - let empty_listeners = super::box_pointer(crate::array::js_array_alloc(0) as *const u8); - let empty_once = super::box_pointer(crate::array::js_array_alloc(0) as *const u8); - set_listener_storage(stream, event, empty_listeners, empty_once); - remove_event_name(stream, event); - for (listener, _) in removed { - emit_meta_event(stream, b"removeListener", &[event, listener]); + if all { + let keys = crate::proxy::js_reflect_own_keys(events.get_nanbox_f64()); + let keys = scope.root_nanbox_f64(keys); + let keys_arr = + super::raw_ptr_from_value(keys.get_nanbox_f64()) as *const crate::array::ArrayHeader; + let len = crate::array::js_array_length(keys_arr); + for i in 0..len { + let keys_arr = super::raw_ptr_from_value(keys.get_nanbox_f64()) + as *const crate::array::ArrayHeader; + let key = crate::array::js_array_get_f64(keys_arr, i); + if super::string_value_eq(key, b"removeListener") { + continue; + } + remove_all_via_method(target.get_nanbox_f64(), key); + } + let meta = name_key(b"removeListener"); + remove_all_via_method(target.get_nanbox_f64(), meta); + reset_events(target.get_nanbox_f64()); + return; + } + let listeners = scope.root_nanbox_f64(get_key(events.get_nanbox_f64(), event.get_nanbox_f64())); + if is_callable_value(listeners.get_nanbox_f64()) { + remove_via_method( + target.get_nanbox_f64(), + event.get_nanbox_f64(), + listeners.get_nanbox_f64(), + ); + } else if is_array_value(listeners.get_nanbox_f64()) { + let arr = super::raw_ptr_from_value(listeners.get_nanbox_f64()) + as *const crate::array::ArrayHeader; + let len = crate::array::js_array_length(arr); + for i in (0..len).rev() { + let arr = super::raw_ptr_from_value(listeners.get_nanbox_f64()) + as *const crate::array::ArrayHeader; + if i >= crate::array::js_array_length(arr) { + continue; + } + let item = crate::array::js_array_get_f64(arr, i); + remove_via_method(target.get_nanbox_f64(), event.get_nanbox_f64(), item); + } + } +} + +fn remove_via_method(target: f64, event: f64, listener: f64) { + if call_method(target, b"removeListener", &[event, listener]).is_none() { + remove_stream_listener_for_event(target, event, listener); } } -fn remove_once_listeners(stream: f64, event: f64) { - let Some((listeners, once_flags)) = listener_storage(stream, event) else { - return; +fn remove_all_via_method(target: f64, event: f64) { + if call_method(target, b"removeAllListeners", &[event]).is_none() { + remove_all_stream_listeners_for_event(target, event); + } +} + +/// The listener (functions or once wrappers) stored for `event`, in order. +fn stored_listeners(target: f64, event: f64) -> Vec { + let scope = crate::gc::RuntimeHandleScope::new(); + let event = scope.root_nanbox_f64(event); + let Some(events) = events_of(target) else { + return Vec::new(); }; - if !super::is_array_like_value(listeners) || !super::is_array_like_value(once_flags) { - return; + let list = get_key(events, event.get_nanbox_f64()); + if is_undefined(list) { + return Vec::new(); } - let listeners_arr = super::raw_ptr_from_value(listeners) as *const crate::array::ArrayHeader; - let once_arr = super::raw_ptr_from_value(once_flags) as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(listeners_arr); - let mut out_listeners = crate::array::js_array_alloc(len); - let mut out_once = crate::array::js_array_alloc(len); - let mut removed = Vec::new(); - for i in 0..len { - let listener = crate::array::js_array_get_f64(listeners_arr, i); - if crate::value::js_is_truthy(crate::array::js_array_get_f64(once_arr, i)) == 0 { - out_listeners = crate::array::js_array_push_f64(out_listeners, listener); - out_once = crate::array::js_array_push_f64( - out_once, - crate::array::js_array_get_f64(once_arr, i), - ); - } else { - removed.push(listener); - } + if is_array_value(list) { + let arr = super::raw_ptr_from_value(list) as *const crate::array::ArrayHeader; + let len = crate::array::js_array_length(arr); + return (0..len) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect(); } - set_listener_storage( - stream, - event, - super::box_pointer(out_listeners as *const u8), - super::box_pointer(out_once as *const u8), - ); - prune_event_if_empty(stream, event); - for listener in removed { - emit_meta_event(stream, b"removeListener", &[event, listener]); + if is_callable_value(list) { + return vec![list]; } + Vec::new() } pub(super) fn stream_listener_count_for_event(stream: f64, event: f64) -> usize { - listener_snapshot(stream, event).len() + let scope = crate::gc::RuntimeHandleScope::new(); + let event = scope.root_nanbox_f64(event); + let Some(events) = events_of(stream) else { + return 0; + }; + let list = get_key(events, event.get_nanbox_f64()); + if is_callable_value(list) { + 1 + } else if is_array_value(list) { + crate::array::js_array_length(super::raw_ptr_from_value(list) as *const _) as usize + } else { + 0 + } } +/// node's `eventNames()`: `Reflect.ownKeys(this._events)` while any remain. fn stream_event_names_array(stream: f64) -> *mut crate::array::ArrayHeader { - let mut out = crate::array::js_array_alloc(0); - for name in event_names_snapshot(stream) { - if stream_listener_count_for_event(stream, name) > 0 { - out = crate::array::js_array_push_f64(out, name); + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(stream); + let count = number_of(get_named(target.get_nanbox_f64(), EVENTS_COUNT_KEY)); + match events_of(target.get_nanbox_f64()) { + Some(events) if count > 0.0 => { + let keys = crate::proxy::js_reflect_own_keys(events); + super::raw_ptr_from_value(keys) as *mut crate::array::ArrayHeader } + _ => crate::array::js_array_alloc(0), } - out } +/// `listeners(type)` (unwrapped) or `rawListeners(type)` (once wrappers kept). fn stream_listeners_array_for_event( stream: f64, event: f64, raw: bool, ) -> *mut crate::array::ArrayHeader { - let snapshot = listener_snapshot(stream, event); - let mut out = crate::array::js_array_alloc(snapshot.len() as u32); - for (listener, once) in snapshot { - if raw && once { - let obj = crate::object::js_object_alloc(0, 1); - crate::object::js_object_set_field_by_name( - obj, - super::hidden_key(b"listener"), - listener, - ); - out = crate::array::js_array_push_f64(out, super::box_pointer(obj as *const u8)); + let scope = crate::gc::RuntimeHandleScope::new(); + let stored = stored_listeners(stream, event); + let stored = scope.root_nanbox_f64_slice(&stored); + let mut values = Vec::with_capacity(stored.len()); + for handle in &stored { + let listener = handle.get_nanbox_f64(); + values.push(if raw { + listener } else { - out = crate::array::js_array_push_f64(out, listener); - } + unwrap_listener(listener) + }); + } + let values = scope.root_nanbox_f64_slice(&values); + // `js_array_alloc` reserves the capacity, so no push below allocates and + // the rooted values stay current across the loop. + let mut out = crate::array::js_array_alloc(values.len() as u32); + for handle in &values { + out = crate::array::js_array_push_f64(out, handle.get_nanbox_f64()); } out } @@ -721,17 +974,86 @@ pub(super) fn call_listener_args(stream: f64, listener: f64, args: &[f64]) -> f6 } } +/// node's `emitUnhandledRejectionOrErr`: a captured listener rejection goes to +/// the emitter's `[Symbol.for('nodejs.rejection')](err, type, ...args)` when +/// it has one, else to `emit('error', err)` with capture switched off for that +/// emit. Captures `[emitter, type, argsArray]`. pub(super) extern "C" fn ns_capture_rejection( closure: *const ClosureHeader, _this: crate::closure::JsThis, reason: f64, ) -> f64 { if closure.is_null() { - return f64::from_bits(super::TAG_UNDEFINED); + return undefined_value(); } - let stream = js_closure_get_capture_f64(closure, 0); - let _ = emit_stream_event(stream, super::string_value(b"error"), &[reason]); - f64::from_bits(super::TAG_UNDEFINED) + let scope = crate::gc::RuntimeHandleScope::new(); + let stream = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 0)); + let event = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 1)); + let args = scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 2)); + let reason = scope.root_nanbox_f64(reason); + let hook_key = unsafe { crate::symbol::js_symbol_for(name_key(b"nodejs.rejection")) }; + let hook = scope.root_nanbox_f64(get_key(stream.get_nanbox_f64(), hook_key)); + if is_callable_value(hook.get_nanbox_f64()) { + let mut call_args = vec![reason.get_nanbox_f64(), event.get_nanbox_f64()]; + if is_array_value(args.get_nanbox_f64()) { + let arr = super::raw_ptr_from_value(args.get_nanbox_f64()) + as *const crate::array::ArrayHeader; + for i in 0..crate::array::js_array_length(arr) { + call_args.push(crate::array::js_array_get_f64(arr, i)); + } + } + unsafe { + crate::closure::native_call_value_this( + hook.get_nanbox_f64(), + crate::closure::JsThis::from_f64(stream.get_nanbox_f64()), + call_args.as_ptr(), + call_args.len(), + ); + } + return undefined_value(); + } + set_capture_rejections(stream.get_nanbox_f64(), false); + let error = name_key(b"error"); + emit_via_method(stream.get_nanbox_f64(), &[error, reason.get_nanbox_f64()]); + set_capture_rejections(stream.get_nanbox_f64(), true); + undefined_value() +} + +fn set_capture_rejections(stream: f64, enabled: bool) { + let scope = crate::gc::RuntimeHandleScope::new(); + let stream = scope.root_nanbox_f64(stream); + let key = super::hidden_capture_rejections_key(); + super::set_hidden_value(stream.get_nanbox_f64(), key, bool_value(enabled)); +} + +fn bool_value(value: bool) -> f64 { + f64::from_bits(if value { + super::TAG_TRUE + } else { + super::TAG_FALSE + }) +} + +/// node's `EventEmitter.init(opts)` capture step: a truthy +/// `opts.captureRejections` must be a boolean, and turns capture on. +pub(crate) fn init_event_emitter_capture(target: f64, options: f64) { + if !is_object_value(target) || !is_object_value(options) { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let value = get_named(options, b"captureRejections"); + if crate::value::js_is_truthy(value) == 0 { + return; + } + if value.to_bits() != super::TAG_TRUE { + let message = format!( + "The \"options.captureRejections\" property must be of type boolean. Received {}", + crate::fs::validate::describe_received(value) + ); + crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE"); + } + set_capture_rejections(target.get_nanbox_f64(), true); } fn capture_rejections_enabled(stream: f64) -> bool { @@ -751,19 +1073,34 @@ fn swallow_listener_rejection(result: f64) { } } -fn capture_listener_rejection(stream: f64, result: f64) { +/// node's `addCatch`: route a listener's rejected promise to +/// [`ns_capture_rejection`] with the emit's type and arguments. +fn capture_listener_rejection(stream: f64, event: f64, args: &[f64], result: f64) { if crate::promise::js_value_is_promise(result) == 0 { return; } - let promise = crate::value::js_nanbox_get_pointer(result) as *mut crate::promise::Promise; - if promise.is_null() { - return; + let scope = crate::gc::RuntimeHandleScope::new(); + let stream = scope.root_nanbox_f64(stream); + let event = scope.root_nanbox_f64(event); + let result = scope.root_nanbox_f64(result); + let arg_handles = scope.root_nanbox_f64_slice(args); + let mut arr = crate::array::js_array_alloc(arg_handles.len() as u32); + for handle in &arg_handles { + arr = crate::array::js_array_push_f64(arr, handle.get_nanbox_f64()); } + let arr = scope.root_nanbox_f64(super::box_pointer(arr as *const u8)); let on_rejected = js_closure_alloc( crate::fn_info!(ns_capture_rejection, 1; with_declared(1)), - 1, + 3, ); - js_closure_set_capture_f64(on_rejected, 0, stream); + js_closure_set_capture_f64(on_rejected, 0, stream.get_nanbox_f64()); + js_closure_set_capture_f64(on_rejected, 1, event.get_nanbox_f64()); + js_closure_set_capture_f64(on_rejected, 2, arr.get_nanbox_f64()); + let promise = crate::value::js_nanbox_get_pointer(result.get_nanbox_f64()) + as *mut crate::promise::Promise; + if promise.is_null() { + return; + } crate::promise::js_promise_then(promise, std::ptr::null(), on_rejected); } @@ -789,73 +1126,82 @@ pub(super) fn emit_stream_event_from_array( /// already delivered, so the unhandled-error throw below fires only when /// neither registry had a listener. pub(super) fn has_stream_listeners(stream: f64, event: f64) -> bool { - event_identity_bytes(event).is_some() && !listener_snapshot(stream, event).is_empty() + stream_listener_count_for_event(stream, event) > 0 } +/// node's `emit(type, ...args)`. pub(super) fn emit_stream_event(stream: f64, event: f64, args: &[f64]) -> f64 { - if event_identity_bytes(event).is_none() { - return f64::from_bits(super::TAG_FALSE); - } - // #10600: `listener_snapshot`'s Vec, `stream`, `event` and `args` are all - // plain Rust locals, not GC roots. A listener can allocate enough to - // trigger a moving minor collection; an unrooted copy then holds a - // retired from-space address for the NEXT listener dispatched from this - // same loop (reproduced: a `class X extends EventEmitter` whose second - // listener allocates heavily segfaults dereferencing the third - // listener's stale closure pointer under the default generational GC — - // confirmed gone under `PERRY_GEN_GC=0`). Root the whole dispatch - // window through one handle scope and re-read every value's current - // (possibly relocated) bits before each call, the pattern `events.rs`'s - // async branch already uses. + // #10600: a listener can allocate enough to trigger a moving collection, + // so the receiver, the event, the arguments and the listener snapshot are + // all rooted for the whole dispatch window and re-read before each call. let scope = crate::gc::RuntimeHandleScope::new(); let stream_h = scope.root_nanbox_f64(stream); let event_h = scope.root_nanbox_f64(event); let arg_handles = scope.root_nanbox_f64_slice(args); - if super::string_value_eq(event, b"error") { + let is_error = super::string_value_eq(event, b"error"); + if is_error { if let Some(first) = args.first() { - super::set_hidden_value(stream, super::hidden_error_key(), *first); - super::refresh_readable_aborted_flag(stream); + let first = scope.root_nanbox_f64(*first); + let key = super::hidden_error_key(); + super::set_hidden_value(stream_h.get_nanbox_f64(), key, first.get_nanbox_f64()); + super::refresh_readable_aborted_flag(stream_h.get_nanbox_f64()); } - let monitor_event = error_monitor_event(); - let monitor_snapshot = listener_snapshot(stream, monitor_event); - if monitor_snapshot.iter().any(|(_, once)| *once) { - remove_once_listeners(stream, monitor_event); - } - let monitor_listener_values: Vec = monitor_snapshot.iter().map(|(l, _)| *l).collect(); - let monitor_handles = scope.root_nanbox_f64_slice(&monitor_listener_values); - for handle in &monitor_handles { - let live_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - call_listener_args( - stream_h.get_nanbox_f64(), - handle.get_nanbox_f64(), - &live_args, - ); + } + let events = events_of(stream_h.get_nanbox_f64()); + let mut unhandled_error = is_error; + match events { + Some(events) => { + if is_error { + let events = scope.root_nanbox_f64(events); + let monitor = error_monitor_event(); + if !is_undefined(get_key(events.get_nanbox_f64(), monitor)) { + let live = + crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + let mut monitor_args = Vec::with_capacity(live.len() + 1); + monitor_args.push(monitor); + monitor_args.extend_from_slice(&live); + emit_via_method(stream_h.get_nanbox_f64(), &monitor_args); + } + unhandled_error = is_undefined(get_named(events.get_nanbox_f64(), b"error")); + } } + None if !is_error => return f64::from_bits(super::TAG_FALSE), + None => {} + } + if unhandled_error { + let err = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles) + .first() + .copied() + .unwrap_or_else(undefined_value); + crate::exception::js_throw(err); } - let snapshot = listener_snapshot(stream_h.get_nanbox_f64(), event_h.get_nanbox_f64()); - if snapshot.is_empty() { - if super::string_value_eq(event_h.get_nanbox_f64(), b"error") { - let err = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(super::TAG_UNDEFINED)); - crate::exception::js_throw(err); - } + let Some(events) = events_of(stream_h.get_nanbox_f64()) else { + return f64::from_bits(super::TAG_FALSE); + }; + let handler = get_key(events, event_h.get_nanbox_f64()); + if is_undefined(handler) { return f64::from_bits(super::TAG_FALSE); } - if snapshot.iter().any(|(_, once)| *once) { - remove_once_listeners(stream_h.get_nanbox_f64(), event_h.get_nanbox_f64()); - } + // node clones the array before dispatch, so listeners added or removed + // by a listener take effect from the next emit. + let listener_values: Vec = if is_callable_value(handler) { + vec![handler] + } else if is_array_value(handler) { + let arr = super::raw_ptr_from_value(handler) as *const crate::array::ArrayHeader; + (0..crate::array::js_array_length(arr)) + .map(|i| crate::array::js_array_get_f64(arr, i)) + .collect() + } else { + Vec::new() + }; + let listener_handles = scope.root_nanbox_f64_slice(&listener_values); // Node's Readable data delivery path does not route async `data` listener // rejections through captureRejections; custom EventEmitter-style events do. + // As in node's `addCatch`, the capture flag is consulted only for a + // listener that returned a promise. let is_data = super::string_value_eq(event_h.get_nanbox_f64(), b"data"); - let capture_rejections = capture_rejections_enabled(stream_h.get_nanbox_f64()) - && !super::string_value_eq(event_h.get_nanbox_f64(), b"error") - && !is_data; - let listener_values: Vec = snapshot.iter().map(|(l, _)| *l).collect(); - let listener_handles = scope.root_nanbox_f64_slice(&listener_values); for handle in &listener_handles { let live_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); let result = call_listener_args( @@ -863,8 +1209,16 @@ pub(super) fn emit_stream_event(stream: f64, event: f64, args: &[f64]) -> f64 { handle.get_nanbox_f64(), &live_args, ); - if capture_rejections { - capture_listener_rejection(stream_h.get_nanbox_f64(), result); + if crate::promise::js_value_is_promise(result) == 0 { + continue; + } + if !is_error && !is_data && capture_rejections_enabled(stream_h.get_nanbox_f64()) { + capture_listener_rejection( + stream_h.get_nanbox_f64(), + event_h.get_nanbox_f64(), + &crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles), + result, + ); } else if is_data { // Node's Readable swallows a rejection returned by an async `data` // listener — it is neither captured to `error` nor surfaced as an @@ -878,10 +1232,3 @@ pub(super) fn emit_stream_event(stream: f64, event: f64, args: &[f64]) -> f64 { fn error_monitor_event() -> f64 { unsafe { crate::symbol::js_symbol_for(super::string_value(b"events.errorMonitor")) } } - -fn emit_meta_event(stream: f64, name: &[u8], args: &[f64]) { - let event = super::string_value(name); - if stream_listener_count_for_event(stream, event) > 0 { - let _ = emit_stream_event(stream, event, args); - } -} diff --git a/crates/perry-runtime/src/node_stream_keys.rs b/crates/perry-runtime/src/node_stream_keys.rs index 3de13c110d..5de98b129b 100644 --- a/crates/perry-runtime/src/node_stream_keys.rs +++ b/crates/perry-runtime/src/node_stream_keys.rs @@ -53,11 +53,6 @@ pub(super) fn hidden_ended_key() -> *mut crate::string::StringHeader { hidden_key(STREAM_ENDED_KEY) } -#[inline] -pub(super) fn hidden_max_listeners_key() -> *mut crate::string::StringHeader { - hidden_key(STREAM_MAX_LISTENERS_KEY) -} - #[inline] pub(super) fn hidden_capture_rejections_key() -> *mut crate::string::StringHeader { hidden_key(STREAM_CAPTURE_REJECTIONS_KEY) diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index fe6962e70c..42bcdbf24e 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -1937,14 +1937,13 @@ pub(super) fn readable_methods() -> [(&'static str, StubFn); 39] { /// #5137: the bare `EventEmitter` surface — the same 15 listener/emit /// methods that `readable_methods`/`writable_methods` share, minus all the -/// stream-specific entries. Installed onto `this` by -/// `js_event_emitter_subclass_init` so a source-compiled `class X extends -/// EventEmitter` (e.g. commander's `Command`) gets working -/// `.on`/`.emit`/`.once`/… without routing through the handle-based -/// `js_event_emitter_*` shim. The closures are the generic -/// `ns_*` emitter helpers, which key all state off the receiver object, so -/// they work unchanged on a plain object that never went through a stream -/// constructor. +/// stream-specific entries. `install_event_emitter_prototype` puts ONE set of +/// them on `EventEmitter.prototype`, which a source-compiled `class X extends +/// EventEmitter` (e.g. commander's `Command`) inherits, without routing +/// through the handle-based `js_event_emitter_*` shim. The closures are the +/// generic `ns_*` emitter helpers, which keep all state in the receiver's +/// own `_events`, so they work unchanged on a plain object that never went +/// through a stream constructor. pub(super) fn emitter_methods() -> [(&'static str, StubFn); 15] { [ ("on", &NS_ON2_INFO), diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index f00ba97df3..6cbbe01fe5 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -1230,7 +1230,7 @@ pub(crate) unsafe fn nm_ee_prototype_install( ) { proto.with_mut_ptr::(|proto| { - crate::node_stream::install_event_emitter_prototype_methods(proto); + crate::node_stream::install_event_emitter_prototype(proto); if method == "EventEmitterAsyncResource" { crate::node_stream::install_event_emitter_async_resource_prototype(proto); } diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs index e64c4875ca..f983c1bea3 100644 --- a/crates/perry-runtime/src/object/field_get_set/enumeration.rs +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -1653,6 +1653,7 @@ pub(crate) fn is_internal_runtime_key_bytes(b: &[u8]) -> bool { || b == crate::async_hooks::ASYNC_RESOURCE_EVENT_EMITTER_KEY || is_class_capture_key(b) || b.starts_with(crate::node_stream::NATIVE_BASE_SUPER_PREFIX) + || b == crate::node_stream::STREAM_CAPTURE_REJECTIONS_KEY || b.starts_with(b"__perry_computed_field_key_") || b == b"#" || b == b"#" diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 61b8ddd3c5..5da49e4492 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -542,7 +542,14 @@ unsafe fn nm_ee_dynamic_super( ), ); } - return Some(crate::node_stream::js_event_emitter_subclass_init(this_val)); + let options = if !args_ptr.is_null() && args_len > 0 { + *args_ptr + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + return Some(crate::node_stream::js_event_emitter_subclass_init( + this_val, options, + )); } } None diff --git a/test-files/test_gap_eventemitter_shared_prototype.ts b/test-files/test_gap_eventemitter_shared_prototype.ts new file mode 100644 index 0000000000..ad3d0f1118 --- /dev/null +++ b/test-files/test_gap_eventemitter_shared_prototype.ts @@ -0,0 +1,253 @@ +// EventEmitter methods live on ONE shared EventEmitter.prototype, as in node: +// an emitter instance owns only node's state (`_events`, `_eventsCount`, +// `_maxListeners`), and that state is the real listener store. Covers class +// subclasses, overrides with super calls, util.inherits / EventEmitter.call +// function constructors, Object.create(EventEmitter.prototype) and a +// setPrototypeOf mixin, plus the listener API over that store. +import { EventEmitter } from "node:events"; +import * as util from "node:util"; + +const METHODS = [ + "on", "addListener", "once", "off", "removeListener", "emit", + "prependListener", "prependOnceListener", "removeAllListeners", + "listenerCount", "listeners", "rawListeners", "eventNames", + "setMaxListeners", "getMaxListeners", +]; +const own = (o: any, k: string) => Object.prototype.hasOwnProperty.call(o, k); +const ownMethods = (o: any) => METHODS.filter((m) => own(o, m)).length; +const show = (label: string, o: any) => { + console.log(label, "keys", JSON.stringify(Object.keys(o))); + console.log(label, "names", JSON.stringify(Object.getOwnPropertyNames(o))); + console.log(label, "own methods", ownMethods(o), "own on", own(o, "on")); +}; + +// ── 1. The prototype ───────────────────────────────────────────────── +const proto: any = EventEmitter.prototype; +console.log("proto names", JSON.stringify(Object.getOwnPropertyNames(proto).sort())); +for (const k of ["on", "emit", "_events", "_eventsCount", "_maxListeners"]) { + const d = Object.getOwnPropertyDescriptor(proto, k)!; + console.log("desc", k, typeof d.value, d.writable, d.enumerable, d.configurable); +} +console.log("aliases", proto.addListener === proto.on, proto.off === proto.removeListener); +console.log("proto state", proto._events, proto._eventsCount, proto._maxListeners); + +// ── 2. A class subclass ────────────────────────────────────────────── +class Bus extends EventEmitter {} +const b: any = new Bus(); +show("sub", b); +console.log("sub inherits", METHODS.every((m) => typeof b[m] === "function")); +console.log("sub shares", b.on === proto.on, b.emit === new Bus().emit, b.on === b.addListener); +console.log("sub state", Object.getPrototypeOf(b._events) === null, b._eventsCount, b._maxListeners); +console.log("sub proto chain", Object.getPrototypeOf(Bus.prototype) === proto, b instanceof EventEmitter); + +// A subclass with its own fields: the same own state plus the field. +class Fielded extends EventEmitter { + count = 0; + label: string; + constructor() { + super(); + this.label = "f"; + } +} +const fd: any = new Fielded(); +console.log("fielded keys", JSON.stringify(Object.keys(fd).sort()), ownMethods(fd)); + +// ── 3. Overrides and super ─────────────────────────────────────────── +const log: string[] = []; +class Logged extends EventEmitter { + emit(ev: string | symbol, ...args: any[]): boolean { + log.push("emit:" + String(ev)); + return super.emit(ev, ...args); + } + on(ev: string | symbol, fn: (...a: any[]) => void): this { + log.push("on:" + String(ev)); + return super.on(ev, fn); + } +} +class Deeper extends Logged { + emit(ev: string | symbol, ...args: any[]): boolean { + log.push("deeper:" + String(ev)); + return super.emit(ev, ...args); + } +} +const lg: any = new Deeper(); +console.log("override own", own(lg, "emit"), own(lg, "on"), ownMethods(lg)); +lg.on("a", (x: number) => log.push("a=" + x)); +lg.addListener("a", (x: number) => log.push("a2=" + x)); +lg.on("newListener", (ev: string) => log.push("new:" + ev)); +lg.on("b", () => log.push("b")); +console.log("emit result", lg.emit("a", 7), lg.emit("nobody")); +console.log("log", log.join(",")); +console.log("override counts", lg.listenerCount("a"), JSON.stringify(lg.eventNames())); + +// ── 4. util.inherits and EventEmitter.call(this) ───────────────────── +function Legacy(this: any) { + EventEmitter.call(this); + this.name = "legacy"; +} +util.inherits(Legacy, EventEmitter); +const lgc: any = new (Legacy as any)(); +show("inherits", lgc); +lgc.once("ping", (v: number) => console.log("inherits ping", v)); +lgc.emit("ping", 1); +lgc.emit("ping", 2); +console.log("inherits after", lgc.listenerCount("ping"), JSON.stringify(Object.keys(lgc))); + +function Bare(this: any) { + EventEmitter.call(this); +} +const bare: any = new (Bare as any)(); +console.log("call only", JSON.stringify(Object.keys(bare)), typeof bare.on); + +// ── 5. Object.create(EventEmitter.prototype) and a mixin ───────────── +const oc: any = Object.create(EventEmitter.prototype); +console.log("ocreate before", JSON.stringify(Object.keys(oc)), oc._eventsCount); +oc.on("q", (v: number) => console.log("ocreate q", v)); +oc.emit("q", 9); +console.log("ocreate after", JSON.stringify(Object.keys(oc)), oc._eventsCount); + +const mix: any = { name: "mix" }; +Object.setPrototypeOf(mix, EventEmitter.prototype); +EventEmitter.call(mix); +show("mixin", mix); +mix.once("w", (v: number) => console.log("mixin w", v)); +mix.emit("w", 1); +mix.emit("w", 2); + +// ── 6. The listener API over the `_events` store ───────────────────── +const e: any = new Bus(); +const f1 = () => console.log("f1"); +const f2 = () => console.log("f2"); +const f3 = () => console.log("f3"); +e.on("x", f1); +console.log("store one", typeof e._events.x, e._events.x === f1, e._eventsCount); +e.prependListener("x", f2); +e.once("x", f3); +console.log("store many", Array.isArray(e._events.x), e._events.x.length, e._eventsCount); +console.log("listeners", e.listeners("x").map((f: any) => f === f1 ? "f1" : f === f2 ? "f2" : f === f3 ? "f3" : "?").join(",")); +const raw = e.rawListeners("x"); +console.log("raw once wrapper", typeof raw[2], raw[2] === f3, raw[2].listener === f3); +e.emit("x"); +console.log("after once", e.listenerCount("x")); +e.prependOnceListener("x", f3); +e.emit("x"); +e.off("x", f1); +console.log("after off", e.listenerCount("x"), e._events.x === f2); +e.removeListener("x", f2); +console.log("after remove", e.listenerCount("x"), JSON.stringify(Object.keys(e._events)), e._eventsCount); +e.on("y", f1); +e.on("z", f2); +const sym = Symbol("s"); +e.on(sym, f3); +console.log("names", e.eventNames().map(String).join(",")); +e.removeAllListeners("y"); +console.log("names2", e.eventNames().map(String).join(",")); +e.removeAllListeners(); +console.log("names3", JSON.stringify(e.eventNames()), e._eventsCount); +console.log("max", e.getMaxListeners(), own(e, "_maxListeners")); +e.setMaxListeners(3); +console.log("max2", e.getMaxListeners(), e._maxListeners); + +// newListener / removeListener meta events. +const meta: any = new Bus(); +const metaLog: string[] = []; +meta.on("removeListener", (ev: string) => metaLog.push("rm:" + String(ev))); +meta.on("newListener", (ev: string) => metaLog.push("new:" + String(ev))); +meta.on("k", f1); +meta.once("k", f2); +meta.emit("k"); +meta.removeAllListeners("k"); +console.log("meta", metaLog.join(",")); + +// ── 7. Errors ──────────────────────────────────────────────────────── +const er: any = new Bus(); +try { + er.emit("error", new Error("boom")); +} catch (err: any) { + console.log("threw", err.message); +} +er.on(EventEmitter.errorMonitor, (err: Error) => console.log("monitor", err.message)); +try { + er.emit("error", new Error("watched")); +} catch (err: any) { + console.log("threw2", err.message); +} +er.on("error", (err: Error) => console.log("handled", err.message)); +console.log("emit error", er.emit("error", new Error("ok"))); +try { + er.on("x", 42); +} catch (err: any) { + console.log("bad listener", err.name, err.code); +} +console.log("rejection symbol", EventEmitter.captureRejectionSymbol === Symbol.for("nodejs.rejection")); + +// ── 8. Many emitters under a moving collector ──────────────────────── +let total = 0; +for (let i = 0; i < 3000; i++) { + const em: any = i % 2 ? new Bus() : new Deeper(); + em.on("tick", (n: number) => { + const junk = []; + for (let j = 0; j < 20; j++) junk.push({ j, s: "x" + j }); + total += n + junk.length; + }); + em.once("tick", (n: number) => { total += n; }); + em.emit("tick", 1); + em.emit("tick", 2); + if (em.listenerCount("tick") !== 1 || ownMethods(em) !== 0) console.log("BAD", i); +} +console.log("stress", total, log.length > 0); + +// ── 9. Every heritage form reaches the one prototype ──────────────── +import { EventEmitter as AliasedEE } from "node:events"; +import eventsDefault from "node:events"; +const Bound = EventEmitter; +class FormA extends AliasedEE {} +class FormB extends Bound {} +const FormC = class extends EventEmitter {}; +const Mixin = any>(Base: T) => class extends Base { mixed = true; }; +class FormD extends Mixin(EventEmitter) {} +class FormF extends eventsDefault {} +class FormG extends eventsDefault.EventEmitter {} +function pickBase(): typeof EventEmitter { return EventEmitter; } +class FormH extends pickBase() {} +for (const [n, K] of [["A", FormA], ["B", FormB], ["C", FormC], ["D", FormD], ["F", FormF], ["G", FormG], ["H", FormH]] as any) { + const o = new K(); + let got = 0; + o.on("x", (v: number) => { got += v; }); + o.once("x", (v: number) => { got += 10 * v; }); + const r1 = o.emit("x", 1); + const r2 = o.emit("x", 2); + console.log("form", n, r1, r2, got, o.listenerCount("x"), JSON.stringify(Object.keys(o).sort()), ownMethods(o), o instanceof EventEmitter); +} + +// ── 10. captureRejections through super(opts) ──────────────────────── +class Captures extends EventEmitter { + constructor() { super({ captureRejections: true }); } +} +const cap: any = new Captures(); +console.log("capture keys", JSON.stringify(Object.keys(cap)), ownMethods(cap)); +cap.on("error", (err: Error) => console.log("captured", err.message)); +cap.on("job", async (n: number) => { throw new Error("async boom " + n); }); +cap.emit("job", 1); +class Hooked extends EventEmitter { + constructor() { super({ captureRejections: true }); } + [Symbol.for("nodejs.rejection")](err: Error, ev: string, n: number) { + console.log("rejection hook", err.message, ev, n); + } +} +const hooked: any = new Hooked(); +hooked.on("job", async () => { throw new Error("hooked"); }); +hooked.emit("job", 2); +class NotCaptured extends EventEmitter {} +const quiet: any = new NotCaptured(); +quiet.on("job", async () => 1); +quiet.emit("job"); +try { + class BadOption extends EventEmitter { + constructor() { super({ captureRejections: 1 as any }); } + } + new BadOption(); +} catch (err: any) { + console.log("bad option", err.name, err.code); +} +setTimeout(() => console.log("done"), 10); diff --git a/test-files/test_gap_eventemitter_shared_prototype_cjs.cts b/test-files/test_gap_eventemitter_shared_prototype_cjs.cts new file mode 100644 index 0000000000..de2a66f0da --- /dev/null +++ b/test-files/test_gap_eventemitter_shared_prototype_cjs.cts @@ -0,0 +1,34 @@ +// CommonJS forms of an EventEmitter subclass: `require("events")` as the base +// (the module IS EventEmitter), `require("events").EventEmitter`, +// util.inherits with EventEmitter.call(this), a setPrototypeOf-linked +// function constructor, and EventEmitter.call(this) alone (which, as in node, +// gives the object emitter state but no methods). No instance owns a method. +const EE = require("events"); +const { EventEmitter } = require("events"); +const util = require("util"); +const own = (o: any, k: string) => Object.prototype.hasOwnProperty.call(o, k); +class D extends EE {} +class F extends EE.EventEmitter {} +class G extends EventEmitter {} +for (const [n, K] of [["D", D], ["F", F], ["G", G]] as any[]) { + const o = new K(); + console.log(n, typeof o.on, typeof o.emit, typeof o.once, JSON.stringify(Object.keys(o)), own(o, "on"), own(o, "emit")); + o.on("x", (v: any) => console.log(n, "got", v)); + console.log(n, o.emit("x", 1), o.listenerCount("x"), o.on === EE.prototype.on); +} +function L(this: any) { EE.call(this); } +util.inherits(L, EE); +const l: any = new (L as any)(); +l.once("y", (v: any) => console.log("L got", v)); +l.emit("y", 2); +l.emit("y", 3); +console.log("L", JSON.stringify(Object.keys(l)), own(l, "once"), l.listenerCount("y")); +function M(this: any) { EventEmitter.call(this); } +Object.setPrototypeOf(M.prototype, EventEmitter.prototype); +const m: any = new (M as any)(); +m.on("z", () => console.log("M z")); +m.emit("z"); +console.log("M", JSON.stringify(Object.keys(m)), JSON.stringify(Object.keys(m._events))); +function N(this: any) { EventEmitter.call(this); } +const nn: any = new (N as any)(); +console.log("N", JSON.stringify(Object.keys(nn)), typeof nn.on);