mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
fix(events): share AbortSignal EventTarget dispatch (#769)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -133,6 +133,7 @@ pub(crate) struct CallbackInvocation<'s, 'a> {
|
||||
relevant_identity: Option<WindowExecutionContextIdentity>,
|
||||
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<v8::Local<'s, v8::Object>>,
|
||||
@@ -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::Value> = 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)
|
||||
|
||||
@@ -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: (),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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::<v8::Object>::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::<crate::native_bridge::RuntimeObservableContextToken>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -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::{
|
||||
|
||||
@@ -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>,
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+59
-54
@@ -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::<v8::Object>::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);
|
||||
}
|
||||
}
|
||||
|
||||
+77
-145
@@ -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<Self> {
|
||||
// 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<v8::Local<'s, v8::Object>>,
|
||||
) -> 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<SimpleObjectAddListenerCall<'s>>,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "EventTarget.removeEventListener")]
|
||||
struct SimpleObjectRemoveListenerArgs<'s> {
|
||||
#[webidl(with = simple_object_remove_listener_call)]
|
||||
call: webidl::ParseOutcome<SimpleObjectRemoveListenerCall<'s>>,
|
||||
}
|
||||
|
||||
struct SimpleObjectAddListenerCall<'s> {
|
||||
event_type: String,
|
||||
listener: SimpleObjectResolvedEventListener<'s>,
|
||||
options: webidl::EventListenerOptions,
|
||||
signal: Option<ResolvedAbortSignal<'s>>,
|
||||
}
|
||||
|
||||
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<String, webidl::WebIdlError> {
|
||||
if args.length() == 0 {
|
||||
return Err(webidl::WebIdlError::custom_message(missing_message));
|
||||
}
|
||||
webidl::convert::<webidl::DomString>(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::ParseOutcome<SimpleObjectAddListenerCall<'s>>, 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::ParseOutcome<SimpleObjectRemoveListenerCall<'s>>, 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::<SimpleObjectAddListenerArgs>(scope, args) else {
|
||||
let Some(call) = webidl::parse_args::<AddEventListenerArgs>(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::<v8::Object>::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::<SimpleObjectRemoveListenerArgs>(scope, args) else {
|
||||
let Some(call) = webidl::parse_args::<RemoveEventListenerArgs>(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::<v8::Object>::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<SimpleObjectResolvedEventListener<'s>> {
|
||||
if value.is_null_or_undefined() {
|
||||
return None;
|
||||
}
|
||||
let callback = v8::Local::<v8::Object>::try_from(value).ok()?;
|
||||
listener: webidl::WebIdlCallbackInterface,
|
||||
) -> SimpleObjectResolvedEventListener<'s> {
|
||||
let callback_value = listener.value(scope);
|
||||
let callback = v8::Local::<v8::Object>::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,
|
||||
|
||||
@@ -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: (),
|
||||
|
||||
@@ -56,6 +56,14 @@ fn setup_worker_context() -> (v8::OwnedIsolate, v8::Global<v8::Context>) {
|
||||
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}`);
|
||||
|
||||
@@ -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::WebIdlCallbackInterface>,
|
||||
#[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::WebIdlCallbackInterface>,
|
||||
#[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<ResolvedAbortSignal<'s>>,
|
||||
}
|
||||
|
||||
fn add_event_listener_options<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
index: i32,
|
||||
) -> Result<AddEventListenerOptions<'s>, 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<webidl::EventListenerOptions, webidl::WebIdlError> {
|
||||
let value = args.get(index);
|
||||
let capture = if let Ok(object) = v8::Local::<v8::Object>::try_from(value) {
|
||||
webidl::parse_dictionary_object::<RemoveEventListenerOptions>(scope, object)?.capture
|
||||
} else {
|
||||
value.boolean_value(scope)
|
||||
};
|
||||
Ok(webidl::EventListenerOptions {
|
||||
capture,
|
||||
..webidl::EventListenerOptions::default()
|
||||
})
|
||||
}
|
||||
@@ -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::{
|
||||
|
||||
@@ -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<v8::Global<v8::Value>> {
|
||||
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, '_>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<v8::Global<v8::Object>>,
|
||||
aborted: bool,
|
||||
reason: Option<v8::Global<v8::Value>>,
|
||||
onabort: Option<v8::Global<v8::Function>>,
|
||||
listeners: HashMap<String, Vec<AbortListener>>,
|
||||
abort_algorithms: Vec<v8::Global<v8::Function>>,
|
||||
linked_target_listeners: Vec<AbortLinkedTargetListener>,
|
||||
linked_message_port_listeners: Vec<AbortLinkedMessagePortListener>,
|
||||
dependent_signals: Vec<u32>,
|
||||
}
|
||||
|
||||
#[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<AbortListener>,
|
||||
onabort: Option<v8::Global<v8::Function>>,
|
||||
}
|
||||
|
||||
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<v8::Local<'_, v8::Value>>,
|
||||
) -> 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<super::EventCallbackId> {
|
||||
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<AbortListener> {
|
||||
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<super::EventCallbackId>,
|
||||
) {
|
||||
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,
|
||||
|
||||
@@ -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::<v8::Function>::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()],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<webidl::WebIdlCallbackInterface>,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "AbortSignal.removeEventListener")]
|
||||
struct AbortSignalRemoveEventListenerArgs {
|
||||
#[webidl(required)]
|
||||
event_type: String,
|
||||
#[webidl(required, converter = "callback_interface", nullable)]
|
||||
listener: Option<webidl::WebIdlCallbackInterface>,
|
||||
}
|
||||
|
||||
#[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::<AbortSignalAddEventListenerArgs>(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::<AbortSignalRemoveEventListenerArgs>(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::<AbortSignalDispatchEventArgs<'s>>(scope, &args) else {
|
||||
rv.set_bool(false);
|
||||
return;
|
||||
};
|
||||
let Ok(event) = v8::Local::<v8::Object>::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::<v8::Function>::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>,
|
||||
|
||||
@@ -319,9 +319,6 @@ impl JsContextHost {
|
||||
}
|
||||
|
||||
fn release_retired_event_callbacks(&mut self, retired: HashSet<EventCallbackId>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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::<v8::Object>::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::<v8::Object>::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<crate::native_bridge::abort::PreparedAbortListener> {
|
||||
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, '_>,
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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/", "<!doctype html><body>");
|
||||
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);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
mod abort_signal_events;
|
||||
mod broadcast_channel;
|
||||
mod chrome;
|
||||
mod clipboard_storage;
|
||||
|
||||
@@ -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"}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
|
||||
|
||||
@@ -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<u32, WorkerAbortSignalState>,
|
||||
controllers: HashMap<u32, u32>,
|
||||
}
|
||||
@@ -40,8 +33,6 @@ pub(super) struct WorkerAbortSignalState {
|
||||
signal: Option<v8::Global<v8::Object>>,
|
||||
aborted: bool,
|
||||
reason: Option<v8::Global<v8::Value>>,
|
||||
onabort: Option<v8::Global<v8::Function>>,
|
||||
listeners: HashMap<String, Vec<WorkerAbortListener>>,
|
||||
abort_algorithms: Vec<v8::Global<v8::Function>>,
|
||||
linked_message_port_listeners: Vec<WorkerAbortLinkedMessagePortListener>,
|
||||
dependent_signals: Vec<u32>,
|
||||
@@ -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<v8::Local<'_, v8::Value>>,
|
||||
) -> 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<v8::Local<'s, v8::Function>> {
|
||||
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::<v8::Function>::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>,
|
||||
|
||||
@@ -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<WorkerAbortListenerId>,
|
||||
onabort: Option<v8::Global<v8::Function>>,
|
||||
}
|
||||
|
||||
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<WebIdlCallbackInterface>,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "AbortSignal.removeEventListener")]
|
||||
struct WorkerAbortRemoveEventListenerArgs {
|
||||
#[webidl(required)]
|
||||
event_type: String,
|
||||
#[webidl(required, converter = "callback_interface", nullable)]
|
||||
listener: Option<WebIdlCallbackInterface>,
|
||||
}
|
||||
|
||||
#[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::<v8::Object>::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::<v8::Object>::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<PreparedWorkerAbortListener> {
|
||||
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::<WorkerAbortAddEventListenerArgs>(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::<WorkerAbortRemoveEventListenerArgs>(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::<WorkerAbortDispatchEventArgs<'s>>(scope, &args) else {
|
||||
rv.set_bool(false);
|
||||
return;
|
||||
};
|
||||
let Ok(event) = v8::Local::<v8::Object>::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<RefCell<WorkerAbortStore>>,
|
||||
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::<v8::Function>::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<RefCell<WorkerAbortStore>>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod abort_signal_events;
|
||||
use std::pin::pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -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<EventListenerOptions, WebIdlError> {
|
||||
let Ok(object) = v8::Local::<v8::Object>::try_from(value) else {
|
||||
return Ok(EventListenerOptions {
|
||||
capture: value.boolean_value(scope),
|
||||
..EventListenerOptions::default()
|
||||
});
|
||||
};
|
||||
let parsed = parse_dictionary_object::<EventListenerOptionsMembers>(scope, object)?;
|
||||
Ok(EventListenerOptions {
|
||||
capture: parsed.capture,
|
||||
once: parsed.once,
|
||||
passive: parsed.passive,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user