diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index c426185e4b..8adbbd8a29 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -1071,7 +1071,13 @@ pub extern "C" fn js_arraylike_slice( } else { clamp_index(start, len) }; - let e = if has_end == 0 { + // ECMA-262 §23.1.3.25 step 4: an `end` of `undefined` (whether omitted OR + // passed explicitly, e.g. `Array.prototype.slice.call(arr, 1, undefined)`) + // means "to the end" (relativeEnd = len). Only a present, non-undefined + // `end` is run through ToIntegerOrInfinity. `clamp_index` maps the + // TAG_UNDEFINED bit pattern (a NaN) to 0, which would wrongly empty the + // slice — so special-case it here. + let e = if has_end == 0 || end.to_bits() == crate::value::TAG_UNDEFINED { len } else { clamp_index(end, len) diff --git a/crates/perry-runtime/src/atomics.rs b/crates/perry-runtime/src/atomics.rs index 55420870d1..9d79d0acdf 100644 --- a/crates/perry-runtime/src/atomics.rs +++ b/crates/perry-runtime/src/atomics.rs @@ -475,11 +475,12 @@ pub extern "C" fn js_atomics_load(_closure: *const ClosureHeader, view: f64, ind #[no_mangle] pub extern "C" fn js_atomics_is_lock_free(_closure: *const ClosureHeader, size: f64) -> f64 { - // ToIntegerOrInfinity(size) (runs `valueOf`/`toString`, so `'1'`, `true`, - // `{valueOf:()=>1}` and `3.14`→3 all behave like Node), then a membership - // test over the lock-free element widths. - let n = numeric_arg(size); - nanbox_bool(matches!(n as i64, 1 | 2 | 4 | 8)) + // ToNumber(size) (runs `valueOf`/`toString`, so `'4'`→4 behaves like Node), + // then an EXACT membership test over the lock-free element widths. Node/V8 + // compares the raw number, so `4.9` is NOT floored to 4 — it is simply not a + // valid element width and returns false. + let n = number_arg(size); + nanbox_bool(n == 1.0 || n == 2.0 || n == 4.0 || n == 8.0) } #[no_mangle] diff --git a/crates/perry-runtime/src/builtins/globals.rs b/crates/perry-runtime/src/builtins/globals.rs index ec32a9f578..7a1e1b2daf 100644 --- a/crates/perry-runtime/src/builtins/globals.rs +++ b/crates/perry-runtime/src/builtins/globals.rs @@ -281,9 +281,26 @@ pub extern "C" fn js_decode_uri_component(value: f64) -> i64 { const ESCAPE_UNESCAPED: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./"; +/// `ToString(value)` throws a TypeError for a Symbol argument. `escape` / +/// `unescape` (and the URI family) apply ToString to their input, so a Symbol +/// must reject rather than coerce to a `"Symbol(x)"` description string. +fn throw_if_symbol(value: f64) { + if (value.to_bits() & 0xFFFF_0000_0000_0000) == crate::value::POINTER_TAG + && crate::symbol::is_registered_symbol( + (value.to_bits() & crate::value::POINTER_MASK) as usize, + ) + { + let msg = b"Cannot convert a Symbol value to a string"; + let msg_str = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_typeerror_new(msg_str); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); + } +} + /// escape(string) -> string (legacy, ES Annex B B.2.1.1) #[no_mangle] pub extern "C" fn js_escape(value: f64) -> i64 { + throw_if_symbol(value); let input = extract_str_from_nanbox(value); let mut result = String::with_capacity(input.len() * 3); let mut buf = [0u16; 2]; @@ -310,6 +327,7 @@ pub extern "C" fn js_escape(value: f64) -> i64 { /// unescape(string) -> string (legacy, ES Annex B B.2.1.2) #[no_mangle] pub extern "C" fn js_unescape(value: f64) -> i64 { + throw_if_symbol(value); let input = extract_str_from_nanbox(value); let chars: Vec = input.chars().collect(); // Reassemble into UTF-16 code units, then decode, so `%uD835%uDFD8`-style diff --git a/crates/perry-runtime/src/fs/dir_glob_watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch.rs index 4d95cb0a41..9694725813 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch.rs @@ -1522,7 +1522,10 @@ fn start_promise_watcher(id: usize, state: &mut PromiseWatchState) { if state.active || state.closed { return; } - state.snapshot = snapshot_watch_target(&state.path, state.recursive).unwrap_or_default(); + // Keep the creation-time baseline (seeded in `js_fs_promises_watch`) rather + // than re-snapshotting here — re-snapshotting would discard any events that + // occurred between `watch()` and this first `.next()` pull, which Node + // delivers (it buffers from FSWatcher creation, not from first iteration). let timer_callback = poll_closure_value(promise_watcher_poll_impl as *const u8, id); let timer_id = crate::timer::setInterval(timer_callback as i64, FS_WATCH_POLL_INTERVAL_MS); if !state.persistent { @@ -2207,11 +2210,19 @@ pub extern "C" fn js_fs_promises_watch(path_value: f64, options_value: f64) -> f Ok(signal) => signal, Err(err) => crate::exception::js_throw(err), }; - if let Err(err) = snapshot_watch_target(&path, recursive) { - unsafe { + // Capture the directory state at creation time. Node registers the + // FSWatcher synchronously in `watch()` and buffers events emitted before + // the first `.next()` pull, so a file written between `watch()` and the + // first iteration is still delivered. Perry's watcher is poll-based and + // previously took its baseline snapshot lazily at the first `.next()`, + // which silently dropped those pre-iteration events. Seed the baseline + // here so the first poll diffs against the creation-time state. + let initial_snapshot = match snapshot_watch_target(&path, recursive) { + Ok(snapshot) => snapshot, + Err(err) => unsafe { crate::exception::js_throw(build_fs_error_value(&err, "watch", &path)); - } - } + }, + }; let id = next_watch_id(); let object_value = build_promise_watcher_object(id); let abort_listener = signal @@ -2235,7 +2246,7 @@ pub extern "C" fn js_fs_promises_watch(path_value: f64, options_value: f64) -> f timer_id: 0, persistent, active: false, - snapshot: WatchSnapshot::new(), + snapshot: initial_snapshot, queue: VecDeque::new(), pending: VecDeque::new(), signal: signal_value, diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index 227ec37b67..bf8ed616b7 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -48,22 +48,17 @@ pub extern "C" fn js_promise_resolved(value: f64) -> *mut Promise { // Issue #586: ECMAScript thenable assimilation. The async-to-generator // transform rewrites every `await x` into `Promise.resolve(x).then(...)` // — which means thenable assimilation has to happen here, not in the - // codegen-side `Expr::Await` lowering. `js_assimilate_thenable` returns - // a fresh Promise wrapper that follows the thenable's `.then(resolve, - // reject)` callbacks; chain its eventual state into our outer promise - // via the same `js_promise_resolve_with_promise` pattern as the real- - // Promise arm above. Drizzle's `QueryPromise` (`then` triggers the SQL - // round-trip) is the load-bearing motivating case (#488). - let assim = js_assimilate_thenable(value); - if assim.to_bits() != value.to_bits() && js_value_is_promise(assim) != 0 { - let inner = crate::value::js_nanbox_get_pointer(assim) as *mut Promise; - if !inner.is_null() && inner != promise { - js_promise_resolve_with_promise(promise, inner); - return promise; - } - } - - js_promise_resolve(promise, value); + // codegen-side `Expr::Await` lowering. `promise_resolve_assimilating` + // implements the spec PromiseResolveThenableJob: a thenable's `.then` is + // invoked from a SCHEDULED microtask, never synchronously during resolve. + // This matters for `Promise.race`/`Promise.any` over a thenable, where Node + // does not call the thenable's `then` until the job runs (so the count of + // synchronous `then` invocations stays 0). Primitives (fast path above) and + // native Promises (short-circuit above) never reach here, so the per-await + // steady state is untouched; only real thenables (drizzle's `QueryPromise`, + // object literals with `then`) defer by one microtask — which the await + // loop drains, leaving the resolved value identical. + super::combinators::promise_resolve_assimilating(promise, value); promise } diff --git a/crates/perry-stdlib/src/events.rs b/crates/perry-stdlib/src/events.rs index 4b8f3a0eb2..208b218048 100644 --- a/crates/perry-stdlib/src/events.rs +++ b/crates/perry-stdlib/src/events.rs @@ -1615,26 +1615,97 @@ pub unsafe extern "C" fn js_events_once( promise } +// `events.on(...)` async-iterator state. Node's `on()` returns an async +// iterator that buffers emitted events and blocks `next()` until one arrives. +// The shared state lives in a GC-rooted JS array (the returned handle keeps it +// reachable) with this fixed layout: +// [0] buffer — FIFO of `[arg]` arrays awaiting consumption +// [1] pending — FIFO of `next()` Promises blocked on a future event +// [2] done — bool: iteration ended (return() / abort) +// [3] abort_reason — the AbortError to reject `next()` with, or undefined +// [4] handle — emitter handle (for listener removal on return) +// [5] listener — the queue listener closure (for removal on return) +const EVENTS_ON_BUFFER: u32 = 0; +const EVENTS_ON_PENDING: u32 = 1; +const EVENTS_ON_DONE: u32 = 2; +const EVENTS_ON_ABORT: u32 = 3; +const EVENTS_ON_HANDLE: u32 = 4; +const EVENTS_ON_LISTENER: u32 = 5; +const EVENTS_ON_ITER_SHAPE_ID: u32 = 0x7FFF_FF60; + +unsafe fn events_on_state_new() -> *mut ArrayHeader { + let state = js_array_alloc(6); + let buffer = js_array_alloc(0); + let pending = js_array_alloc(0); + let _ = js_array_push_f64(state, js_nanbox_pointer(buffer as i64)); + let _ = js_array_push_f64(state, js_nanbox_pointer(pending as i64)); + let _ = js_array_push_f64(state, TAG_FALSE_F64); + let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); + let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); + let _ = js_array_push_f64(state, f64::from_bits(TAG_UNDEFINED_F64_BITS)); + state +} + +unsafe fn events_on_state_array(state: *mut ArrayHeader, idx: u32) -> *mut ArrayHeader { + js_nanbox_get_pointer(perry_runtime::array::js_array_get_f64(state, idx)) as *mut ArrayHeader +} + +unsafe fn events_on_state_set(state: *mut ArrayHeader, idx: u32, value: f64) { + perry_runtime::array::js_array_set_f64_unchecked(state, idx, value); +} + +/// Build a `{ value, done }` iterator-result object. +fn events_iter_result(value: f64, done: bool) -> f64 { + let packed = b"value\0done\0"; + let obj = perry_runtime::object::js_object_alloc_with_shape( + EVENTS_ON_ITER_SHAPE_ID, + 2, + packed.as_ptr(), + packed.len() as u32, + ); + perry_runtime::object::js_object_set_field(obj, 0, JSValue::from_bits(value.to_bits())); + perry_runtime::object::js_object_set_field(obj, 1, JSValue::bool(done)); + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) +} + +/// A Promise already resolved with `{ value, done }`. +fn events_resolved_iter_promise(value: f64, done: bool) -> f64 { + let p = perry_runtime::promise::js_promise_resolved(events_iter_result(value, done)); + f64::from_bits(JSValue::pointer(p as *const u8).bits()) +} + +fn register_events_on_arities() { + perry_runtime::closure::js_register_closure_arity(events_on_next as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(events_on_return as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(events_on_aiter_self as *const u8, 0); + perry_runtime::closure::js_register_closure_arity(events_on_async_iterator as *const u8, 0); +} + +/// The queue listener fired for each emitted event. Resolves a blocked `next()` +/// Promise immediately if one is waiting, otherwise buffers the `[arg]` array. extern "C" fn events_on_queue_listener(closure: *const ClosureHeader, arg0: f64) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; - let queue = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - let abort_promise = js_closure_get_capture_ptr(closure, 1) as *mut Promise; - if !queue.is_null() { + let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + if state.is_null() { + return f64::from_bits(TAG_UNDEFINED_F64_BITS); + } + unsafe { let mut args = js_array_alloc(0); args = js_array_push_f64(args, arg0); let args_val = js_nanbox_pointer(args as i64); - if abort_promise.is_null() { - let _ = js_array_push_f64(queue, args_val); + + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + if !pending.is_null() && js_array_length(pending) > 0 { + let promise = js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) + as *mut Promise; + if !promise.is_null() { + js_promise_resolve(promise, events_iter_result(args_val, false)); + } } else { - let abort_val = js_nanbox_pointer(abort_promise as i64); - let len = js_array_length(queue); - if len == 0 { - let _ = js_array_push_f64(queue, args_val); - let _ = js_array_push_f64(queue, abort_val); - } else { - perry_runtime::array::js_array_set_f64_unchecked(queue, len - 1, args_val); - let _ = js_array_push_f64(queue, abort_val); + let buffer = events_on_state_array(state, EVENTS_ON_BUFFER); + if !buffer.is_null() { + let _ = js_array_push_f64(buffer, args_val); } } } @@ -1642,22 +1713,136 @@ extern "C" fn events_on_queue_listener(closure: *const ClosureHeader, arg0: f64) f64::from_bits(TAG_UNDEFINED_F64_BITS) } -extern "C" fn events_on_async_iterator(closure: *const ClosureHeader) -> f64 { +/// `next()` — drain a buffered event, reject on abort, finish when done, or +/// return a pending Promise the listener will resolve on the next event. +extern "C" fn events_on_next(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::js_closure_get_capture_ptr; + + let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + if state.is_null() { + return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); + } + unsafe { + let buffer = events_on_state_array(state, EVENTS_ON_BUFFER); + if !buffer.is_null() && js_array_length(buffer) > 0 { + let args_val = perry_runtime::array::js_array_shift_f64(buffer); + return events_resolved_iter_promise(args_val, false); + } + let abort = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_ABORT); + if abort.to_bits() != TAG_UNDEFINED_F64_BITS { + let p = js_promise_new(); + js_promise_reject(p, abort); + return f64::from_bits(JSValue::pointer(p as *const u8).bits()); + } + let done = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_DONE); + if done.to_bits() == TAG_TRUE_F64.to_bits() { + return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); + } + // No event ready yet: hand back a pending Promise; the listener resolves + // it (or the abort listener rejects it) when the next event lands. + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + let p = js_promise_new(); + if !pending.is_null() { + let _ = js_array_push_f64(pending, js_nanbox_pointer(p as i64)); + } + f64::from_bits(JSValue::pointer(p as *const u8).bits()) + } +} + +/// `return()` — end iteration: mark done, detach the listener, settle any +/// blocked `next()` with `{ done: true }`. +extern "C" fn events_on_return(closure: *const ClosureHeader) -> f64 { use perry_runtime::closure::js_closure_get_capture_ptr; - let queue = js_closure_get_capture_ptr(closure, 0); - js_nanbox_pointer(queue) + let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + if state.is_null() { + return events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true); + } + unsafe { + events_on_state_set(state, EVENTS_ON_DONE, TAG_TRUE_F64); + // Detach the queue listener from the emitter so no further events queue. + let handle = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_HANDLE); + let listener = perry_runtime::array::js_array_get_f64(state, EVENTS_ON_LISTENER); + if handle.to_bits() != TAG_UNDEFINED_F64_BITS + && listener.to_bits() != TAG_UNDEFINED_F64_BITS + { + let handle_id = handle as Handle; + let listener_ptr = js_nanbox_get_pointer(listener); + if let Some(emitter) = get_handle_mut::(handle_id) { + remove_listener_by_callback(emitter, listener_ptr); + } + } + // Resolve any blocked `next()` with completion. + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + if !pending.is_null() { + while js_array_length(pending) > 0 { + let promise = + js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) + as *mut Promise; + if !promise.is_null() { + js_promise_resolve( + promise, + events_iter_result(f64::from_bits(TAG_UNDEFINED_F64_BITS), true), + ); + } + } + } + } + events_resolved_iter_promise(f64::from_bits(TAG_UNDEFINED_F64_BITS), true) +} + +extern "C" fn events_on_aiter_self(closure: *const ClosureHeader) -> f64 { + perry_runtime::closure::js_closure_get_capture_f64(closure, 0) +} + +/// `queue[Symbol.asyncIterator]()` — build a fresh `{ next, return }` iterator +/// object bound to the shared state. +extern "C" fn events_on_async_iterator(closure: *const ClosureHeader) -> f64 { + use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; + + let state = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + register_events_on_arities(); + + let packed = b"next\0return\0"; + let obj = perry_runtime::object::js_object_alloc_with_shape( + EVENTS_ON_ITER_SHAPE_ID + 1, + 2, + packed.as_ptr(), + packed.len() as u32, + ); + let next_cl = js_closure_alloc(events_on_next as *const u8, 1); + js_closure_set_capture_ptr(next_cl, 0, state as i64); + perry_runtime::object::js_object_set_field(obj, 0, JSValue::pointer(next_cl as *const u8)); + let ret_cl = js_closure_alloc(events_on_return as *const u8, 1); + js_closure_set_capture_ptr(ret_cl, 0, state as i64); + perry_runtime::object::js_object_set_field(obj, 1, JSValue::pointer(ret_cl as *const u8)); + + let iter_val = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); + let async_iterator = perry_runtime::symbol::well_known_symbol("asyncIterator"); + if !async_iterator.is_null() { + let self_cl = js_closure_alloc(events_on_aiter_self as *const u8, 1); + perry_runtime::closure::js_closure_set_capture_f64(self_cl, 0, iter_val); + unsafe { + perry_runtime::symbol::js_object_set_symbol_property( + iter_val, + js_nanbox_pointer(async_iterator as i64), + js_nanbox_pointer(self_cl as i64), + ); + } + } + iter_val } -unsafe fn install_events_on_async_iterator(queue: *mut ArrayHeader) { +unsafe fn install_events_on_async_iterator(queue: *mut ArrayHeader, state: *mut ArrayHeader) { use perry_runtime::closure::{js_closure_alloc, js_closure_set_capture_ptr}; + register_events_on_arities(); let async_iterator = perry_runtime::symbol::well_known_symbol("asyncIterator"); if async_iterator.is_null() { return; } let closure = js_closure_alloc(events_on_async_iterator as *const u8, 1); - js_closure_set_capture_ptr(closure, 0, queue as i64); + js_closure_set_capture_ptr(closure, 0, state as i64); perry_runtime::symbol::js_object_set_symbol_property( js_nanbox_pointer(queue as i64), js_nanbox_pointer(async_iterator as i64), @@ -1671,7 +1856,7 @@ extern "C" fn events_on_abort_listener(closure: *const ClosureHeader) -> f64 { let handle = js_closure_get_capture_ptr(closure, 0) as Handle; let data_listener = js_closure_get_capture_ptr(closure, 1); let signal_ptr = js_closure_get_capture_ptr(closure, 2) as *mut ObjectHeader; - let abort_promise = js_closure_get_capture_ptr(closure, 3) as *mut Promise; + let state = js_closure_get_capture_ptr(closure, 3) as *mut ArrayHeader; let event_name_ptr = js_closure_get_capture_ptr(closure, 4) as *const StringHeader; if let Some(emitter) = get_handle_mut::(handle) { @@ -1700,8 +1885,24 @@ extern "C" fn events_on_abort_listener(closure: *const ClosureHeader) -> f64 { js_nanbox_pointer(closure as i64), ); } - if !abort_promise.is_null() { - js_promise_reject(abort_promise, perry_runtime::url::js_abort_error_value()); + // Mark the iterator aborted and reject any blocked `next()`. Buffered + // events drained before the abort still surface; only once the buffer is + // empty does `next()` observe the stored AbortError (matching Node). + if !state.is_null() { + let abort_err = perry_runtime::url::js_abort_error_value(); + events_on_state_set(state, EVENTS_ON_ABORT, abort_err); + events_on_state_set(state, EVENTS_ON_DONE, TAG_TRUE_F64); + let pending = events_on_state_array(state, EVENTS_ON_PENDING); + if !pending.is_null() { + while js_array_length(pending) > 0 { + let promise = + js_nanbox_get_pointer(perry_runtime::array::js_array_shift_f64(pending)) + as *mut Promise; + if !promise.is_null() { + js_promise_reject(promise, abort_err); + } + } + } } } @@ -1728,10 +1929,11 @@ extern "C" fn events_abort_listener_dispose(closure: *const ClosureHeader) -> f6 f64::from_bits(TAG_UNDEFINED_F64_BITS) } -/// `events.on(emitter, eventName[, options])` — returns an async-iterable queue of -/// argument arrays. Perry's `for await` lowering already accepts plain arrays -/// as async-iterable inputs, so the current implementation backs the iterator -/// with an Array and appends one `[arg]` entry per emitted event. +/// `events.on(emitter, eventName[, options])` — returns a Node-style async +/// iterator. `[Symbol.asyncIterator]()` builds a `{ next, return }` object bound +/// to shared state: emitted events are buffered as `[arg]` arrays, `next()` +/// drains the buffer (or blocks on a Promise the listener resolves on the next +/// event), and an `AbortSignal` makes a buffer-empty `next()` reject. #[no_mangle] pub unsafe extern "C" fn js_events_on( target_value: f64, @@ -1743,8 +1945,12 @@ pub unsafe extern "C" fn js_events_on( ensure_gc_scanner_registered(); let target = event_helper_target(target_value).unwrap_or_else(|| throw_invalid_emitter(target_value)); + // `queue` is the returned async-iterable handle; `state` holds the buffer / + // pending / done / abort bookkeeping and is kept alive through the handle's + // `Symbol.asyncIterator` closure capture. let queue = js_array_alloc(0); - install_events_on_async_iterator(queue); + let state = events_on_state_new(); + install_events_on_async_iterator(queue, state); let event_name = match string_from_header(event_name_ptr) { Some(name) => name, None => return queue, @@ -1753,18 +1959,9 @@ pub unsafe extern "C" fn js_events_on( if signal.is_some_and(signal_is_aborted) { perry_runtime::exception::js_throw(perry_runtime::url::js_abort_error_value()); } - let abort_promise = if signal.is_some() { - js_promise_new() - } else { - std::ptr::null_mut() - }; - let listener = js_closure_alloc(events_on_queue_listener as *const u8, 2); - js_closure_set_capture_ptr(listener, 0, queue as i64); - js_closure_set_capture_ptr(listener, 1, abort_promise as i64); - if !abort_promise.is_null() { - let _ = js_array_push_f64(queue, js_nanbox_pointer(abort_promise as i64)); - } + let listener = js_closure_alloc(events_on_queue_listener as *const u8, 1); + js_closure_set_capture_ptr(listener, 0, state as i64); let handle = match target { EventHelperTarget::EventEmitter(handle) => { @@ -1790,13 +1987,21 @@ pub unsafe extern "C" fn js_events_on( } }; + // Record the emitter handle + listener so `return()` can detach cleanly. + events_on_state_set(state, EVENTS_ON_HANDLE, handle as f64); + events_on_state_set( + state, + EVENTS_ON_LISTENER, + js_nanbox_pointer(listener as i64), + ); + if let Some(signal) = signal { if let Some(signal_ptr) = object_ptr_from_value(signal) { let abort_listener = js_closure_alloc(events_on_abort_listener as *const u8, 5); js_closure_set_capture_ptr(abort_listener, 0, handle); js_closure_set_capture_ptr(abort_listener, 1, listener as i64); js_closure_set_capture_ptr(abort_listener, 2, signal_ptr as i64); - js_closure_set_capture_ptr(abort_listener, 3, abort_promise as i64); + js_closure_set_capture_ptr(abort_listener, 3, state as i64); js_closure_set_capture_ptr(abort_listener, 4, event_name_ptr as i64); perry_runtime::url::js_abort_signal_add_listener( signal_ptr, diff --git a/scripts/check_file_size.sh b/scripts/check_file_size.sh index 1148bc7ee6..f420141c96 100755 --- a/scripts/check_file_size.sh +++ b/scripts/check_file_size.sh @@ -279,6 +279,13 @@ crates/perry-transform/src/generator/lower.rs # server `session` event after the client `connect`. Splitting the session # event pump from the handle/settings surface is tracked under #1435. crates/perry-ext-http-server/src/http2_server.rs +# node:events bundled module (EventEmitter handle surface, once/on helpers, +# AbortSignal wiring, AsyncResource). Crossed the 2000-line gate after the +# `events.on(...)` real async-iterator rewrite (proper { next, return } over a +# buffered/pending-promise queue, replacing the bare-array stub). Splitting the +# on/once iterator machinery into the existing `events/` submodule is tracked +# under #1435 with the other module-size cleanups. +crates/perry-stdlib/src/events.rs EOF )