From 4f49591c523a346cb639463f1e31785aa9d16b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 6 Sep 2026 19:32:29 +0200 Subject: [PATCH] fix(runtime): link cluster default prototype --- changelog.d/9905-cluster-default-prototype.md | 5 ++ crates/perry-runtime/src/cluster.rs | 46 +++++++++++++++++-- .../perry-runtime/src/object/native_module.rs | 2 +- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 changelog.d/9905-cluster-default-prototype.md diff --git a/changelog.d/9905-cluster-default-prototype.md b/changelog.d/9905-cluster-default-prototype.md new file mode 100644 index 0000000000..79e6c468fb --- /dev/null +++ b/changelog.d/9905-cluster-default-prototype.md @@ -0,0 +1,5 @@ +### Fixed + +- The `node:cluster` default export now inherits from the canonical + `EventEmitter.prototype`, so reflective prototype checks agree with Node while + preserving the cached singleton used by cluster event methods. diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index 4dfcc26d8e..2798e8a4cc 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -241,11 +241,51 @@ thread_local! { /// `import cluster from "node:cluster"`. Cached (see /// `should_cache_native_module_namespace`), so EventEmitter methods can return /// it for `cluster.on(...) === cluster` chaining. -fn cluster_default_value() -> f64 { - crate::object::js_create_native_module_namespace( +pub(crate) fn cluster_default_value() -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let cluster = scope.root_nanbox_f64(crate::object::js_create_native_module_namespace( b"cluster.default".as_ptr(), "cluster.default".len(), - ) + )); + let event_emitter = scope.root_nanbox_f64(crate::object::bound_native_callable_export_value( + "events", + "EventEmitter", + )); + if let Some(event_emitter_proto) = + crate::object::ordinary_function_prototype_value_for_read(event_emitter.get_nanbox_f64()) + { + let event_emitter_proto = scope.root_nanbox_f64(event_emitter_proto); + let cluster_addr = (cluster.get_nanbox_u64() & crate::value::POINTER_MASK) as usize; + let proto_bits = event_emitter_proto.get_nanbox_u64(); + if crate::object::prototype_chain::object_static_prototype(cluster_addr) != Some(proto_bits) + { + crate::object::prototype_chain::object_set_static_prototype(cluster_addr, proto_bits); + } + } + cluster.get_nanbox_f64() +} + +#[cfg(test)] +mod cluster_default_prototype_tests { + use super::*; + + #[test] + fn default_export_inherits_from_event_emitter() { + let scope = crate::gc::RuntimeHandleScope::new(); + let cluster = scope.root_nanbox_f64(cluster_default_value()); + let event_emitter = scope.root_nanbox_f64( + crate::object::bound_native_callable_export_value("events", "EventEmitter"), + ); + let expected = crate::object::ordinary_function_prototype_value_for_read( + event_emitter.get_nanbox_f64(), + ) + .expect("EventEmitter must expose a prototype object"); + + assert_eq!( + crate::object::js_object_get_prototype_of(cluster.get_nanbox_f64()).to_bits(), + expected.to_bits(), + ); + } } fn cluster_emitter_event_name(event: f64) -> Option { diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index b7440e4687..d427223761 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -694,7 +694,7 @@ pub(crate) fn cjs_default_export_value(module_name: &str) -> Option { // #3687: `node:cluster` default import is a distinct EventEmitter-shaped // `cluster.default` namespace (its `on`/`emit`/… reads diverge from the // bare `import * as` namespace). - "cluster" => create_cjs_default_namespace("cluster"), + "cluster" => Some(crate::cluster::cluster_default_value()), // #3693: `node:dgram` default === the module namespace (CJS // `module.exports`); a cached singleton makes `dgram === ns.default`. "dgram" => Some(js_create_native_module_namespace(