From a68cc86a3e76247cabef4e467129b1b63d0229e3 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 9 Sep 2026 17:50:18 +0800 Subject: [PATCH] fix(events): preserve native dispatch state and listener lifetime Set and clear child Window event dispatch fields, and clear Worker propagation flags after native delivery. Remove once listeners before invocation and skip listeners removed by an earlier callback, including during synchronous nested dispatch. Add Window, child Window, and Worker regression probes for active event state, redispatch after stopImmediatePropagation, and once/listener removal during nested dispatch. --- moli-core/tests/web_apis.rs | 3 + moli-core/tests/web_apis/event_dispatch.rs | 129 +++++++++++++++ moli-renderer-v8/src/context_bootstrap.rs | 6 +- .../src/context_bootstrap/media_queries.rs | 6 +- .../context_bootstrap/media_queries/events.rs | 6 +- .../context_host/child_events.rs | 25 ++- .../src/worker/thread/dispatch.rs | 150 +++++------------- 7 files changed, 203 insertions(+), 122 deletions(-) create mode 100644 moli-core/tests/web_apis/event_dispatch.rs diff --git a/moli-core/tests/web_apis.rs b/moli-core/tests/web_apis.rs index f97dad368c..5619670e62 100644 --- a/moli-core/tests/web_apis.rs +++ b/moli-core/tests/web_apis.rs @@ -10,6 +10,9 @@ use moli_fetch::FetchConfig; use support::FixtureServer; use tokio::time::Duration; +#[path = "web_apis/event_dispatch.rs"] +mod event_dispatch; + fn diagnostic_global<'a>( page: &'a moli_core::page::Page, name: &str, diff --git a/moli-core/tests/web_apis/event_dispatch.rs b/moli-core/tests/web_apis/event_dispatch.rs new file mode 100644 index 0000000000..4dc61ea1aa --- /dev/null +++ b/moli-core/tests/web_apis/event_dispatch.rs @@ -0,0 +1,129 @@ +use super::*; + +fn markup_url(server: &FixtureServer, markup: &str) -> String { + let mut url = url::Url::parse(&server.url("/compat/child-dynamic-markup-document")).unwrap(); + url.query_pairs_mut().append_pair("markup", markup); + url.into() +} + +pub(super) async fn run_probe( + browser: &Browser, + server: &FixtureServer, + target: &str, + source: &str, +) -> Result { + let markup = if target == "worker" { + let source = serde_json::to_string(&format!( + "self.finish = value => postMessage(value);\n{source}" + ))?; + format!( + r#""# + ) + } else { + format!( + "" + ) + }; + let url = markup_url(server, &markup); + let url = if target == "child" { + markup_url( + server, + &format!( + "", + url.replace('&', "&") + ), + ) + } else { + url + }; + let mut page = browser.fetch(&url).await?; + let expression = if target == "child" { + "document.getElementById('target').contentWindow.done.then(JSON.stringify)" + } else { + "done.then(JSON.stringify)" + }; + let value = page + .evaluate_runtime_expression_with_await_async(expression, true) + .await?; + Ok(serde_json::from_str( + value["value"].as_str().expect("callback probe result"), + )?) +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_message_events_keep_dispatch_state_and_clear_propagation_flags() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let source = r#" + const log = []; + addEventListener('message', event => { + log.push(['during', event.currentTarget === self, event.eventPhase === 2, + typeof document === 'undefined' || window.event === event]); + event.initEvent('mutated', true, true); + log.push(event.type); + event.stopImmediatePropagation(); + setTimeout(() => { + log.push(['after', event.currentTarget === null, event.eventPhase === 0, + typeof document === 'undefined' || window.event === undefined]); + dispatchEvent(event); + finish(log); + }, 0); + }, {once: true}); + addEventListener('message', () => log.push('second')); + if (typeof document !== 'undefined') postMessage('go', '*'); + "#; + for target in ["window", "child", "worker"] { + let observed = run_probe(&browser, &server, target, source).await?; + assert_eq!( + observed, + serde_json::json!([ + ["during", true, true, true], + "message", + ["after", true, true, true], + "second" + ]), + "target={target}" + ); + } + server.shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_message_listeners_observe_once_and_removal_during_nested_dispatch() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let source = r#" + const log = []; + let calls = 0; + const removed = () => log.push('unexpected removed listener'); + addEventListener('message', () => { + log.push('once'); + if (++calls > 1) return; + removeEventListener('message', removed); + dispatchEvent(new Event('message')); + }, {once: true}); + addEventListener('message', removed); + addEventListener('message', () => { + log.push('retained'); + setTimeout(() => finish(log), 0); + }); + if (typeof document !== 'undefined') postMessage('go', '*'); + "#; + for target in ["window", "child", "worker"] { + let observed = run_probe(&browser, &server, target, source).await?; + assert_eq!( + observed, + serde_json::json!(["once", "retained", "retained"]), + "target={target}" + ); + } + server.shutdown().await; + Ok(()) +} diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index ae944cee1f..f3dff1d7cd 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -311,9 +311,9 @@ pub(crate) use self::media_queries::{ mark_simple_event_target_slot, simple_event_target_add_event_listener_callback, simple_event_target_dispatch_event_callback, simple_event_target_inspector_listener_snapshots, simple_event_target_remove_event_listener_callback, simple_event_target_slot_name, - simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, - simple_object_event_set_ordered_handler, simple_object_event_target_add_listener, - simple_object_event_target_remove_listener, + simple_object_event_listener_is_registered, simple_object_event_listeners_snapshot, + simple_object_event_remove_listener_value_for_type, simple_object_event_set_ordered_handler, + simple_object_event_target_add_listener, simple_object_event_target_remove_listener, }; pub(crate) use self::message_ports::{ MessagePortDeliveryRunResult, MessagePortEventListenerId, MessagePortEventListenerSnapshot, diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries.rs b/moli-renderer-v8/src/context_bootstrap/media_queries.rs index 68e07773c1..24834eeff0 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries.rs @@ -14,9 +14,9 @@ pub(crate) use self::events::{ mark_simple_event_target_slot, simple_event_target_add_event_listener_callback, simple_event_target_dispatch_event_callback, simple_event_target_inspector_listener_snapshots, simple_event_target_remove_event_listener_callback, simple_event_target_slot_name, - simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, - simple_object_event_set_ordered_handler, simple_object_event_target_add_listener, - simple_object_event_target_remove_listener, + simple_object_event_listener_is_registered, simple_object_event_listeners_snapshot, + simple_object_event_remove_listener_value_for_type, simple_object_event_set_ordered_handler, + simple_object_event_target_add_listener, simple_object_event_target_remove_listener, }; pub(super) use self::events::{ media_query_list_add_event_listener_callback, media_query_list_add_listener_callback, diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs index f00a16faa4..60206bd71a 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs @@ -20,8 +20,8 @@ pub(crate) use simple_event_target::{ mark_simple_event_target_slot, simple_event_target_add_event_listener_callback, simple_event_target_dispatch_event_callback, simple_event_target_inspector_listener_snapshots, simple_event_target_remove_event_listener_callback, simple_event_target_slot_name, - simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, - simple_object_event_set_ordered_handler, simple_object_event_target_add_listener, - simple_object_event_target_register_webidl_listener, + simple_object_event_listener_is_registered, simple_object_event_listeners_snapshot, + simple_object_event_remove_listener_value_for_type, simple_object_event_set_ordered_handler, + simple_object_event_target_add_listener, simple_object_event_target_register_webidl_listener, simple_object_event_target_remove_listener, }; diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs index d6ec3d8948..a52c5acf9f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs @@ -2,7 +2,11 @@ use super::{ JsContextHost, OwnerDispatchScope, child_frame_runtime::WINDOW_EVENT_HANDLER_PROPERTIES, }; use crate::{ - context_bootstrap::{EventHandlerType, apply_event_handler_return_value, event_is_error_event}, + context_bootstrap::{ + EVENT_DISPATCHING_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, EVENT_STOP_PROPAGATION_SLOT, + EventHandlerType, apply_event_handler_return_value, event_is_error_event, + set_event_internal_flag, + }, document_runtime::DomHandle, document_runtime::EventTargetHandle, exception_reporting::invoke_event_handler, @@ -497,6 +501,12 @@ impl JsContextHost { }; let _ = event.set(scope, v8str(scope, "target").into(), target); let _ = event.set(scope, v8str(scope, "currentTarget").into(), window.into()); + let _ = event.set( + scope, + v8str(scope, "eventPhase").into(), + v8::Integer::new(scope, 2).into(), + ); + set_event_internal_flag(scope, event, EVENT_DISPATCHING_SLOT, true); if event_type == "load" { install_child_body_load_attribute_handler_if_needed(scope, self, handle); @@ -545,6 +555,19 @@ impl JsContextHost { break; } } + let _ = event.set( + scope, + v8str(scope, "eventPhase").into(), + v8::Integer::new(scope, 0).into(), + ); + let _ = event.set( + scope, + v8str(scope, "currentTarget").into(), + v8::null(scope).into(), + ); + set_event_internal_flag(scope, event, EVENT_DISPATCHING_SLOT, false); + set_event_internal_flag(scope, event, EVENT_STOP_PROPAGATION_SLOT, false); + set_event_internal_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, false); self.pop_child_subresource_request_scope(); restore_child_window_event_dispatch(scope, previous_active_child_window); } diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index 718a83fb90..4cad04aade 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -16,8 +16,8 @@ use crate::context_bootstrap::{ dispatch_message_port_events_for_port_collecting_errors, dispatch_service_worker_controller_change, ensure_message_port_wrapper_for_id, event_internal_bool_flag, mark_event_trusted, runtime_message_allowed_for_current_target, - set_event_internal_flag, simple_object_event_listeners_snapshot, - simple_object_event_remove_listener_value_for_type, + set_event_internal_flag, simple_object_event_listener_is_registered, + simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, structured_deserialize_value_for_message_event, }; use crate::exception_reporting::{ @@ -695,6 +695,8 @@ fn clear_event_dispatch_fields(scope: &mut v8::PinScope<'_, '_>, event: v8::Loca EVENT_DISPATCHING_SLOT, v8::Boolean::new(scope, false).into(), ); + set_event_internal_flag(scope, event, EVENT_STOP_PROPAGATION_SLOT, false); + set_event_internal_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, false); } fn worker_event_prevent_default_callback( @@ -859,6 +861,7 @@ fn dispatch_worker_promise_rejection_event<'s>( listener, global, event, + event_type, "WorkerGlobalScope promise rejection listener", ) { let (report, exception) = *error; @@ -873,16 +876,6 @@ fn dispatch_worker_promise_rejection_event<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } let allows_default = !event_bool_property(scope, event, "defaultPrevented"); @@ -997,8 +990,31 @@ fn invoke_worker_listener<'s>( listener: &SimpleObjectEventListenerSnapshot<'s>, target: v8::Local<'s, v8::Object>, event: v8::Local<'s, v8::Object>, + event_type: &str, callback_name: &str, ) -> Result<(), WorkerExceptionError> { + if event_stop_immediate_propagation(scope, event) + || !simple_object_event_listener_is_registered( + scope, + target, + WORKER_GLOBAL_LISTENERS_SLOT, + event_type, + listener.original, + listener.capture, + ) + { + return Ok(()); + } + if listener.once { + simple_object_event_remove_listener_value_for_type( + scope, + target, + WORKER_GLOBAL_LISTENERS_SLOT, + event_type, + listener.original, + listener.capture, + ); + } let arguments = [event.into()]; let invocation = listener.invocation(target.into(), &arguments, Some(event)); match CallbackInvoker::invoke( @@ -1088,6 +1104,7 @@ pub(super) fn dispatch_worker_error_event<'s>( listener, global, event, + "error", "WorkerGlobalScope error listener", ) { let (nested_report, nested_exception) = *nested_report; @@ -1099,16 +1116,6 @@ pub(super) fn dispatch_worker_error_event<'s>( parent_tx, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "error", - listener.original, - listener.capture, - ); - } } let handled = event_bool_property(scope, event, "defaultPrevented"); @@ -1332,6 +1339,7 @@ fn dispatch_service_worker_lifecycle_event_in_context<'s>( listener, global, event_object, + event_type, "ServiceWorkerGlobalScope lifecycle listener", ) { let (report, exception) = *error; @@ -1346,16 +1354,6 @@ fn dispatch_service_worker_lifecycle_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1458,6 +1456,7 @@ fn dispatch_service_worker_fetch_event_in_context<'s>( listener, global, event_object, + "fetch", "ServiceWorkerGlobalScope fetch listener", ) { let (report, exception) = *error; @@ -1472,16 +1471,6 @@ fn dispatch_service_worker_fetch_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "fetch", - listener.original, - listener.capture, - ); - } if event_stop_immediate_propagation(scope, event_object) { break; } @@ -1549,6 +1538,7 @@ fn dispatch_service_worker_message_event_in_context<'s>( listener, global, event_object, + event_type, "ServiceWorkerGlobalScope message listener", ) { let (report, exception) = *error; @@ -1563,16 +1553,6 @@ fn dispatch_service_worker_message_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1636,6 +1616,7 @@ fn dispatch_service_worker_notification_event_in_context<'s>( listener, global, event_object, + event_type, "ServiceWorkerGlobalScope notification listener", ) { let (report, exception) = *error; @@ -1650,16 +1631,6 @@ fn dispatch_service_worker_notification_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1711,6 +1682,7 @@ fn dispatch_service_worker_push_event_in_context<'s>( listener, global, event_object, + "push", "ServiceWorkerGlobalScope push listener", ) { let (report, exception) = *error; @@ -1725,16 +1697,6 @@ fn dispatch_service_worker_push_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "push", - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1790,6 +1752,7 @@ fn dispatch_service_worker_sync_event_in_context<'s>( listener, global, event_object, + "sync", "ServiceWorkerGlobalScope sync listener", ) { let (report, exception) = *error; @@ -1804,16 +1767,6 @@ fn dispatch_service_worker_sync_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "sync", - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1873,6 +1826,7 @@ fn dispatch_service_worker_periodic_sync_event_in_context<'s>( listener, global, event_object, + "periodicsync", "ServiceWorkerGlobalScope periodicsync listener", ) { let (report, exception) = *error; @@ -1887,16 +1841,6 @@ fn dispatch_service_worker_periodic_sync_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "periodicsync", - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -4403,6 +4347,7 @@ fn dispatch_worker_global_message_event<'s>( listener, global, event, + event_type, "WorkerGlobalScope message listener", ) { let (report, exception) = *error; @@ -4417,16 +4362,6 @@ fn dispatch_worker_global_message_event<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } clear_event_dispatch_fields(scope, event); @@ -4520,6 +4455,7 @@ pub(super) fn dispatch_shared_worker_connect_event( listener, global, event, + "connect", "SharedWorkerGlobalScope connect listener", ) { let (report, exception) = *error; @@ -4534,16 +4470,6 @@ pub(super) fn dispatch_shared_worker_connect_event( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "connect", - listener.original, - listener.capture, - ); - } } clear_event_dispatch_fields(scope, event);