diff --git a/moli-renderer-v8/src/abort_signal_route.rs b/moli-renderer-v8/src/abort_signal_route.rs index 04f168f142..9195ab2a59 100644 --- a/moli-renderer-v8/src/abort_signal_route.rs +++ b/moli-renderer-v8/src/abort_signal_route.rs @@ -183,9 +183,6 @@ pub(crate) fn event_listener_signal_from_options_value<'s>( return Some(None); }; let signal_key = v8str(scope, "signal"); - if !options.has(scope, signal_key.into()).unwrap_or(false) { - return Some(None); - } let signal_value = options.get(scope, signal_key.into())?; if signal_value.is_undefined() { return Some(None); diff --git a/moli-renderer-v8/src/callback_invocation.rs b/moli-renderer-v8/src/callback_invocation.rs index 1b944327c5..8ebbef29e9 100644 --- a/moli-renderer-v8/src/callback_invocation.rs +++ b/moli-renderer-v8/src/callback_invocation.rs @@ -133,6 +133,7 @@ pub(crate) struct CallbackInvocation<'s, 'a> { relevant_identity: Option, host_ptr: Option<*mut JsContextHost>, is_callable: bool, + legacy_event_handler: bool, operation_name: &'a str, arguments: &'a [v8::Local<'s, v8::Value>], current_event: Option>, @@ -158,12 +159,20 @@ impl<'s, 'a> CallbackInvocation<'s, 'a> { relevant_identity: None, host_ptr: None, is_callable, + legacy_event_handler: false, operation_name, arguments, current_event, } } + /// HTML event handlers retain non-callable objects but do not look up + /// callback-interface operations or enter their realm when invoked. + pub(crate) fn with_legacy_event_handler(mut self) -> Self { + self.legacy_event_handler = true; + self + } + pub(crate) fn with_execution_context_currentness( mut self, host_ptr: *mut JsContextHost, @@ -195,6 +204,10 @@ impl CallbackInvoker { if let Some(host_ptr) = invocation.host_ptr { unsafe { &*host_ptr }.debug_assert_not_in_structural_mutation("callback invocation"); } + if invocation.legacy_event_handler && !invocation.is_callable { + let value: v8::Local = v8::undefined(scope).into(); + return CallbackInvocationOutcome::Returned(v8::Global::new(scope, value)); + } if let (Some(host_ptr), Some(identity)) = (invocation.host_ptr, invocation.relevant_identity) && !unsafe { &*host_ptr }.window_execution_context_identity_is_current(identity) diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 35e6bb0495..7d56361e53 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -1,3 +1,4 @@ +pub(crate) mod abort_signal_events; mod animation_runtime; mod assets; pub(crate) mod bridge_descriptor; @@ -12,6 +13,7 @@ pub(crate) mod css_stylesheet_runtime; mod dom_rect; mod event_document; mod event_legacy; +pub(crate) mod event_target_dispatch; mod event_template; mod events; pub(crate) mod exposed_interfaces; @@ -698,11 +700,11 @@ pub(crate) fn install_worker_base64_runtime_state<'s>( } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::AbortSignal, enumerable)] +#[webapi(interface = web_api_interfaces::AbortSignal, enumerable, receiver)] struct WorkerAbortSignalTemplateDeclaration { #[webapi( static_method = "abort", - length = 1, + length = 0, callback = crate::worker::abort::worker_abort_signal_static_abort_callback )] abort_static: (), @@ -721,27 +723,6 @@ struct WorkerAbortSignalTemplateDeclaration { )] any: (), - #[webapi( - method = "addEventListener", - length = 2, - callback = crate::worker::abort::worker_abort_signal_add_event_listener_callback - )] - add_event_listener: (), - - #[webapi( - method = "removeEventListener", - length = 2, - callback = crate::worker::abort::worker_abort_signal_remove_event_listener_callback - )] - remove_event_listener: (), - - #[webapi( - method = "dispatchEvent", - length = 1, - callback = crate::worker::abort::worker_abort_signal_dispatch_event_callback - )] - dispatch_event: (), - #[webapi( method = "throwIfAborted", length = 0, @@ -757,8 +738,8 @@ struct WorkerAbortSignalTemplateDeclaration { #[webapi( accessor_property, - getter = crate::worker::abort::worker_abort_signal_onabort_getter_function, - setter = crate::worker::abort::worker_abort_signal_onabort_setter_function + getter = abort_signal_events::onabort_getter, + setter = abort_signal_events::onabort_setter )] onabort: (), } diff --git a/moli-renderer-v8/src/context_bootstrap/abort_signal_events.rs b/moli-renderer-v8/src/context_bootstrap/abort_signal_events.rs new file mode 100644 index 0000000000..6d7048431d --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/abort_signal_events.rs @@ -0,0 +1,65 @@ +use super::{ + construct_original_event, dispatch_simple_event_target_event, + install_simple_event_target_ordered_handlers, mark_simple_event_target_slot, + simple_object_event_set_ordered_handler, +}; +use crate::util::{get_private_value, set_private_value}; + +const LISTENERS_SLOT: &str = "__moliAbortSignalListeners"; +const ONABORT_SLOT: &str = "__moliAbortSignalOnabort"; + +// Abort state and algorithms remain in their Window/worker owner. The signal's +// EventTarget state uses the same registry for inherited methods and native abort. +pub(crate) fn initialize<'s>(scope: &mut v8::PinScope<'s, '_>, signal: v8::Local<'s, v8::Object>) { + mark_simple_event_target_slot(scope, signal, LISTENERS_SLOT); + install_simple_event_target_ordered_handlers(scope, signal); + set_private_value(scope, signal, ONABORT_SLOT, v8::null(scope).into()); +} + +pub(crate) fn dispatch_abort<'s>( + scope: &mut v8::PinScope<'s, '_>, + signal: v8::Local<'s, v8::Object>, +) { + let context = signal + .get_creation_context(scope) + .unwrap_or_else(|| scope.get_current_context()); + let scope = &mut v8::ContextScope::new(scope, context); + if let Some(event) = construct_original_event(scope, "abort") { + super::mark_event_trusted(scope, event); + dispatch_simple_event_target_event(scope, signal, LISTENERS_SLOT, "abort", event); + } +} + +pub(crate) fn onabort_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + rv.set( + get_private_value(scope, args.this(), ONABORT_SLOT) + .unwrap_or_else(|| v8::null(scope).into()), + ); +} + +pub(crate) fn onabort_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let active = args.get(0).is_object(); + let value = if active { + args.get(0) + } else { + v8::null(scope).into() + }; + let signal = args.this(); + set_private_value(scope, signal, ONABORT_SLOT, value); + simple_object_event_set_ordered_handler( + scope, + signal, + LISTENERS_SLOT, + "abort", + ONABORT_SLOT, + active, + ); +} diff --git a/moli-renderer-v8/src/context_bootstrap/event_target_dispatch.rs b/moli-renderer-v8/src/context_bootstrap/event_target_dispatch.rs new file mode 100644 index 0000000000..7f1e335009 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/event_target_dispatch.rs @@ -0,0 +1,89 @@ +use super::events::{clear_event_dispatch_fields, set_event_dispatch_fields}; +use super::{ + EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, EVENT_STOP_PROPAGATION_SLOT, + clear_event_composed_path, event_initialized, event_internal_bool_flag, event_is_dispatching, + new_dom_exception_value, set_event_composed_path, set_event_internal_flag, set_event_trusted, +}; +use crate::util::{throw_type_error, v8str}; + +pub(crate) fn prepare_script_dispatch<'s>( + scope: &mut v8::PinScope<'s, '_>, + target: v8::Local<'s, v8::Object>, + value: v8::Local<'s, v8::Value>, +) -> Option<(v8::Local<'s, v8::Object>, String)> { + let event = v8::Local::::try_from(value).ok(); + let Some((event, initialized)) = + event.and_then(|event| event_initialized(scope, event).map(|flag| (event, flag))) + else { + throw_type_error(scope, "EventTarget.dispatchEvent requires an Event."); + return None; + }; + if !initialized || event_is_dispatching(scope, event) { + let error = new_dom_exception_value( + scope, + "The event is uninitialized or already being dispatched.", + "InvalidStateError", + ); + scope.throw_exception(error); + return None; + } + if !target_execution_context_is_live(scope, target) { + return None; + } + set_event_trusted(scope, event, false); + let event_type = event + .get(scope, v8str(scope, "type").into())? + .to_string(scope)? + .to_rust_string_lossy(scope); + Some((event, event_type)) +} + +pub(crate) fn target_execution_context_is_live<'s>( + scope: &mut v8::PinScope<'s, '_>, + target: v8::Local<'s, v8::Object>, +) -> bool { + let Some(context) = target.get_creation_context(scope) else { + return true; + }; + if context + .get_slot::() + .is_none() + { + return true; + } + let Some(host_ptr) = crate::util::context_host_ptr_from_global_bridge(scope) else { + return true; + }; + // A borrowed method still dispatches on the receiver's relevant realm. + // Retained objects must not resume execution after that realm is retired. + let host = unsafe { &*host_ptr }; + host.window_execution_context_identity_for_v8_context(scope, context) + .is_some_and(|identity| host.window_execution_context_identity_is_current(identity)) +} + +// Even a pre-stopped event needs finish_dispatch, but its listeners must not run. +pub(crate) fn begin_dispatch<'s>( + scope: &mut v8::PinScope<'s, '_>, + target: v8::Local<'s, v8::Object>, + event: v8::Local<'s, v8::Object>, +) -> bool { + set_event_dispatch_fields(scope, target, event); + let path = v8::Array::new_with_elements(scope, &[target.into()]); + set_event_composed_path(scope, event, path); + !event_internal_bool_flag(scope, event, EVENT_STOP_PROPAGATION_SLOT) +} + +pub(crate) fn finish_dispatch<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, +) { + clear_event_dispatch_fields(scope, event); + clear_event_composed_path(scope, event); + for flag in [ + EVENT_STOP_PROPAGATION_SLOT, + EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, + EVENT_PASSIVE_SLOT, + ] { + set_event_internal_flag(scope, event, flag, false); + } +} diff --git a/moli-renderer-v8/src/context_bootstrap/events.rs b/moli-renderer-v8/src/context_bootstrap/events.rs index d63004b239..f146429afb 100644 --- a/moli-renderer-v8/src/context_bootstrap/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/events.rs @@ -345,7 +345,7 @@ pub(super) use methods::{ event_composed_path_callback, event_prevent_default_callback, event_return_value_getter_function, event_return_value_setter_function, event_stop_immediate_propagation_callback, event_stop_propagation_callback, - event_time_stamp_getter_function, + event_time_stamp_getter_function, set_event_default_prevented, }; pub(in crate::context_bootstrap) use subclasses::run_navigate_event_precommit_handlers; pub(super) use subclasses::{ diff --git a/moli-renderer-v8/src/context_bootstrap/events/methods.rs b/moli-renderer-v8/src/context_bootstrap/events/methods.rs index f5c35deddf..64fa35cb86 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/methods.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/methods.rs @@ -1,6 +1,6 @@ use super::*; -pub(super) fn set_event_default_prevented( +pub(in crate::context_bootstrap) fn set_event_default_prevented( scope: &mut v8::PinScope<'_, '_>, event: v8::Local<'_, v8::Object>, ) { diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target.rs index 2442f991ec..4d000c5529 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target.rs @@ -19,9 +19,8 @@ pub(crate) use install::{ pub(in crate::context_bootstrap::media_queries::events::simple_event_target) use listeners::simple_event_target_uses_ordered_handlers; pub(crate) use listeners::{ SimpleObjectEventListenerInspectorSnapshot, SimpleObjectEventListenerSnapshot, - simple_event_target_inspector_listener_snapshots, 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_event_target_inspector_listener_snapshots, simple_object_event_listeners_snapshot, + simple_object_event_remove_listener_value_for_type, simple_object_event_set_ordered_handler, }; pub(crate) use listeners::{ simple_object_event_target_add_listener, simple_object_event_target_register_webidl_listener, diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs index 3f93bfac39..ebc8d4f052 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs @@ -2,13 +2,12 @@ use super::*; use crate::{ callback_invocation::{CallbackInvocation, CallbackInvocationOutcome, CallbackInvoker}, context_bootstrap::events::{ - EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, clear_event_composed_path, - clear_event_dispatch_fields, event_internal_bool_flag, set_event_composed_path, - set_event_dispatch_fields, set_event_internal_flag, + EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, event_internal_bool_flag, + set_event_default_prevented, set_event_internal_flag, }, exception_reporting::CallbackExceptionLogLevel, host::report_event_callback_exception, - util::{context_host_ptr_from_global_bridge, serialize_v8_array}, + util::context_host_ptr_from_global_bridge, }; fn event_stop_immediate_propagation<'s>( @@ -26,26 +25,14 @@ pub(in crate::context_bootstrap::media_queries::events::simple_event_target) fn slot_name: &str, rv: &mut v8::ReturnValue<'s, v8::Value>, ) { - let event_value = args.get(0); - if !event_value.is_object() || event_value.is_function() { - throw_type_error( + let Some((event, event_type)) = + crate::context_bootstrap::event_target_dispatch::prepare_script_dispatch( scope, - "Failed to execute 'dispatchEvent': parameter 1 is not an object.", - ); - return; - } - let Ok(event) = v8::Local::::try_from(event_value) else { - throw_type_error( - scope, - "Failed to execute 'dispatchEvent': parameter 1 is not an object.", - ); - return; - }; - let Some(event_type) = object_string_property_defined(scope, event, "type") else { - throw_type_error( - scope, - "Failed to execute 'dispatchEvent': event type is required.", - ); + args.this(), + args.get(0), + ) + else { + rv.set_bool(false); return; }; @@ -62,11 +49,10 @@ pub(crate) fn dispatch_simple_event_target_event<'s>( event_type: &str, event: v8::Local<'s, v8::Object>, ) -> bool { - set_event_dispatch_fields(scope, target, event); - let path = serialize_v8_array(scope, [target]).unwrap_or_else(|| v8::Array::new(scope, 1)); - set_event_composed_path(scope, event, path); + let can_invoke = + crate::context_bootstrap::event_target_dispatch::begin_dispatch(scope, target, event); - if !simple_event_target_uses_ordered_handlers(scope, target) { + if can_invoke && !simple_event_target_uses_ordered_handlers(scope, target) { let handler_name = format!("on{event_type}"); if let Some(handler_key) = v8_string(scope, &handler_name) && let Some(handler_value) = target.get(scope, handler_key.into()) @@ -78,7 +64,7 @@ pub(crate) fn dispatch_simple_event_target_event<'s>( .get_creation_context(scope) .unwrap_or(current_context); let incumbent_context = scope.get_incumbent_context().unwrap_or(current_context); - let _ = invoke_simple_event_callback( + let returned = invoke_simple_event_callback( scope, event_type, &format!("simple event target {handler_name}"), @@ -90,57 +76,61 @@ pub(crate) fn dispatch_simple_event_target_event<'s>( &[event.into()], event, ); + if let Some(returned) = returned { + apply_handler_return_value(scope, event, v8::Local::new(scope, &returned)); + } } } - if !event_stop_immediate_propagation(scope, event) { - let listeners = - simple_object_event_listeners_snapshot(scope, target, slot_name, event_type); + if !event_internal_bool_flag( + scope, + event, + crate::context_bootstrap::EVENT_STOP_PROPAGATION_SLOT, + ) { 'phases: for capture_phase in [true, false] { + // Capture listeners may add listeners for the subsequent bubble phase. + let listeners = + simple_object_event_listeners_snapshot(scope, target, slot_name, event_type); for listener in listeners .iter() .filter(|listener| listener.capture == capture_phase) { - if !simple_object_event_listener_is_registered( - scope, - target, - slot_name, - event_type, - listener.original, - listener.capture, - ) { + let Some(listener) = + listener.prepare_for_invocation(scope, target, slot_name, event_type) + else { continue; - } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - target, - slot_name, - event_type, - listener.original, - listener.capture, - ); - } + }; set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, listener.passive); - let _ = invoke_simple_event_listener( + let returned = invoke_simple_event_listener( scope, event_type, &format!("simple event target {event_type} listener"), - listener, + &listener, target.into(), &[event.into()], event, ); + if listener.handler_slot.is_some() + && let Some(returned) = returned + { + apply_handler_return_value(scope, event, v8::Local::new(scope, &returned)); + } set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, false); if event_stop_immediate_propagation(scope, event) { break 'phases; } } + if event_internal_bool_flag( + scope, + event, + crate::context_bootstrap::EVENT_STOP_PROPAGATION_SLOT, + ) { + break; + } } } - clear_event_dispatch_fields(scope, event); - clear_event_composed_path(scope, event); + crate::context_bootstrap::event_target_dispatch::finish_dispatch(scope, event); let default_prevented = object_bool_property(scope, event, "defaultPrevented").unwrap_or(false); !default_prevented } @@ -257,9 +247,24 @@ fn invoke_simple_event_callback_with_invocation<'s>( None, &report, ); + } else { + let _ = crate::worker::dispatch_current_worker_callback_exception(scope, *report); } None } CallbackInvocationOutcome::Retired => None, } } + +fn apply_handler_return_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + returned: v8::Local<'s, v8::Value>, +) { + if returned.is_false() + && object_bool_property(scope, event, "cancelable").unwrap_or(false) + && !event_internal_bool_flag(scope, event, EVENT_PASSIVE_SLOT) + { + set_event_default_prevented(scope, event); + } +} diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs index 7a91bceed9..4a5415c04c 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/listeners.rs @@ -1,6 +1,7 @@ use super::*; -use crate::abort_signal_route::{ResolvedAbortSignal, event_listener_signal_from_options_value}; +use crate::abort_signal_route::ResolvedAbortSignal; use crate::callback_invocation::CallbackInvocation; +use crate::event_listener_args::{AddEventListenerArgs, RemoveEventListenerArgs}; use crate::util::{ get_private_object, get_private_value, new_null_prototype_object, set_private_value, v8_string, }; @@ -79,7 +80,9 @@ struct SimpleObjectEventHandlerEntryDeclaration<'scope> { callable: bool, } +#[derive(Clone)] pub(crate) struct SimpleObjectEventListenerSnapshot<'s> { + entry: v8::Local<'s, v8::Object>, pub(crate) original: v8::Local<'s, v8::Value>, callback: v8::Local<'s, v8::Object>, relevant_context: v8::Local<'s, v8::Context>, @@ -111,13 +114,47 @@ struct SimpleObjectResolvedEventListener<'s> { } impl<'s> SimpleObjectEventListenerSnapshot<'s> { + pub(crate) fn prepare_for_invocation( + &self, + scope: &mut v8::PinScope<'s, '_>, + target: v8::Local<'s, v8::Object>, + slot_name: &str, + event_type: &str, + ) -> Option { + // Removing and re-adding the same callback creates a different entry. + // The removed entry must stay inactive in an existing dispatch snapshot. + if !simple_object_event_listener_entry_registered( + scope, target, slot_name, event_type, self.entry, + ) { + return None; + } + let listener = if self.handler_slot.is_some() { + // An active handler keeps its registration position, but its value + // and callback contexts can change before dispatch reaches it. + simple_object_event_listener_snapshot_entry(scope, self.entry.into())? + } else { + self.clone() + }; + if listener.once { + simple_object_event_remove_listener_value_for_type( + scope, + target, + slot_name, + event_type, + listener.original, + listener.capture, + ); + } + Some(listener) + } + pub(crate) fn invocation<'a>( &self, callback_this: v8::Local<'s, v8::Value>, arguments: &'a [v8::Local<'s, v8::Value>], current_event: Option>, ) -> CallbackInvocation<'s, 'a> { - CallbackInvocation::new( + let invocation = CallbackInvocation::new( self.callback, callback_this, self.relevant_context, @@ -126,7 +163,12 @@ impl<'s> SimpleObjectEventListenerSnapshot<'s> { "handleEvent", arguments, current_event, - ) + ); + if self.handler_slot.is_some() { + invocation.with_legacy_event_handler() + } else { + invocation + } } pub(crate) fn relevant_context(&self) -> v8::Local<'s, v8::Context> { @@ -139,117 +181,27 @@ impl<'s> SimpleObjectEventListenerSnapshot<'s> { } } -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "EventTarget.addEventListener")] -struct SimpleObjectAddListenerArgs<'s> { - #[webidl(with = simple_object_add_listener_call)] - call: webidl::ParseOutcome>, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "EventTarget.removeEventListener")] -struct SimpleObjectRemoveListenerArgs<'s> { - #[webidl(with = simple_object_remove_listener_call)] - call: webidl::ParseOutcome>, -} - -struct SimpleObjectAddListenerCall<'s> { - event_type: String, - listener: SimpleObjectResolvedEventListener<'s>, - options: webidl::EventListenerOptions, - signal: Option>, -} - -struct SimpleObjectRemoveListenerCall<'s> { - event_type: String, - listener: v8::Local<'s, v8::Value>, - options: webidl::EventListenerOptions, -} - -fn required_simple_object_event_type<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - prefix: &'static str, - missing_message: &'static str, -) -> Result { - if args.length() == 0 { - return Err(webidl::WebIdlError::custom_message(missing_message)); - } - webidl::convert::(scope, args.get(0), webidl::Context::argument(prefix, 1)) - .map(Into::into) -} - -fn simple_object_add_listener_call<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - _index: i32, -) -> Result>, webidl::WebIdlError> { - let event_type = required_simple_object_event_type( - scope, - args, - "EventTarget.addEventListener", - "Failed to execute 'addEventListener' on 'EventTarget': 1 argument required, but only 0 present.", - )?; - let options = webidl::event_listener_options(scope, args, 2, true); - let Some(signal) = event_listener_signal_from_options_value(scope, args.get(2)) else { - return Ok(webidl::ParseOutcome::Skip); - }; - let Some(listener) = simple_object_event_listener_parts(scope, args.get(1)) else { - return Ok(webidl::ParseOutcome::Skip); - }; - Ok(webidl::ParseOutcome::Parsed(SimpleObjectAddListenerCall { - event_type, - listener, - options, - signal, - })) -} - -fn simple_object_remove_listener_call<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - _index: i32, -) -> Result>, webidl::WebIdlError> { - let event_type = required_simple_object_event_type( - scope, - args, - "EventTarget.removeEventListener", - "Failed to execute 'removeEventListener' on 'EventTarget': 1 argument required, but only 0 present.", - )?; - let listener = args.get(1); - if listener.is_null_or_undefined() { - return Ok(webidl::ParseOutcome::Skip); - } - let options = webidl::event_listener_options(scope, args, 2, false); - Ok(webidl::ParseOutcome::Parsed( - SimpleObjectRemoveListenerCall { - event_type, - listener, - options, - }, - )) -} - pub(crate) fn simple_object_event_target_add_listener<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, slot_name: &str, ) { - let Some(parsed) = webidl::parse_args::(scope, args) else { + let Some(call) = webidl::parse_args::(scope, args) else { return; }; - let webidl::ParseOutcome::Parsed(call) = parsed.call else { + let Some(listener) = call.listener else { return; }; + let listener = simple_object_event_listener_parts(scope, listener); let target = args.this(); simple_object_event_target_register_resolved_listener( scope, target, slot_name, call.event_type, - call.listener, - call.options, - call.signal, + listener, + call.options.options, + call.options.signal, ); } @@ -269,20 +221,7 @@ pub(crate) fn simple_object_event_target_register_webidl_listener<'s>( listener: webidl::WebIdlCallbackInterface, options: webidl::EventListenerOptions, ) { - let callback_value = listener.value(scope); - let callback = v8::Local::::try_from(callback_value) - .expect("converted EventListener callback must remain an object"); - let relevant_context = listener.relevant_context(scope); - let incumbent_context = listener.incumbent_context(scope); - let (relevant_context_anchor, incumbent_context_anchor) = - simple_callback_context_anchors_for_contexts(scope, relevant_context, incumbent_context); - let listener = SimpleObjectResolvedEventListener { - original: callback.into(), - callback, - relevant_context_anchor, - incumbent_context_anchor, - is_callable: listener.callable_at_conversion(), - }; + let listener = simple_object_event_listener_parts(scope, listener); simple_object_event_target_register_resolved_listener( scope, target, slot_name, event_type, listener, options, None, ); @@ -363,19 +302,20 @@ pub(crate) fn simple_object_event_target_remove_listener<'s>( args: &v8::FunctionCallbackArguments<'s>, slot_name: &str, ) { - let Some(parsed) = webidl::parse_args::(scope, args) else { + let Some(call) = webidl::parse_args::(scope, args) else { return; }; - let webidl::ParseOutcome::Parsed(call) = parsed.call else { + let Some(listener) = call.listener else { return; }; + let listener = listener.value(scope); let target = args.this(); simple_object_event_remove_listener_value_for_type( scope, target, slot_name, &call.event_type, - call.listener, + listener, call.options.capture, ); } @@ -618,7 +558,6 @@ pub(crate) fn simple_object_event_set_ordered_handler<'s>( target.get(scope, key.into()) }) .and_then(|value| v8::Local::::try_from(value).ok()) - .filter(|callback| callback.is_callable()) else { return; }; @@ -638,6 +577,13 @@ pub(crate) fn simple_object_event_set_ordered_handler<'s>( SIMPLE_EVENT_TARGET_LISTENER_CALLBACK_SLOT, callback.into(), ); + let callable = v8::Boolean::new(scope, callback.is_callable()); + set_private_value( + scope, + entry, + SIMPLE_EVENT_TARGET_LISTENER_CALLABLE_SLOT, + callable.into(), + ); set_private_value( scope, entry, @@ -856,21 +802,22 @@ fn remove_simple_object_event_type_order<'s>( fn simple_object_event_listener_parts<'s>( scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> Option> { - if value.is_null_or_undefined() { - return None; - } - let callback = v8::Local::::try_from(value).ok()?; + listener: webidl::WebIdlCallbackInterface, +) -> SimpleObjectResolvedEventListener<'s> { + let callback_value = listener.value(scope); + let callback = v8::Local::::try_from(callback_value) + .expect("converted EventListener callback must remain an object"); + let relevant_context = listener.relevant_context(scope); + let incumbent_context = listener.incumbent_context(scope); let (relevant_context_anchor, incumbent_context_anchor) = - simple_callback_context_anchors(scope, callback); - Some(SimpleObjectResolvedEventListener { - original: value, + simple_callback_context_anchors_for_contexts(scope, relevant_context, incumbent_context); + SimpleObjectResolvedEventListener { + original: callback.into(), callback, relevant_context_anchor, incumbent_context_anchor, - is_callable: callback.is_callable(), - }) + is_callable: listener.callable_at_conversion(), + } } fn simple_callback_context_anchors<'s>( @@ -940,7 +887,7 @@ fn simple_object_event_handler_entry_object<'s>( callback, relevant_context_anchor, incumbent_context_anchor, - true, + callback.is_callable(), ) .bind(scope) .expect("SimpleObject event handler entry declaration should bind") @@ -999,22 +946,6 @@ fn simple_object_event_listener_entry_registered<'s>( simple_object_event_listener_array_contains_entry(scope, listeners, entry) } -pub(crate) fn simple_object_event_listener_is_registered<'s>( - scope: &mut v8::PinScope<'s, '_>, - target: v8::Local<'s, v8::Object>, - slot_name: &str, - event_type: &str, - original: v8::Local<'s, v8::Value>, - capture: bool, -) -> bool { - let Some(listeners) = - simple_object_event_listener_array(scope, target, slot_name, event_type, false) - else { - return false; - }; - simple_object_event_listener_array_contains_original(scope, listeners, original, capture) -} - fn simple_object_event_listener_original<'s>( scope: &mut v8::PinScope<'s, '_>, candidate: v8::Local<'s, v8::Value>, @@ -1059,6 +990,7 @@ fn simple_object_event_listener_snapshot_entry<'s>( simple_object_private_bool_slot(scope, entry, SIMPLE_EVENT_TARGET_LISTENER_PASSIVE_SLOT) .unwrap_or(false); Some(SimpleObjectEventListenerSnapshot { + entry, original, callback, relevant_context, diff --git a/moli-renderer-v8/src/context_bootstrap/shared_installers/abort_statics.rs b/moli-renderer-v8/src/context_bootstrap/shared_installers/abort_statics.rs index 9320b7a054..843a3b3628 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared_installers/abort_statics.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared_installers/abort_statics.rs @@ -1,3 +1,4 @@ +use crate::context_bootstrap::abort_signal_events; use crate::native_bridge::abort; use crate::web_api_interfaces; use moli_webapi_declare::{WebApiFunctionTemplate, v8}; @@ -7,7 +8,7 @@ use moli_webapi_declare::{WebApiFunctionTemplate, v8}; struct AbortSignalConstructorDeclaration { #[webapi( static_method = "abort", - length = 1, + length = 0, callback = abort::abort_signal_static_abort_callback, enumerable )] @@ -29,26 +30,8 @@ struct AbortSignalConstructorDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::AbortSignal, enumerable)] +#[webapi(interface = web_api_interfaces::AbortSignal, enumerable, receiver)] struct AbortSignalPrototypeDeclaration { - #[webapi( - method, - length = 2, - callback = abort::abort_signal_add_event_listener_callback - )] - add_event_listener: (), - #[webapi( - method, - length = 2, - callback = abort::abort_signal_remove_event_listener_callback - )] - remove_event_listener: (), - #[webapi( - method, - length = 1, - callback = abort::abort_signal_dispatch_event_callback - )] - dispatch_event: (), #[webapi( method, length = 0, @@ -69,8 +52,8 @@ struct AbortSignalPrototypeDeclaration { reason: (), #[webapi( accessor_property, - getter = abort::abort_signal_onabort_getter_callback, - setter = abort::abort_signal_onabort_setter_callback, + getter = abort_signal_events::onabort_getter, + setter = abort_signal_events::onabort_setter, enumerable )] onabort: (), diff --git a/moli-renderer-v8/src/context_bootstrap/worker_host/tests.rs b/moli-renderer-v8/src/context_bootstrap/worker_host/tests.rs index 450d18fa48..92226bc07f 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/tests.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/tests.rs @@ -56,6 +56,14 @@ fn setup_worker_context() -> (v8::OwnedIsolate, v8::Global) { let ctx_scope = &mut v8::ContextScope::new(scope, context); let global = context.global(ctx_scope); + crate::context_bootstrap::exposed_interfaces::install_worker_exposed_interfaces( + ctx_scope, + global, + crate::context_bootstrap::exposed_interfaces::RealmKind::DedicatedWorker, + false, + vec![crate::context_bootstrap::find_constructor_spec("Event").expect("Event spec")], + ) + .expect("standalone Worker tests need intrinsic event constructors"); install_dom_exception(ctx_scope, global); let form_data_template = crate::context_bootstrap::build_named_constructor_template(ctx_scope, "FormData") @@ -545,7 +553,7 @@ fn constructor_declared_event_target_slots_ignore_reflection_and_spoofing() { if (typeof w.onmessage !== "function") { throw new Error("onmessage getter should ignore public slot spoofing"); } - w.dispatchEvent({ type: "message" }); + w.dispatchEvent(new Event("message")); const result = calls.join("|"); if (result !== "listener:message|handler:message") { throw new Error(`Worker ordered dispatch was spoofed: ${result}`); diff --git a/moli-renderer-v8/src/event_listener_args.rs b/moli-renderer-v8/src/event_listener_args.rs new file mode 100644 index 0000000000..642418fe25 --- /dev/null +++ b/moli-renderer-v8/src/event_listener_args.rs @@ -0,0 +1,73 @@ +//! The shared EventTarget argument boundary, before target-specific mutation. + +use crate::abort_signal_route::{ResolvedAbortSignal, event_listener_signal_from_options_value}; +use crate::webidl; + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "EventTarget.addEventListener")] +pub(crate) struct AddEventListenerArgs<'s> { + #[webidl(required, name = "type")] + pub(crate) event_type: String, + #[webidl(required, converter = "callback_interface", nullable)] + pub(crate) listener: Option, + #[webidl(with = add_event_listener_options)] + pub(crate) options: AddEventListenerOptions<'s>, +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "EventTarget.removeEventListener")] +pub(crate) struct RemoveEventListenerArgs { + #[webidl(required, name = "type")] + pub(crate) event_type: String, + #[webidl(required, converter = "callback_interface", nullable)] + pub(crate) listener: Option, + #[webidl(with = remove_event_listener_options)] + pub(crate) options: webidl::EventListenerOptions, +} + +pub(crate) struct AddEventListenerOptions<'s> { + pub(crate) options: webidl::EventListenerOptions, + pub(crate) signal: Option>, +} + +fn add_event_listener_options<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + index: i32, +) -> Result, webidl::WebIdlError> { + // Inherited members come first, followed by this dictionary's members in + // lexical order. Finish conversion even when the callback is null. + let value = args.get(index); + let options = webidl::add_event_listener_options_value(scope, value)?; + let signal = event_listener_signal_from_options_value(scope, value).ok_or_else(|| { + webidl::WebIdlError::pending_exception(webidl::Context::member( + "AddEventListenerOptions", + "signal", + )) + })?; + Ok(AddEventListenerOptions { options, signal }) +} + +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "EventListenerOptions")] +struct RemoveEventListenerOptions { + #[webidl(default = false)] + capture: bool, +} + +fn remove_event_listener_options<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + index: i32, +) -> Result { + let value = args.get(index); + let capture = if let Ok(object) = v8::Local::::try_from(value) { + webidl::parse_dictionary_object::(scope, object)?.capture + } else { + value.boolean_value(scope) + }; + Ok(webidl::EventListenerOptions { + capture, + ..webidl::EventListenerOptions::default() + }) +} diff --git a/moli-renderer-v8/src/host.rs b/moli-renderer-v8/src/host.rs index 1d08f5601f..e00995e6a7 100644 --- a/moli-renderer-v8/src/host.rs +++ b/moli-renderer-v8/src/host.rs @@ -32,8 +32,7 @@ pub(super) use self::events::{ DispatchStatus, HostEventTargetRegistry, PublicEventDispatchResult, create_host_event, dispatch_host_event, dispatch_public_event, dispatch_public_event_with_original_target, event_dispatch_status, event_target_value, host_event_defaults, invoke_prepared_event_callback, - invoke_prepared_event_callback_on_object, report_event_callback_exception, - report_event_listener_exception, + report_event_callback_exception, report_event_listener_exception, }; pub(crate) use self::events::{EventListenerInspectorSnapshot, EventListenerRegistration}; pub(super) use self::scripts::{ diff --git a/moli-renderer-v8/src/host/events.rs b/moli-renderer-v8/src/host/events.rs index 4612dfdf3c..0343f9a4cc 100644 --- a/moli-renderer-v8/src/host/events.rs +++ b/moli-renderer-v8/src/host/events.rs @@ -169,35 +169,6 @@ pub(crate) fn invoke_prepared_event_callback<'s>( ) } -/// Invokes an EventListener whose EventTarget has an API-specific object -/// residence rather than a DOM `EventTargetHandle`. -/// -/// AbortSignal uses this path so its listener ordering remains with AbortStore -/// while callback Realm/currentness, `window.event`, dynamic `handleEvent` -/// lookup, and exception reporting stay identical to other EventListeners. -pub(crate) fn invoke_prepared_event_callback_on_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - host_ptr: *mut JsContextHost, - event_type: &str, - callback_name: &str, - callback: crate::native_bridge::PreparedEventCallback, - receiver: v8::Local<'s, v8::Object>, - event: v8::Local<'s, v8::Object>, -) -> Option> { - let _dom_debugger_pause = unsafe { &*host_ptr } - .schedule_dom_debugger_event_listener_pause_for_interface(event_type, "AbortSignal"); - invoke_prepared_event_callback_with_receiver( - scope, - host_ptr, - event_type, - callback_name, - callback, - receiver.into(), - Some(event), - &[event.into()], - ) -} - #[allow(clippy::too_many_arguments)] fn invoke_prepared_event_callback_with_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index c7f7359912..936a4936b9 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -53,6 +53,7 @@ mod document_script_scheduler; mod document_task_lane; mod dom_parser; mod dynamic_script_owner; +mod event_listener_args; mod exception_reporting; mod frame_owner_model; mod host; diff --git a/moli-renderer-v8/src/native_bridge/abort.rs b/moli-renderer-v8/src/native_bridge/abort.rs index 31a7ce5dc4..4b87028df7 100644 --- a/moli-renderer-v8/src/native_bridge/abort.rs +++ b/moli-renderer-v8/src/native_bridge/abort.rs @@ -1,8 +1,8 @@ use crate::web_api_interfaces; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use super::super::document_runtime::EventTargetHandle; -use super::super::util::{get_private_value, set_private_value, v8_string, v8str}; +use super::super::util::{get_private_value, set_private_value, v8str}; use crate::context_bootstrap::{MessagePortEventListenerId, new_dom_exception_value}; use crate::types::MessagePortId; use moli_webapi_declare::WebApiObject; @@ -12,16 +12,14 @@ mod event; mod signal; mod statics; +use crate::context_bootstrap::abort_signal_events; pub(crate) use controller::{ abort_controller_abort_callback, abort_controller_constructor_callback, abort_controller_signal_getter_callback, }; -use event::dispatch_abort; pub(crate) use signal::{ - abort_signal_aborted_getter_callback, abort_signal_add_event_listener_callback, - abort_signal_dispatch_event_callback, abort_signal_onabort_getter_callback, - abort_signal_onabort_setter_callback, abort_signal_reason_getter_callback, - abort_signal_remove_event_listener_callback, abort_signal_throw_if_aborted_callback, + abort_signal_aborted_getter_callback, abort_signal_reason_getter_callback, + abort_signal_throw_if_aborted_callback, }; pub(crate) use statics::{ abort_signal_any_callback, abort_signal_static_abort_callback, abort_signal_timeout_callback, @@ -44,45 +42,12 @@ struct AbortSignalState { signal: Option>, aborted: bool, reason: Option>, - onabort: Option>, - listeners: HashMap>, abort_algorithms: Vec>, linked_target_listeners: Vec, linked_message_port_listeners: Vec, dependent_signals: Vec, } -#[derive(Clone, Copy)] -pub(super) struct AbortListener { - pub(super) callback_id: super::EventCallbackId, - pub(super) capture: bool, - pub(super) once: bool, - pub(super) passive: bool, -} - -pub(super) struct PreparedAbortListener { - pub(super) callback: super::PreparedEventCallback, - pub(super) passive: bool, -} - -#[derive(Default)] -pub(super) struct AbortDispatchSnapshot { - listeners: Vec, - onabort: Option>, -} - -impl AbortSignalState { - fn take_dispatch_snapshot(&mut self, event_type: &str) -> AbortDispatchSnapshot { - let listeners = self.listeners.get(event_type).cloned().unwrap_or_default(); - let onabort = if event_type == "abort" { - self.onabort.clone() - } else { - None - }; - AbortDispatchSnapshot { listeners, onabort } - } -} - struct AbortLinkedTargetListener { target: EventTargetHandle, event_type: String, @@ -149,23 +114,10 @@ impl AbortStore { .map(|value| value as u32) } - fn define_hidden_value( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, - key: &str, - value: v8::Local<'_, v8::Value>, - ) { - let Some(key) = v8_string(scope, key) else { - return; - }; - let _ = - object.define_own_property(scope, key.into(), value, v8::PropertyAttribute::DONT_ENUM); - } - - fn init_signal( + fn init_signal<'s>( &mut self, - scope: &mut v8::PinScope<'_, '_>, - signal: v8::Local<'_, v8::Object>, + scope: &mut v8::PinScope<'s, '_>, + signal: v8::Local<'s, v8::Object>, aborted: bool, reason: Option>, ) -> u32 { @@ -185,14 +137,15 @@ impl AbortStore { ABORT_SIGNAL_ID_SLOT, v8::Number::new(scope, signal_id as f64).into(), ); + abort_signal_events::initialize(scope, signal); signal_id } - fn init_controller( + fn init_controller<'s>( &mut self, - scope: &mut v8::PinScope<'_, '_>, + scope: &mut v8::PinScope<'s, '_>, controller: v8::Local<'_, v8::Object>, - signal: v8::Local<'_, v8::Object>, + signal: v8::Local<'s, v8::Object>, ) { let signal_id = self.init_signal(scope, signal, false, None); let controller_id = self.alloc_controller_id(); @@ -250,108 +203,6 @@ impl AbortStore { .map(|reason| v8::Local::new(scope, reason)) } - pub(super) fn listener_callback_ids( - &self, - signal_id: u32, - event_type: &str, - capture: bool, - ) -> Vec { - self.signal_state(signal_id) - .and_then(|state| state.listeners.get(event_type)) - .into_iter() - .flatten() - .filter(|listener| listener.capture == capture) - .map(|listener| listener.callback_id) - .collect() - } - - pub(super) fn register_listener( - &mut self, - signal_id: u32, - event_type: &str, - callback_id: super::EventCallbackId, - capture: bool, - once: bool, - passive: bool, - ) -> bool { - let Some(state) = self.signal_state_mut(signal_id) else { - return false; - }; - state - .listeners - .entry(event_type.to_owned()) - .or_default() - .push(AbortListener { - callback_id, - capture, - once, - passive, - }); - true - } - - pub(super) fn unregister_listener_by_id( - &mut self, - signal_id: u32, - event_type: &str, - callback_id: super::EventCallbackId, - capture: bool, - ) -> bool { - let Some(state) = self.signal_state_mut(signal_id) else { - return false; - }; - let mut remove_entry = false; - let mut removed = false; - if let Some(listeners) = state.listeners.get_mut(event_type) { - listeners.retain(|candidate| { - let matches = candidate.callback_id == callback_id && candidate.capture == capture; - removed |= matches; - !matches - }); - remove_entry = listeners.is_empty(); - } - if remove_entry { - state.listeners.remove(event_type); - } - removed - } - - pub(super) fn claim_listener_for_dispatch( - &mut self, - signal_id: u32, - event_type: &str, - callback_id: super::EventCallbackId, - ) -> Option { - let state = self.signal_state_mut(signal_id)?; - let listener = state - .listeners - .get(event_type)? - .iter() - .find(|listener| listener.callback_id == callback_id) - .copied()?; - if listener.once { - let _ = self.unregister_listener_by_id( - signal_id, - event_type, - callback_id, - listener.capture, - ); - } - Some(listener) - } - - pub(super) fn unregister_signal_event_callbacks( - &mut self, - callback_ids: &HashSet, - ) { - for state in self.signals.values_mut() { - state.listeners.retain(|_, listeners| { - listeners.retain(|listener| !callback_ids.contains(&listener.callback_id)); - !listeners.is_empty() - }); - } - } - pub(crate) fn register_abort_algorithm<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, @@ -468,7 +319,6 @@ impl AbortStore { }; let Some(( abort_algorithms, - dispatch_snapshot, linked_target_listeners, linked_message_port_listeners, dependent_signals, @@ -486,10 +336,8 @@ impl AbortStore { let linked_message_port_listeners = std::mem::take(&mut state.linked_message_port_listeners); let dependent_signals = state.dependent_signals.clone(); - let dispatch_snapshot = state.take_dispatch_snapshot("abort"); Some(( abort_algorithms, - dispatch_snapshot, linked_target_listeners, linked_message_port_listeners, dependent_signals, @@ -505,13 +353,7 @@ impl AbortStore { linked.listener_id, ); } - dispatch_abort( - scope, - host as *mut super::JsContextHost, - signal, - signal_id, - dispatch_snapshot, - ); + abort_signal_events::dispatch_abort(scope, signal); for linked in linked_target_listeners { host.remove_registered_event_listener_by_id( linked.target, diff --git a/moli-renderer-v8/src/native_bridge/abort/event.rs b/moli-renderer-v8/src/native_bridge/abort/event.rs index 097834e34f..0c7da3091a 100644 --- a/moli-renderer-v8/src/native_bridge/abort/event.rs +++ b/moli-renderer-v8/src/native_bridge/abort/event.rs @@ -1,12 +1,4 @@ -use super::{AbortDispatchSnapshot, AbortStore}; -use crate::context_bootstrap::{ - EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, event_internal_bool_flag, - set_event_internal_flag, -}; use crate::exception_reporting::invoke_callback; -use crate::host::invoke_prepared_event_callback_on_object; -use crate::native_bridge::JsContextHost; -use crate::util::{v8_string, v8str}; pub(super) fn invoke_abort_algorithms<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -27,47 +19,6 @@ pub(super) fn invoke_abort_algorithms<'s>( } } -pub(super) fn dispatch_abort<'s>( - scope: &mut v8::PinScope<'s, '_>, - host_ptr: *mut JsContextHost, - signal: v8::Local<'_, v8::Object>, - signal_id: u32, - dispatch_snapshot: AbortDispatchSnapshot, -) { - // Abort listener/event-handler delivery is entered from both Rust-side - // state transitions and V8 native binding shims. Like XHR, that means the - // incoming `signal` local is not guaranteed to share the exact scope - // lifetime required by the structured exception reporter. Normalize it up - // front so listeners and `onabort` retain local `TryCatch`, structured - // stderr, and no stdout pollution. - let signal = local_object_in_scope(scope, signal); - let global = scope.get_current_context().global(scope); - let Some(event_ctor) = global - .get(scope, v8str(scope, "Event").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { - return; - }; - let Some(event_type) = v8_string(scope, "abort") else { - return; - }; - let Some(event) = event_ctor.new_instance(scope, &[event_type.into()]) else { - return; - }; - AbortStore::define_hidden_value(scope, event, "target", signal.into()); - AbortStore::define_hidden_value(scope, event, "currentTarget", signal.into()); - - invoke_abort_event_callbacks( - scope, - host_ptr, - signal, - signal_id, - dispatch_snapshot, - "abort", - event, - ); -} - pub(super) fn local_object_in_scope<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'_, v8::Object>, @@ -75,56 +26,3 @@ pub(super) fn local_object_in_scope<'s>( let global = v8::Global::new(scope, object); v8::Local::new(scope, global) } - -pub(super) fn invoke_abort_event_callbacks<'s>( - scope: &mut v8::PinScope<'s, '_>, - host_ptr: *mut JsContextHost, - signal: v8::Local<'s, v8::Object>, - signal_id: u32, - dispatch_snapshot: AbortDispatchSnapshot, - event_type: &str, - event: v8::Local<'s, v8::Object>, -) { - for listener in dispatch_snapshot.listeners { - let Some(listener) = (unsafe { &mut *host_ptr }) - .claim_abort_signal_event_listener_for_dispatch( - scope, - signal_id, - event_type, - listener.callback_id, - ) - else { - continue; - }; - let callback_name = format!("AbortSignal {event_type} listener"); - set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, listener.passive); - let _ = invoke_prepared_event_callback_on_object( - scope, - host_ptr, - event_type, - &callback_name, - listener.callback, - signal, - event, - ); - set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, false); - if event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) { - break; - } - } - if event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) { - return; - } - if event_type == "abort" - && let Some(onabort) = dispatch_snapshot.onabort - { - let onabort = v8::Local::new(scope, &onabort); - let _ = invoke_callback( - scope, - "AbortSignal.onabort", - onabort, - signal.into(), - &[event.into()], - ); - } -} diff --git a/moli-renderer-v8/src/native_bridge/abort/signal.rs b/moli-renderer-v8/src/native_bridge/abort/signal.rs index a9597afe2f..ba43412ba6 100644 --- a/moli-renderer-v8/src/native_bridge/abort/signal.rs +++ b/moli-renderer-v8/src/native_bridge/abort/signal.rs @@ -1,162 +1,5 @@ use super::AbortStore; -use super::event::{invoke_abort_event_callbacks, local_object_in_scope}; -use crate::util::{context_host_ptr_from_global_bridge, v8str}; -use crate::webidl; - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "AbortSignal.addEventListener")] -struct AbortSignalAddEventListenerArgs { - #[webidl(required)] - event_type: String, - #[webidl(required, converter = "callback_interface", nullable)] - listener: Option, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "AbortSignal.removeEventListener")] -struct AbortSignalRemoveEventListenerArgs { - #[webidl(required)] - event_type: String, - #[webidl(required, converter = "callback_interface", nullable)] - listener: Option, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "AbortSignal.dispatchEvent")] -struct AbortSignalDispatchEventArgs<'s> { - #[webidl(required)] - event: v8::Local<'s, v8::Value>, -} - -pub(crate) fn abort_signal_add_event_listener_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_undefined(); - return; - }; - let signal = args.this(); - if AbortStore::signal_id_from_object(scope, signal).is_none() { - rv.set_undefined(); - return; - } - let Some(parsed) = webidl::parse_args::(scope, &args) else { - rv.set_undefined(); - return; - }; - let Some(listener) = parsed.listener else { - rv.set_undefined(); - return; - }; - let options = webidl::event_listener_options(scope, &args, 2, true); - unsafe { &mut *host_ptr }.register_abort_signal_event_listener( - scope, - signal, - &parsed.event_type, - listener, - options, - ); - rv.set_undefined(); -} - -pub(crate) fn abort_signal_remove_event_listener_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_undefined(); - return; - }; - let signal = args.this(); - if AbortStore::signal_id_from_object(scope, signal).is_none() { - rv.set_undefined(); - return; - } - let Some(parsed) = webidl::parse_args::(scope, &args) - else { - rv.set_undefined(); - return; - }; - let Some(listener) = parsed.listener else { - rv.set_undefined(); - return; - }; - let capture = webidl::event_listener_options(scope, &args, 2, true).capture; - unsafe { &mut *host_ptr }.unregister_abort_signal_event_listener( - scope, - signal, - &parsed.event_type, - &listener, - capture, - ); - rv.set_undefined(); -} - -pub(crate) fn abort_signal_dispatch_event_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_bool(false); - return; - }; - let signal = args.this(); - let Some(parsed) = webidl::parse_args::>(scope, &args) else { - rv.set_bool(false); - return; - }; - let Ok(event) = v8::Local::::try_from(parsed.event) else { - rv.set_bool(false); - return; - }; - let Some(signal_id) = AbortStore::signal_id_from_object(scope, signal) else { - rv.set_bool(false); - return; - }; - let Some(event_type) = event - .get(scope, v8str(scope, "type").into()) - .and_then(|value| value.to_string(scope)) - .map(|s| s.to_rust_string_lossy(scope)) - else { - rv.set_bool(false); - return; - }; - let default_prevented_key = v8str(scope, "defaultPrevented"); - // `dispatchEvent(...)` receives both `this` and the event object from the V8 callback frame. - // Re-root them into the current scope before invoking user handlers so this path can share the - // same reporting helper as the Rust-driven `dispatch_abort(...)` path without narrowing the - // callback signature that `FunctionTemplate::builder(...)` expects. - let signal = local_object_in_scope(scope, signal); - let event = local_object_in_scope(scope, event); - AbortStore::define_hidden_value(scope, event, "target", signal.into()); - AbortStore::define_hidden_value(scope, event, "currentTarget", signal.into()); - let Some(dispatch_snapshot) = unsafe { &mut *host_ptr } - .native_bridge_mut() - .abort - .signal_state_mut(signal_id) - .map(|state| state.take_dispatch_snapshot(&event_type)) - else { - rv.set_bool(false); - return; - }; - invoke_abort_event_callbacks( - scope, - host_ptr, - signal, - signal_id, - dispatch_snapshot, - &event_type, - event, - ); - let result = event - .get(scope, default_prevented_key.into()) - .is_none_or(|value| !value.boolean_value(scope)); - rv.set_bool(result); -} +use crate::util::context_host_ptr_from_global_bridge; pub(crate) fn abort_signal_aborted_getter_callback<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -215,66 +58,6 @@ pub(crate) fn abort_signal_reason_getter_callback<'s>( rv.set(reason); } -pub(crate) fn abort_signal_onabort_getter_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_null(); - return; - }; - let signal = args.this(); - if AbortStore::signal_id_from_object(scope, signal).is_none() { - rv.set_null(); - return; - } - let Some(onabort) = AbortStore::signal_id_from_object(scope, signal) - .and_then(|id| { - unsafe { &mut *host_ptr } - .native_bridge_mut() - .abort - .signal_state(id) - }) - .and_then(|state| state.onabort.as_ref()) - .map(|onabort| v8::Local::new(scope, onabort)) - else { - rv.set_null(); - return; - }; - rv.set(onabort.into()); -} - -pub(crate) fn abort_signal_onabort_setter_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_undefined(); - return; - }; - let signal = args.this(); - if AbortStore::signal_id_from_object(scope, signal).is_none() { - rv.set_undefined(); - return; - } - let Some(signal_id) = AbortStore::signal_id_from_object(scope, signal) else { - rv.set_undefined(); - return; - }; - if let Some(state) = unsafe { &mut *host_ptr } - .native_bridge_mut() - .abort - .signal_state_mut(signal_id) - { - state.onabort = v8::Local::::try_from(args.get(0)) - .ok() - .map(|function| v8::Global::new(scope, function)); - } - rv.set_undefined(); -} - pub(crate) fn abort_signal_throw_if_aborted_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs b/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs index 83e440af42..2222cabff8 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs @@ -319,9 +319,6 @@ impl JsContextHost { } fn release_retired_event_callbacks(&mut self, retired: HashSet) { - self.bridge - .abort - .unregister_signal_event_callbacks(&retired); for callback_id in retired { self.unregister_abort_target_listener(callback_id); self.release_event_callback(callback_id); @@ -405,9 +402,6 @@ impl JsContextHost { self.remove_event_callback_registrations(&retired); self.retire_child_window_event_callbacks(&retired); self.remove_message_port_event_callbacks(&retired); - self.bridge - .abort - .unregister_signal_event_callbacks(&retired); for callback_id in retired { self.unregister_abort_target_listener(callback_id); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/signal_bridge.rs b/moli-renderer-v8/src/native_bridge/context_host/signal_bridge.rs index 381eb3e9fa..f28cef13ee 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/signal_bridge.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/signal_bridge.rs @@ -1,5 +1,4 @@ use super::*; -use crate::webidl; impl JsContextHost { pub(crate) fn is_abort_signal<'s>( @@ -45,121 +44,6 @@ impl JsContextHost { ); } - pub(crate) fn register_abort_signal_event_listener<'s>( - &mut self, - scope: &mut v8::PinScope<'s, '_>, - signal: v8::Local<'s, v8::Object>, - event_type: &str, - callback: webidl::WebIdlCallbackInterface, - options: webidl::EventListenerOptions, - ) -> bool { - let Some(signal_id) = - crate::native_bridge::abort::AbortStore::signal_id_from_object(scope, signal) - else { - return false; - }; - if !self.bridge.abort.is_signal_object(scope, signal) { - return false; - } - let callback_value = callback.value(scope); - let Ok(callback_object) = v8::Local::::try_from(callback_value) else { - return false; - }; - if self - .bridge - .abort - .listener_callback_ids(signal_id, event_type, options.capture) - .into_iter() - .any(|callback_id| self.event_callback_matches(scope, callback_id, callback_object)) - { - return false; - } - - let callback_id = self.register_webidl_event_callback(scope, callback); - if self.bridge.abort.register_listener( - signal_id, - event_type, - callback_id, - options.capture, - options.once, - options.passive.unwrap_or(false), - ) { - true - } else { - self.release_event_callback(callback_id); - false - } - } - - pub(crate) fn unregister_abort_signal_event_listener<'s>( - &mut self, - scope: &mut v8::PinScope<'s, '_>, - signal: v8::Local<'s, v8::Object>, - event_type: &str, - callback: &webidl::WebIdlCallbackInterface, - capture: bool, - ) -> bool { - let Some(signal_id) = - crate::native_bridge::abort::AbortStore::signal_id_from_object(scope, signal) - else { - return false; - }; - let callback_value = callback.value(scope); - let Ok(callback_object) = v8::Local::::try_from(callback_value) else { - return false; - }; - let Some(callback_id) = self - .bridge - .abort - .listener_callback_ids(signal_id, event_type, capture) - .into_iter() - .find(|callback_id| self.event_callback_matches(scope, *callback_id, callback_object)) - else { - return false; - }; - if !self - .bridge - .abort - .unregister_listener_by_id(signal_id, event_type, callback_id, capture) - { - return false; - } - self.release_event_callback(callback_id); - true - } - - pub(in crate::native_bridge) fn claim_abort_signal_event_listener_for_dispatch( - &mut self, - scope: &mut v8::PinScope<'_, '_>, - signal_id: u32, - event_type: &str, - callback_id: EventCallbackId, - ) -> Option { - let listener = - self.bridge - .abort - .claim_listener_for_dispatch(signal_id, event_type, callback_id)?; - let callback = self.prepare_event_callback(scope, callback_id); - if listener.once { - self.release_event_callback(callback_id); - } - let Some(callback) = callback else { - if !listener.once { - let _ = self.bridge.abort.unregister_listener_by_id( - signal_id, - event_type, - callback_id, - listener.capture, - ); - } - return None; - }; - Some(crate::native_bridge::abort::PreparedAbortListener { - callback, - passive: listener.passive, - }) - } - pub(crate) fn register_abort_signal_algorithm<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs index 005ba6ebf1..d5d2e0f115 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs @@ -4464,7 +4464,7 @@ async fn shared_worker_declared_surface_ignores_reflection_and_spoofing() { if (typeof worker.onerror !== "function") { throw new Error("onerror getter should ignore public slot spoofing"); } - worker.dispatchEvent({ type: "error" }); + worker.dispatchEvent(new Event("error")); const dispatchResult = __sharedWorkerSurfaceCalls.join("|"); if (dispatchResult !== "listener:error|handler:error") { throw new Error(`SharedWorker ordered dispatch was spoofed: ${dispatchResult}`); diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/abort_signal_events.js b/moli-renderer-v8/src/script_vm/tests/browser_api/abort_signal_events.js new file mode 100644 index 0000000000..73d3f60db7 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/abort_signal_events.js @@ -0,0 +1,192 @@ +function abortSignalEventTargetProbe(realm = globalThis, methods = realm.EventTarget.prototype) { + const failures = []; + const scenarios = []; + const check = (condition, label) => { if (!condition) failures.push(label); }; + const scenario = (name, run) => { + scenarios.push(name); + try { run(); } catch (error) { failures.push(name + ': ' + error.name + ': ' + error.message); } + }; + scenario('inherited methods', () => { + const signal = new realm.AbortController().signal; + check(Object.getPrototypeOf(realm.AbortSignal.prototype) === realm.EventTarget.prototype, 'parent prototype'); + for (const name of ['addEventListener', 'removeEventListener', 'dispatchEvent']) { + check(!Object.hasOwn(realm.AbortSignal.prototype, name), 'own method ' + name); + check(signal[name] === realm.EventTarget.prototype[name], 'method identity ' + name); + } + check(realm.AbortSignal.abort.length === 0, 'optional abort reason length'); + }); + scenario('borrowed native listeners', () => { + const controller = new realm.AbortController(); + const signal = controller.signal; + const calls = []; + function kept(event) { + calls.push('kept'); + check(this === signal && event.target === signal && event.currentTarget === signal, 'native callback receiver'); + check(event.eventPhase === 2 && event.isTrusted && signal.aborted && signal.reason === 'reason', 'native abort event state'); + } + const removed = () => calls.push('removed'); + methods.addEventListener.call(signal, 'abort', kept); + signal.addEventListener('abort', kept); + signal.addEventListener('abort', removed); + methods.removeEventListener.call(signal, 'abort', removed); + controller.abort('reason'); + controller.abort('again'); + check(calls.join() === 'kept', 'native listener sharing: ' + calls); + }); + scenario('ordered handler and capture', () => { + const signal = new realm.AbortController().signal; + const calls = []; + signal.onabort = () => calls.push('old'); + methods.addEventListener.call(signal, 'abort', () => calls.push('bubble')); + signal.onabort = () => { calls.push('handler'); return false; }; + signal.addEventListener('abort', () => calls.push('capture'), true); + const event = new realm.Event('abort', {cancelable: true}); + check(methods.dispatchEvent.call(signal, event) === false && event.defaultPrevented, 'handler cancels event'); + check(calls.join() === 'capture,handler,bubble', 'listener order: ' + calls); + check(!signal.aborted && signal.reason === undefined, 'synthetic abort preserves signal state'); + check(event.target === signal && event.currentTarget === null && event.eventPhase === 0 && + event.composedPath().length === 0, 'dispatch cleanup'); + signal.onabort = null; + signal.onabort = () => calls.push('last'); + calls.length = 0; + signal.dispatchEvent(new realm.Event('abort')); + check(calls.join() === 'capture,bubble,last', 'handler reactivation: ' + calls); + const handler = {handleEvent() { failures.push('EventHandler invoked handleEvent'); }}; + signal.onabort = handler; + check(signal.onabort === handler, 'noncallable EventHandler identity'); + calls.length = 0; + signal.dispatchEvent(new realm.Event('abort')); + check(calls.join() === 'capture,bubble', 'object replaces callable handler: ' + calls); + signal.addEventListener('abort', () => calls.push('tail')); + signal.onabort = () => calls.push('restored'); + calls.length = 0; + signal.dispatchEvent(new realm.Event('abort')); + check(calls.join() === 'capture,bubble,restored,tail', 'object preserves handler position: ' + calls); + signal.onabort = 4; + check(signal.onabort === null, 'primitive EventHandler conversion'); + }); + scenario('signal option removal', () => { + const target = new realm.AbortController().signal; + const controller = new realm.AbortController(); + let count = 0; + const callback = () => ++count; + target.addEventListener('x', callback, {signal: controller.signal}); + methods.dispatchEvent.call(target, new realm.Event('x')); + controller.abort(); + target.dispatchEvent(new realm.Event('x')); + methods.addEventListener.call(target, 'x', callback, {signal: controller.signal}); + target.dispatchEvent(new realm.Event('x')); + check(count === 1, 'aborted option listener count: ' + count); + const owner = new realm.AbortController(); + owner.signal.addEventListener('abort', () => failures.push('self-signal listener'), {signal: owner.signal}); + owner.abort(); + }); + scenario('dispatch phase mutation', () => { + const signal = new realm.AbortController().signal; + const calls = []; + const removed = () => calls.push('removed'); + signal.addEventListener('x', removed); + signal.addEventListener('x', () => { + calls.push('capture'); + signal.removeEventListener('x', removed); + signal.addEventListener('x', () => calls.push('late')); + }, {capture: true, once: true}); + signal.addEventListener('x', () => calls.push('bubble')); + methods.dispatchEvent.call(signal, new realm.Event('x')); + check(calls.join() === 'capture,bubble,late', 'phase snapshot: ' + calls); + calls.length = 0; + signal.dispatchEvent(new realm.Event('x')); + check(calls.join() === 'bubble,late', 'once listener removed: ' + calls); + }); + scenario('reentrant once and passive', () => { + const signal = new realm.AbortController().signal; + let once = 0; + signal.addEventListener('x', () => { + ++once; + signal.dispatchEvent(new realm.Event('x')); + }, {once: true}); + signal.dispatchEvent(new realm.Event('x')); + check(once === 1, 'once listener recursion'); + signal.addEventListener('x', event => event.preventDefault(), {passive: true}); + const event = new realm.Event('x', {cancelable: true}); + check(signal.dispatchEvent(event) === true && !event.defaultPrevented, 'passive listener cancellation'); + }); + return {scenarios, failures}; +} + +function abortSignalReceiverProbe(realm = globalThis) { + const failures = []; + let checks = 0; + const signal = new realm.AbortController().signal; + const revoked = Proxy.revocable(signal, {}); + revoked.revoke(); + const invalid = [{}, Object.create(realm.AbortSignal.prototype), Object.create(signal), + new Proxy(signal, {}), revoked.proxy]; + const prototype = realm.AbortSignal.prototype; + let reads = 0; + const type = {toString() { ++reads; return 'x'; }}; + const options = {get capture() { ++reads; return false; }}; + const operations = [ + value => prototype.addEventListener.call(value, type, () => {}, options), + value => prototype.removeEventListener.call(value, type, () => {}, options), + value => prototype.dispatchEvent.call(value, new realm.Event('x')), + value => prototype.throwIfAborted.call(value), + ...['aborted', 'reason', 'onabort'].map(name => + value => Object.getOwnPropertyDescriptor(prototype, name).get.call(value)), + value => Object.getOwnPropertyDescriptor(prototype, 'onabort').set.call(value, () => {}), + ]; + for (const [index, operation] of operations.entries()) for (const value of invalid) { + ++checks; + try { operation(value); failures.push('accepted receiver ' + index); } + catch (error) { if (!(error instanceof realm.TypeError)) failures.push('wrong exception realm ' + index); } + } + if (reads) failures.push('converted arguments before receiver check'); + let dictionaryReads = []; + signal.addEventListener('x', null, new Proxy({}, {get(_, key) { + dictionaryReads.push(key); return undefined; + }})); + if (dictionaryReads.join() !== 'capture,once,passive,signal') failures.push('dictionary order: ' + dictionaryReads); + const sentinel = new realm.Error('capture conversion'); + let invoked = false; + try { + signal.addEventListener('failed-conversion', () => { invoked = true; }, { + get capture() { throw sentinel; }, + get once() { failures.push('conversion continued after capture threw'); return false; }, + }); + failures.push('accepted throwing options'); + } catch (error) { + if (error !== sentinel) failures.push('replaced options exception'); + } + signal.dispatchEvent(new realm.Event('failed-conversion')); + if (invoked) failures.push('registered listener after options exception'); + const removedReads = []; + signal.removeEventListener('x', null, new Proxy({}, {get(_, key) { + removedReads.push(key); return undefined; + }})); + if (removedReads.join() !== 'capture') failures.push('remove dictionary order: ' + removedReads); + return {checks, failures}; +} + +function abortSignalLifetimeProbe() { + const failures = []; + const frame = document.body.appendChild(document.createElement('iframe')); + const realm = frame.contentWindow; + const controller = new realm.AbortController(); + const signal = controller.signal; + const methods = [EventTarget.prototype.dispatchEvent, realm.EventTarget.prototype.dispatchEvent]; + let calls = 0; + signal.addEventListener('x', () => ++calls); + frame.remove(); + for (const method of methods) { + const event = new Event('x'); + try { + if (method.call(signal, event) !== false || event.target !== null) failures.push('retired dispatch result'); + } catch (error) { failures.push('retired dispatch: ' + error.name); } + for (const [value, expected] of [[null, 'TypeError'], [{}, 'TypeError'], [document.createEvent('Event'), 'InvalidStateError']]) { + try { method.call(signal, value); failures.push('accepted invalid event'); } + catch (error) { if (error.name !== expected) failures.push('wrong retired error: ' + error.name); } + } + } + if (calls) failures.push('retired callback invoked'); + return {calls, failures}; +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/abort_signal_events.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/abort_signal_events.rs new file mode 100644 index 0000000000..94f8c63aa9 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/abort_signal_events.rs @@ -0,0 +1,54 @@ +use super::*; + +const PROBE: &str = include_str!("abort_signal_events.js"); + +fn run_probe(expression: &str) -> serde_json::Value { + let mut vm = new_parsed_test_vm("https://abort-signal-events.test/", ""); + let result = vm + .eval(&format!("{PROBE}\nJSON.stringify({expression})")) + .unwrap(); + serde_json::from_str(&result).unwrap() +} + +#[test] +fn abort_signal_shares_event_target_listeners_across_realms() { + let result = run_probe( + r#"(() => { + const frame = document.body.appendChild(document.createElement('iframe')); + const realm = frame.contentWindow; + try { + return [abortSignalEventTargetProbe(), + abortSignalEventTargetProbe(realm, EventTarget.prototype), + abortSignalEventTargetProbe(window, realm.EventTarget.prototype)]; + } finally { frame.remove(); } + })()"#, + ); + let rows = result.as_array().unwrap(); + assert_eq!(rows.len(), 3); + for row in rows { + assert_eq!(row["failures"], serde_json::json!([]), "{row}"); + assert_eq!(row["scenarios"].as_array().unwrap().len(), 6); + } +} + +#[test] +fn abort_signal_generated_receiver_checks_use_callee_realm_before_conversion() { + let result = run_probe( + r#"(() => { + const frame = document.body.appendChild(document.createElement('iframe')); + try { return [abortSignalReceiverProbe(), abortSignalReceiverProbe(frame.contentWindow)]; } + finally { frame.remove(); } + })()"#, + ); + for row in result.as_array().unwrap() { + assert_eq!(row["failures"], serde_json::json!([]), "{row}"); + assert_eq!(row["checks"], 40); + } +} + +#[test] +fn abort_signal_inherited_dispatch_preserves_retired_target_validation() { + let result = run_probe("abortSignalLifetimeProbe()"); + assert_eq!(result["failures"], serde_json::json!([]), "{result}"); + assert_eq!(result["calls"], 0); +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index c98b593601..7b3cb81f33 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -1,5 +1,6 @@ use super::*; +mod abort_signal_events; mod broadcast_channel; mod chrome; mod clipboard_storage; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs index 3ec889f4cc..a828a6da6b 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs @@ -1826,9 +1826,9 @@ fn abort_signal_internal_id_is_not_page_visible_or_forgeable() { hasVisibleSlot: "__lmAbortSignalId" in signal, ownNames: Object.getOwnPropertyNames(signal), signalMethods: [ - methodDescriptor(AbortSignal.prototype, "addEventListener"), - methodDescriptor(AbortSignal.prototype, "removeEventListener"), - methodDescriptor(AbortSignal.prototype, "dispatchEvent"), + methodDescriptor(Object.getPrototypeOf(AbortSignal.prototype), "addEventListener"), + methodDescriptor(Object.getPrototypeOf(AbortSignal.prototype), "removeEventListener"), + methodDescriptor(Object.getPrototypeOf(AbortSignal.prototype), "dispatchEvent"), methodDescriptor(AbortSignal.prototype, "throwIfAborted") ], signalAccessors: [ @@ -1850,7 +1850,7 @@ fn abort_signal_internal_id_is_not_page_visible_or_forgeable() { staticAbortAborted: staticAbortSignal.aborted, staticAbortReason: staticAbortSignal.reason, anyForged: probe(() => AbortSignal.any([forged])), - getterForged: abortedGetter.call(forged) + getterForged: probe(() => abortedGetter.call(forged)) }); })() "#, @@ -1859,7 +1859,7 @@ fn abort_signal_internal_id_is_not_page_visible_or_forgeable() { assert_eq!( result, - r#"{"hasVisibleSlot":false,"ownNames":[],"signalMethods":["addEventListener:function:addEventListener:2:true:true:true","removeEventListener:function:removeEventListener:2:true:true:true","dispatchEvent:function:dispatchEvent:1:true:true:true","throwIfAborted:function:throwIfAborted:0:true:true:true"],"signalAccessors":["aborted:function:get aborted:0:true:undefined:true","reason:function:get reason:0:true:undefined:true","onabort:function:get onabort:0:true:function:true"],"controllerMethods":["abort:function:abort:0:true:true:true"],"controllerAccessors":["signal:function:get signal:0:true:undefined:true"],"constructorMethods":["abort:function:abort:1:true:true:true","timeout:function:timeout:1:true:true:true","any:function:any:1:true:true:true"],"staticAbortAborted":true,"staticAbortReason":"static-reason","anyForged":"TypeError","getterForged":false}"# + r#"{"hasVisibleSlot":false,"ownNames":[],"signalMethods":["addEventListener:function:addEventListener:2:true:true:true","removeEventListener:function:removeEventListener:2:true:true:true","dispatchEvent:function:dispatchEvent:1:true:true:true","throwIfAborted:function:throwIfAborted:0:true:true:true"],"signalAccessors":["aborted:function:get aborted:0:true:undefined:true","reason:function:get reason:0:true:undefined:true","onabort:function:get onabort:0:true:function:true"],"controllerMethods":["abort:function:abort:0:true:true:true"],"controllerAccessors":["signal:function:get signal:0:true:undefined:true"],"constructorMethods":["abort:function:abort:0:true:true:true","timeout:function:timeout:1:true:true:true","any:function:any:1:true:true:true"],"staticAbortAborted":true,"staticAbortReason":"static-reason","anyForged":"TypeError","getterForged":"TypeError"}"# ); } diff --git a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs index d6c8dbd3d4..cf410770cd 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -36,12 +36,13 @@ const RAW_GLOBAL_FUNCTION_ALLOWLIST: &[(&str, usize)] = &[ ("custom_elements/definition.rs", 10), ("custom_elements/definition_callbacks.rs", 1), ("host/timers.rs", 1), - ("native_bridge/abort.rs", 3), + // AbortSignal listeners and onabort now use the shared typed EventTarget + // registry. These remaining roots own browser-created abort algorithms. + ("native_bridge/abort.rs", 1), ("native_bridge/abort/event.rs", 1), ("native_bridge/history_queue.rs", 3), ("script_vm/frame_script_jobs.rs", 3), - ("worker/abort.rs", 3), - ("worker/abort/event_listener.rs", 1), + ("worker/abort.rs", 2), ("worker/timer_callback.rs", 1), ]; diff --git a/moli-renderer-v8/src/worker/abort.rs b/moli-renderer-v8/src/worker/abort.rs index f1aa70c93f..da62a1c310 100644 --- a/moli-renderer-v8/src/worker/abort.rs +++ b/moli-renderer-v8/src/worker/abort.rs @@ -13,13 +13,7 @@ use super::global_scope::{ TimerInfo, get_worker_state, reject_worker_fetches_for_signal, worker_isolate_timer_queues, }; -mod event_listener; - -use event_listener::WorkerAbortListener; -pub(crate) use event_listener::{ - worker_abort_signal_add_event_listener_callback, worker_abort_signal_dispatch_event_callback, - worker_abort_signal_remove_event_listener_callback, -}; +use crate::context_bootstrap::abort_signal_events; const WORKER_ABORT_SIGNAL_ID_SLOT: &str = "__lmWorkerAbortSignalId"; const WORKER_ABORT_CONTROLLER_ID_SLOT: &str = "__lmWorkerAbortControllerId"; @@ -30,7 +24,6 @@ const WORKER_ABORT_SIGNAL_REASON_SLOT: &str = "__lmWorkerAbortSignalReason"; pub(super) struct WorkerAbortStore { next_signal_id: u32, next_controller_id: u32, - next_listener_id: u64, signals: HashMap, controllers: HashMap, } @@ -40,8 +33,6 @@ pub(super) struct WorkerAbortSignalState { signal: Option>, aborted: bool, reason: Option>, - onabort: Option>, - listeners: HashMap>, abort_algorithms: Vec>, linked_message_port_listeners: Vec, dependent_signals: Vec, @@ -76,19 +67,6 @@ impl WorkerAbortStore { self.next_controller_id } - fn define_hidden_value( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, - key: &str, - value: v8::Local<'_, v8::Value>, - ) { - let Some(key) = v8_string(scope, key) else { - return; - }; - let _ = - object.define_own_property(scope, key.into(), value, v8::PropertyAttribute::DONT_ENUM); - } - pub(super) fn signal_id_from_object<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, @@ -109,10 +87,10 @@ impl WorkerAbortStore { .map(|value| value as u32) } - fn init_signal( + fn init_signal<'s>( &mut self, - scope: &mut v8::PinScope<'_, '_>, - signal: v8::Local<'_, v8::Object>, + scope: &mut v8::PinScope<'s, '_>, + signal: v8::Local<'s, v8::Object>, aborted: bool, reason: Option>, ) -> u32 { @@ -135,14 +113,15 @@ impl WorkerAbortStore { if let Some(reason) = reason { set_private_value(scope, signal, WORKER_ABORT_SIGNAL_REASON_SLOT, reason); } + abort_signal_events::initialize(scope, signal); signal_id } - fn init_controller( + fn init_controller<'s>( &mut self, - scope: &mut v8::PinScope<'_, '_>, + scope: &mut v8::PinScope<'s, '_>, controller: v8::Local<'_, v8::Object>, - signal: v8::Local<'_, v8::Object>, + signal: v8::Local<'s, v8::Object>, ) { let signal_id = self.init_signal(scope, signal, false, None); let controller_id = self.alloc_controller_id(); @@ -290,12 +269,7 @@ fn abort_worker_signal<'s>( let Some(signal_id) = WorkerAbortStore::signal_id_from_object(scope, signal) else { return; }; - let Some(( - abort_algorithms, - linked_message_port_listeners, - dependent_signals, - dispatch_snapshot, - )) = ({ + let Some((abort_algorithms, linked_message_port_listeners, dependent_signals)) = ({ let mut store = store.borrow_mut(); let Some(state) = store.signal_state_mut(signal_id) else { return; @@ -313,14 +287,11 @@ fn abort_worker_signal<'s>( abort_algorithms, linked_message_port_listeners, state.dependent_signals.clone(), - state.dispatch_snapshot("abort"), )) - }) - else { + }) else { return; }; - let signal = local_object_in_scope(scope, signal); reject_worker_fetches_for_signal(scope, signal_id, reason); invoke_worker_abort_algorithms(scope, signal, reason, abort_algorithms); for linked in linked_message_port_listeners { @@ -330,7 +301,7 @@ fn abort_worker_signal<'s>( linked.listener_id, ); } - event_listener::dispatch_abort(store, scope, signal, signal_id, dispatch_snapshot); + abort_signal_events::dispatch_abort(scope, signal); for dependent_signal_id in dependent_signals { let dependent_signal = { let store = store.borrow(); @@ -361,14 +332,6 @@ fn invoke_worker_abort_algorithms<'s>( } } -fn local_object_in_scope<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'_, v8::Object>, -) -> v8::Local<'s, v8::Object> { - let global = v8::Global::new(scope, object); - v8::Local::new(scope, global) -} - fn create_signal_with_prototype<'s>( scope: &mut v8::PinScope<'s, '_>, prototype_source: v8::Local<'_, v8::Object>, @@ -467,40 +430,6 @@ pub(crate) fn abort_worker_signal_by_id<'s>( abort_worker_signal(&store, scope, signal, reason); } -fn worker_abort_signal_onabort<'s>( - scope: &mut v8::PinScope<'s, '_>, - signal: v8::Local<'s, v8::Object>, -) -> Option> { - worker_abort_store(scope) - .and_then(|state| { - WorkerAbortStore::signal_id_from_object(scope, signal).and_then(|id| { - state - .borrow() - .signal_state(id) - .and_then(|s| s.onabort.clone()) - }) - }) - .map(|onabort| v8::Local::new(scope, &onabort)) -} - -fn set_worker_abort_signal_onabort<'s>( - scope: &mut v8::PinScope<'s, '_>, - signal: v8::Local<'s, v8::Object>, - value: v8::Local<'s, v8::Value>, -) { - let Some(store) = worker_abort_store(scope) else { - return; - }; - let Some(signal_id) = WorkerAbortStore::signal_id_from_object(scope, signal) else { - return; - }; - if let Some(signal_state) = store.borrow_mut().signal_state_mut(signal_id) { - signal_state.onabort = v8::Local::::try_from(value) - .ok() - .map(|function| v8::Global::new(scope, function)); - } -} - pub(super) fn worker_dom_exception_value<'s>( scope: &mut v8::PinScope<'s, '_>, message: &str, @@ -770,31 +699,6 @@ pub(crate) fn worker_abort_signal_reason_getter_function<'s>( rv.set(reason); } -pub(crate) fn worker_abort_signal_onabort_getter_function<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(onabort) = worker_abort_signal_onabort(scope, args.this()) else { - rv.set_null(); - return; - }; - rv.set(onabort.into()); -} - -pub(crate) fn worker_abort_signal_onabort_setter_function<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - if args.length() > 0 { - set_worker_abort_signal_onabort(scope, args.this(), args.get(0)); - } else { - set_worker_abort_signal_onabort(scope, args.this(), v8::undefined(scope).into()); - } - rv.set_undefined(); -} - pub(crate) fn worker_abort_signal_throw_if_aborted_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/worker/abort/event_listener.rs b/moli-renderer-v8/src/worker/abort/event_listener.rs deleted file mode 100644 index 0322b1fee6..0000000000 --- a/moli-renderer-v8/src/worker/abort/event_listener.rs +++ /dev/null @@ -1,435 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use moli_webidl_callback::{PreparedWebIdlCallbackInterface, WebIdlCallbackInterface}; - -use super::{WorkerAbortSignalState, WorkerAbortStore, local_object_in_scope, worker_abort_store}; -use crate::callback_invocation::{CallbackInvocation, CallbackInvocationOutcome, CallbackInvoker}; -use crate::context_bootstrap::{ - EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, event_internal_bool_flag, - set_event_internal_flag, -}; -use crate::exception_reporting::{CallbackExceptionLogLevel, invoke_callback}; -use crate::util::{v8_string, v8str}; -use crate::webidl; - -/// Identity of one EventListener registration inside a worker run. -/// -/// It never crosses the worker boundary. `WorkerAbortStore` is owned by the -/// worker isolate, so destroying or restarting that run retires every id and -/// callback context together. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub(super) struct WorkerAbortListenerId(u64); - -/// Signal-local registration state. -/// -/// The callback owns its conversion-time relevant/incumbent contexts. The -/// surrounding record owns only EventTarget policy: order, duplicate identity, -/// capture, once, and passive. -pub(super) struct WorkerAbortListener { - id: WorkerAbortListenerId, - callback: WebIdlCallbackInterface, - capture: bool, - once: bool, - passive: bool, -} - -/// An immutable view of which registrations existed when dispatch started. -/// -/// Only ids are copied. Every id is claimed again immediately before -/// invocation so removal by an earlier listener is observable, while listeners -/// added during dispatch wait for the next dispatch. -pub(super) struct WorkerAbortDispatchSnapshot { - listener_ids: Vec, - onabort: Option>, -} - -struct PreparedWorkerAbortListener { - callback: PreparedWebIdlCallbackInterface, - passive: bool, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "AbortSignal.addEventListener")] -struct WorkerAbortAddEventListenerArgs { - #[webidl(required)] - event_type: String, - #[webidl(required, converter = "callback_interface", nullable)] - listener: Option, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "AbortSignal.removeEventListener")] -struct WorkerAbortRemoveEventListenerArgs { - #[webidl(required)] - event_type: String, - #[webidl(required, converter = "callback_interface", nullable)] - listener: Option, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "AbortSignal.dispatchEvent")] -struct WorkerAbortDispatchEventArgs<'s> { - #[webidl(required)] - event: v8::Local<'s, v8::Value>, -} - -impl WorkerAbortSignalState { - pub(super) fn dispatch_snapshot(&self, event_type: &str) -> WorkerAbortDispatchSnapshot { - let listener_ids = self - .listeners - .get(event_type) - .into_iter() - .flatten() - .map(|listener| listener.id) - .collect(); - let onabort = (event_type == "abort") - .then(|| self.onabort.clone()) - .flatten(); - WorkerAbortDispatchSnapshot { - listener_ids, - onabort, - } - } -} - -impl WorkerAbortStore { - fn allocate_listener_id(&mut self) -> WorkerAbortListenerId { - self.next_listener_id = self - .next_listener_id - .checked_add(1) - .expect("worker AbortSignal listener id space exhausted"); - WorkerAbortListenerId(self.next_listener_id) - } - - fn register_event_listener( - &mut self, - scope: &mut v8::PinScope<'_, '_>, - signal_id: u32, - event_type: String, - callback: WebIdlCallbackInterface, - options: webidl::EventListenerOptions, - ) -> bool { - let callback_value = callback.value(scope); - let Ok(callback_object) = v8::Local::::try_from(callback_value) else { - return false; - }; - let Some(state) = self.signal_state(signal_id) else { - return false; - }; - if state - .listeners - .get(&event_type) - .into_iter() - .flatten() - .any(|listener| { - listener.capture == options.capture - && listener.callback.matches(scope, callback_object) - }) - { - return false; - } - - let id = self.allocate_listener_id(); - let state = self - .signal_state_mut(signal_id) - .expect("validated worker AbortSignal state must remain resident"); - state - .listeners - .entry(event_type) - .or_default() - .push(WorkerAbortListener { - id, - callback, - capture: options.capture, - once: options.once, - passive: options.passive.unwrap_or(false), - }); - true - } - - fn remove_event_listener( - &mut self, - scope: &mut v8::PinScope<'_, '_>, - signal_id: u32, - event_type: &str, - callback: &WebIdlCallbackInterface, - capture: bool, - ) -> bool { - let Some(state) = self.signal_state_mut(signal_id) else { - return false; - }; - let Some(listeners) = state.listeners.get_mut(event_type) else { - return false; - }; - let callback = callback.value(scope); - let Ok(callback) = v8::Local::::try_from(callback) else { - return false; - }; - let before = listeners.len(); - listeners.retain(|listener| { - listener.capture != capture || !listener.callback.matches(scope, callback) - }); - let removed = listeners.len() != before; - if listeners.is_empty() { - state.listeners.remove(event_type); - } - removed - } - - fn claim_event_listener( - &mut self, - scope: &mut v8::PinScope<'_, '_>, - signal_id: u32, - event_type: &str, - listener_id: WorkerAbortListenerId, - ) -> Option { - let state = self.signal_state_mut(signal_id)?; - let listeners = state.listeners.get_mut(event_type)?; - let index = listeners - .iter() - .position(|listener| listener.id == listener_id)?; - let callback = listeners[index].callback.prepare(scope); - let passive = listeners[index].passive; - if listeners[index].once { - listeners.remove(index); - } - if listeners.is_empty() { - state.listeners.remove(event_type); - } - Some(PreparedWorkerAbortListener { callback, passive }) - } -} - -pub(crate) fn worker_abort_signal_add_event_listener_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(store) = worker_abort_store(scope) else { - rv.set_undefined(); - return; - }; - let signal = args.this(); - let Some(signal_id) = WorkerAbortStore::signal_id_from_object(scope, signal) else { - rv.set_undefined(); - return; - }; - let Some(parsed) = webidl::parse_args::(scope, &args) else { - rv.set_undefined(); - return; - }; - let Some(listener) = parsed.listener else { - rv.set_undefined(); - return; - }; - let options = webidl::event_listener_options(scope, &args, 2, true); - store.borrow_mut().register_event_listener( - scope, - signal_id, - parsed.event_type, - listener, - options, - ); - rv.set_undefined(); -} - -pub(crate) fn worker_abort_signal_remove_event_listener_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(store) = worker_abort_store(scope) else { - rv.set_undefined(); - return; - }; - let signal = args.this(); - let Some(signal_id) = WorkerAbortStore::signal_id_from_object(scope, signal) else { - rv.set_undefined(); - return; - }; - let Some(parsed) = webidl::parse_args::(scope, &args) - else { - rv.set_undefined(); - return; - }; - let Some(listener) = parsed.listener else { - rv.set_undefined(); - return; - }; - let capture = webidl::event_listener_options(scope, &args, 2, true).capture; - store.borrow_mut().remove_event_listener( - scope, - signal_id, - &parsed.event_type, - &listener, - capture, - ); - rv.set_undefined(); -} - -pub(crate) fn worker_abort_signal_dispatch_event_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Some(store) = worker_abort_store(scope) else { - rv.set_bool(false); - return; - }; - let signal = args.this(); - let Some(parsed) = webidl::parse_args::>(scope, &args) else { - rv.set_bool(false); - return; - }; - let Ok(event) = v8::Local::::try_from(parsed.event) else { - rv.set_bool(false); - return; - }; - let Some(signal_id) = WorkerAbortStore::signal_id_from_object(scope, signal) else { - rv.set_bool(false); - return; - }; - let Some(event_type) = event - .get(scope, v8str(scope, "type").into()) - .and_then(|value| value.to_string(scope)) - .map(|value| value.to_rust_string_lossy(scope)) - else { - rv.set_bool(false); - return; - }; - - let default_prevented_key = v8str(scope, "defaultPrevented"); - let signal = local_object_in_scope(scope, signal); - let event = local_object_in_scope(scope, event); - WorkerAbortStore::define_hidden_value(scope, event, "target", signal.into()); - WorkerAbortStore::define_hidden_value(scope, event, "currentTarget", signal.into()); - let dispatch_snapshot = store - .borrow() - .signal_state(signal_id) - .map(|state| state.dispatch_snapshot(&event_type)); - if let Some(dispatch_snapshot) = dispatch_snapshot { - dispatch_event_callbacks( - &store, - scope, - signal, - signal_id, - &event_type, - dispatch_snapshot, - event, - ); - } - let result = event - .get(scope, default_prevented_key.into()) - .is_none_or(|value| !value.boolean_value(scope)); - rv.set_bool(result); -} - -pub(super) fn dispatch_abort<'s>( - store: &Rc>, - scope: &mut v8::PinScope<'s, '_>, - signal: v8::Local<'s, v8::Object>, - signal_id: u32, - dispatch_snapshot: WorkerAbortDispatchSnapshot, -) { - let global = scope.get_current_context().global(scope); - let Some(event_ctor) = global - .get(scope, v8str(scope, "Event").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { - return; - }; - let Some(event_type) = v8_string(scope, "abort") else { - return; - }; - let Some(event) = event_ctor.new_instance(scope, &[event_type.into()]) else { - return; - }; - WorkerAbortStore::define_hidden_value(scope, event, "target", signal.into()); - WorkerAbortStore::define_hidden_value(scope, event, "currentTarget", signal.into()); - dispatch_event_callbacks( - store, - scope, - signal, - signal_id, - "abort", - dispatch_snapshot, - event, - ); -} - -#[allow(clippy::too_many_arguments)] -fn dispatch_event_callbacks<'s>( - store: &Rc>, - scope: &mut v8::PinScope<'s, '_>, - signal: v8::Local<'s, v8::Object>, - signal_id: u32, - event_type: &str, - dispatch_snapshot: WorkerAbortDispatchSnapshot, - event: v8::Local<'s, v8::Object>, -) { - for listener_id in dispatch_snapshot.listener_ids { - let listener = - store - .borrow_mut() - .claim_event_listener(scope, signal_id, event_type, listener_id); - let Some(listener) = listener else { - continue; - }; - set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, listener.passive); - invoke_worker_abort_event_listener( - scope, - &format!("Worker AbortSignal {event_type} listener"), - listener.callback, - signal, - event, - ); - set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, false); - if event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) { - break; - } - } - - if event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) { - return; - } - if let Some(onabort) = dispatch_snapshot.onabort { - let onabort = v8::Local::new(scope, &onabort); - let _ = invoke_callback( - scope, - "Worker AbortSignal.onabort", - onabort, - signal.into(), - &[event.into()], - ); - } -} - -fn invoke_worker_abort_event_listener<'s>( - scope: &mut v8::PinScope<'s, '_>, - callback_name: &str, - callback: PreparedWebIdlCallbackInterface, - signal: v8::Local<'s, v8::Object>, - event: v8::Local<'s, v8::Object>, -) { - let arguments = [event.into()]; - let invocation = CallbackInvocation::new( - callback.callback(scope), - signal.into(), - callback.relevant_context(scope), - callback.incumbent_context(scope), - callback.callable_at_conversion(), - "handleEvent", - &arguments, - None, - ); - if let CallbackInvocationOutcome::Threw(report) = CallbackInvoker::invoke( - scope, - "event listener", - "worker AbortSignal listener threw", - CallbackExceptionLogLevel::Debug, - callback_name, - invocation, - ) { - let _ = crate::worker::dispatch_current_worker_callback_exception(scope, *report); - } -} diff --git a/moli-renderer-v8/src/worker/thread/tests/abort_signal_events.rs b/moli-renderer-v8/src/worker/thread/tests/abort_signal_events.rs new file mode 100644 index 0000000000..c169d4643b --- /dev/null +++ b/moli-renderer-v8/src/worker/thread/tests/abort_signal_events.rs @@ -0,0 +1,27 @@ +use super::*; + +#[tokio::test] +async fn worker_abort_signal_uses_shared_event_target_and_receiver_checks() { + ensure_v8(); + let probe = include_str!("../../../script_vm/tests/browser_api/abort_signal_events.js"); + let mut handle = spawn_worker( + format!( + "{probe}\npostMessage({{events: abortSignalEventTargetProbe(), receivers: abortSignalReceiverProbe()}}); close();" + ), + "https://abort-signal-events.test/worker.js".into(), + ); + let message = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap(); + let result: serde_json::Value = serde_json::from_str(&expect_post_json(message)).unwrap(); + assert_eq!( + result["events"]["failures"], + serde_json::json!([]), + "{result}" + ); + assert_eq!(result["events"]["scenarios"].as_array().unwrap().len(), 6); + assert_eq!( + result["receivers"]["failures"], + serde_json::json!([]), + "{result}" + ); + assert_eq!(result["receivers"]["checks"], 40); +} diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index 5d6d678fe0..3168f7b871 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -7897,7 +7897,7 @@ async fn worker_abort_signal_internal_id_is_not_page_visible_or_forgeable() { hasVisibleSlot: "__lmWorkerAbortSignalId" in signal, ownNames: Object.getOwnPropertyNames(signal), anyForged: probe(() => AbortSignal.any([forged])), - getterForged: abortedGetter.call(forged) + getterForged: probe(() => abortedGetter.call(forged)) }); close(); })(); @@ -7912,7 +7912,7 @@ async fn worker_abort_signal_internal_id_is_not_page_visible_or_forgeable() { .expect("channel closed"); assert_eq!( expect_post_json(msg), - r#"{"hasVisibleSlot":false,"ownNames":[],"anyForged":"TypeError","getterForged":false}"# + r#"{"hasVisibleSlot":false,"ownNames":[],"anyForged":"TypeError","getterForged":"TypeError"}"# ); } diff --git a/moli-renderer-v8/src/worker/thread/tests/mod.rs b/moli-renderer-v8/src/worker/thread/tests/mod.rs index da57d30077..05a3eabb9f 100644 --- a/moli-renderer-v8/src/worker/thread/tests/mod.rs +++ b/moli-renderer-v8/src/worker/thread/tests/mod.rs @@ -1,3 +1,4 @@ +mod abort_signal_events; use std::pin::pin; use std::sync::Arc; use std::time::Duration; diff --git a/moli-webidl/src/helpers.rs b/moli-webidl/src/helpers.rs index 1aff94c31c..c77f4f05f1 100644 --- a/moli-webidl/src/helpers.rs +++ b/moli-webidl/src/helpers.rs @@ -232,3 +232,25 @@ pub fn event_listener_once_option<'s>( ) -> bool { event_listener_options(scope, args, index, false).once } + +/// Converts `(AddEventListenerOptions or boolean)` through `passive`. +/// +/// The caller must convert the platform-specific `signal` member next, before +/// changing the listener list. Getter exceptions stop conversion immediately. +pub fn add_event_listener_options_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, +) -> Result { + let Ok(object) = v8::Local::::try_from(value) else { + return Ok(EventListenerOptions { + capture: value.boolean_value(scope), + ..EventListenerOptions::default() + }); + }; + let parsed = parse_dictionary_object::(scope, object)?; + Ok(EventListenerOptions { + capture: parsed.capture, + once: parsed.once, + passive: parsed.passive, + }) +} diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index c41ccb40da..092bea0471 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -49,11 +49,11 @@ pub use convert::{ }; pub use error::{Context, WebIdlError, WebIdlErrorKind}; pub use helpers::{ - dictionary_arg, dictionary_value, event_listener_once_option, event_listener_once_value, - event_listener_options, event_listener_options_value, is_nullish, optional_number_property, - optional_object_arg, optional_string_property, property, property_non_nullish, - property_non_undefined, property_result, symbol_property_result, throw_dom_exception, - throw_error, throw_index_size_error, throw_type_error, v8_string, + add_event_listener_options_value, dictionary_arg, dictionary_value, event_listener_once_option, + event_listener_once_value, event_listener_options, event_listener_options_value, is_nullish, + optional_number_property, optional_object_arg, optional_string_property, property, + property_non_nullish, property_non_undefined, property_result, symbol_property_result, + throw_dom_exception, throw_error, throw_index_size_error, throw_type_error, v8_string, }; pub use moli_webidl_callback::{ PreparedWebIdlCallbackFunction, PreparedWebIdlCallbackInterface, WebIdlCallbackFunction, diff --git a/moli-wpt-compat/fixtures/wpt/ported/fileapi/filereader-basic.html b/moli-wpt-compat/fixtures/wpt/ported/fileapi/filereader-basic.html index 81f59e0797..628e54c7a4 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/fileapi/filereader-basic.html +++ b/moli-wpt-compat/fixtures/wpt/ported/fileapi/filereader-basic.html @@ -69,10 +69,20 @@ test(function () { reader.addEventListener(undefined, listener); reader.removeEventListener(undefined, listener); - reader.addEventListener("load"); + assert_throws_name( + "TypeError", + () => reader.addEventListener("load"), + "addEventListener callback is required" + ); + assert_throws_name( + "TypeError", + () => reader.removeEventListener("load"), + "removeEventListener callback is required" + ); reader.addEventListener("load", null); - reader.removeEventListener("load"); + reader.addEventListener("load", undefined); reader.removeEventListener("load", null); + reader.removeEventListener("load", undefined); }, "FileReader listener arguments follow WebIDL boundary conversion"); test(function () {