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.
This commit is contained in:
ldm0
2026-09-13 17:02:39 +08:00
parent ddc63754b2
commit a68cc86a3e
7 changed files with 203 additions and 122 deletions
+3
View File
@@ -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,
+129
View File
@@ -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<serde_json::Value> {
let markup = if target == "worker" {
let source = serde_json::to_string(&format!(
"self.finish = value => postMessage(value);\n{source}"
))?;
format!(
r#"<!doctype html><script>
const worker = new Worker(URL.createObjectURL(new Blob([{source}], {{type: 'text/javascript'}})));
window.done = new Promise(resolve => {{
worker.onmessage = event => {{ resolve(event.data); worker.terminate(); }};
}});
worker.postMessage('go');
</script>"#
)
} else {
format!(
"<!doctype html><script>self.done = new Promise(resolve => self.finish = resolve);\n{source}</script>"
)
};
let url = markup_url(server, &markup);
let url = if target == "child" {
markup_url(
server,
&format!(
"<!doctype html><iframe id=target src=\"{}\"></iframe>",
url.replace('&', "&amp;")
),
)
} 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(())
}
+3 -3
View File
@@ -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,
@@ -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,
@@ -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,
};
@@ -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);
}
+38 -112
View File
@@ -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);