From a35f2006d244589dfb5ba849d5da3c0255cebbf9 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 10 Sep 2026 12:51:00 +0800 Subject: [PATCH] fix: construct trusted abort events and reset AbortSignal dispatch state Worker abort events now use the signal realm's intrinsic Event constructor and carry the native trust flag, including timeout and stream cancellation. Share AbortSignal dispatch setup and cleanup between Window and Worker. Script dispatch validates Event state and clears trust, active redispatch throws, and completion clears currentTarget, eventPhase, the event path, and propagation flags. Pre-stopped events skip listeners and remain reusable. Cover controller, composite, stream and timeout events with overwritten or missing public constructors, plus synthetic dispatch and native event reuse, in Window, child frames and dedicated workers. --- .../wpt-cross-current/passed-cases.txt | 1 + .../fixtures/runtime/abort_signal_events.js | 154 ++++++++++++++++++ moli-core/tests/web_apis/abort_signal.rs | 58 +++++++ moli-renderer-v8/src/context_bootstrap.rs | 1 + .../context_bootstrap/abort_signal_events.rs | 62 +++++++ moli-renderer-v8/src/native_bridge/abort.rs | 15 +- .../src/native_bridge/abort/event.rs | 17 +- .../src/native_bridge/abort/signal.rs | 13 +- moli-renderer-v8/src/worker/abort.rs | 13 -- .../src/worker/abort/event_listener.rs | 49 +++--- 10 files changed, 308 insertions(+), 75 deletions(-) create mode 100644 moli-core/tests/fixtures/runtime/abort_signal_events.js create mode 100644 moli-renderer-v8/src/context_bootstrap/abort_signal_events.rs diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 3db01e2c29..3be702bac4 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -3760,6 +3760,7 @@ dom/abort/abort-signal-any.any.js?moli-wpt-any=window dom/abort/abort-signal-timeout.html dom/abort/crashtests/any-on-abort.html dom/abort/crashtests/timeout-close.html +dom/abort/event.any.js?moli-wpt-any=dedicatedworker dom/abort/event.any.js?moli-wpt-any=window dom/abort/reason-constructor.html dom/attributes-are-nodes.html diff --git a/moli-core/tests/fixtures/runtime/abort_signal_events.js b/moli-core/tests/fixtures/runtime/abort_signal_events.js new file mode 100644 index 0000000000..9c24c5da0f --- /dev/null +++ b/moli-core/tests/fixtures/runtime/abort_signal_events.js @@ -0,0 +1,154 @@ +async function runAbortSignalEventProbe(scenario) { + if (typeof document !== 'undefined' && document.readyState !== 'complete') { + await new Promise(resolve => addEventListener('load', resolve, {once: true})); + } + // Let document lifecycle events finish before replacing the public constructor. + await new Promise(resolve => setTimeout(resolve, 0)); + const errors = []; + const events = []; + let reads = 0; + const check = (condition, label) => { if (!condition) errors.push(label); }; + const during = (event, signal, trusted, label) => { + check(event.target === signal && event.srcElement === signal, label + ': target'); + check(event.currentTarget === signal && event.eventPhase === 2, label + ': dispatch fields'); + check(event.isTrusted === trusted, label + ': trust'); + const path = event.composedPath(); + check(path.length === 1 && path[0] === signal, label + ': path'); + }; + const after = (event, signal, label) => { + check(event.target === signal, label + ': target retained'); + check(event.currentTarget === null && event.eventPhase === 0, label + ': dispatch fields cleared'); + check(event.composedPath().length === 0 && !event.cancelBubble, label + ': propagation cleared'); + }; + const throws = (callback, name, label) => { + let caught = false; + try { callback(); } + catch (error) { caught = true; check(error.name === name, label + ': exception type'); } + check(caught, label + ': throws'); + }; + + try { + if (scenario.startsWith('native-')) { + const original = Object.getOwnPropertyDescriptor(globalThis, 'Event'); + const observed = []; + const onerror = event => { errors.push('uncaught: ' + event.message); event.preventDefault(); }; + addEventListener('error', onerror); + let deadline; + try { + if (scenario === 'native-getter') { + Object.defineProperty(globalThis, 'Event', {configurable: true, get() { + reads++; throw new Error('public Event getter'); + }}); + } else if (scenario === 'native-replacement') { + Object.defineProperty(globalThis, 'Event', {configurable: true, value: function () { + reads++; throw new Error('public Event constructor'); + }}); + } else if (scenario === 'native-missing') { + delete globalThis.Event; + } + const record = (signal, label) => signal.addEventListener('abort', function (event) { + events.push(label); + observed.push([event, signal, label]); + during(event, signal, true, label); + check(this === signal && event.type === 'abort', label + ': callback'); + check(!event.bubbles && !event.cancelable && !event.composed, label + ': defaults'); + event.preventDefault(); + check(!event.defaultPrevented, label + ': uncancelable'); + }); + const controller = new AbortController(); + const composite = AbortSignal.any([controller.signal]); + record(controller.signal, 'source'); + record(composite, 'composite'); + const reason = {cancel: true}; + controller.abort(reason); + check(events.join(',') === 'source,composite', 'controller events are synchronous'); + check(composite.reason === reason, 'composite reason identity'); + const writable = new WritableStream({start(controller) { record(controller.signal, 'stream'); }}); + await writable.abort(reason); + const timeout = AbortSignal.timeout(0); + record(timeout, 'timeout'); + await new Promise(resolve => { + deadline = setTimeout(() => { errors.push('timeout abort event missing'); resolve(); }, 500); + timeout.onabort = () => { clearTimeout(deadline); resolve(); }; + }); + check(timeout.reason.name === 'TimeoutError', 'timeout reason'); + // Promise reactions can run at callback cleanup before native dispatch ends. + await new Promise(resolve => setTimeout(resolve, 0)); + } finally { + clearTimeout(deadline); + Object.defineProperty(globalThis, 'Event', original); + removeEventListener('error', onerror); + } + check(reads === 0, 'native event construction does not access public Event'); + check(new Set(observed.map(([event]) => event)).size === 4, 'each signal gets a distinct event'); + for (const [event, signal, label] of observed) { + check(event instanceof Event, label + ': intrinsic prototype'); + check(event.isTrusted, label + ': stays trusted after native dispatch'); + after(event, signal, label); + } + } else if (scenario === 'script') { + const source = new AbortController(); + const recipient = new AbortController(); + let nativeEvent; + source.signal.addEventListener('abort', event => { + nativeEvent = event; + events.push('native'); + during(event, source.signal, true, 'native'); + throws(() => recipient.signal.dispatchEvent(event), 'InvalidStateError', 'active redispatch'); + during(event, source.signal, true, 'rejected redispatch preserves active event'); + event.initEvent('changed', true, true); + check(event.type === 'abort' && !event.bubbles && !event.cancelable, 'active initEvent is ignored'); + event.stopImmediatePropagation(); + }); + source.signal.addEventListener('abort', () => errors.push('stopped listener fired')); + source.abort('reason'); + after(nativeEvent, source.signal, 'native'); + recipient.signal.onabort = event => { + events.push('replayed'); + during(event, recipient.signal, false, 'replayed'); + check(!recipient.signal.aborted && recipient.signal.reason === undefined, 'script event leaves signal pending'); + event.stopPropagation(); + }; + check(recipient.signal.dispatchEvent(nativeEvent), 'replayed abort is uncancelable'); + check(!nativeEvent.isTrusted, 'redispatch clears trust'); + after(nativeEvent, recipient.signal, 'replayed'); + const cancelable = new Event('probe', {cancelable: true}); + recipient.signal.addEventListener('probe', event => { + events.push('passive'); + during(event, recipient.signal, false, 'synthetic'); + const before = event.defaultPrevented; + event.preventDefault(); + check(event.defaultPrevented === before, 'passive cancellation is ignored'); + }, {passive: true}); + recipient.signal.addEventListener('probe', event => { + events.push('active'); + event.preventDefault(); + event.stopImmediatePropagation(); + }, {once: true}); + recipient.signal.addEventListener('probe', () => events.push('tail')); + check(!recipient.signal.dispatchEvent(cancelable), 'canceling a synthetic event returns false'); + after(cancelable, recipient.signal, 'synthetic'); + check(!recipient.signal.dispatchEvent(cancelable), 'redispatch preserves cancellation'); + after(cancelable, recipient.signal, 'synthetic again'); + const stopped = new Event('pre-stopped'); + recipient.signal.addEventListener('pre-stopped', () => events.push('pre-stopped')); + stopped.stopPropagation(); + recipient.signal.dispatchEvent(stopped); + check(!events.includes('pre-stopped'), 'pre-stopped event skips listeners'); + after(stopped, recipient.signal, 'pre-stopped'); + recipient.signal.dispatchEvent(stopped); + check(events.includes('pre-stopped'), 'pre-stopped event can be dispatched again'); + for (const value of [null, undefined, {}, {type: 'abort'}, Object.create(Event.prototype)]) { + throws(() => recipient.signal.dispatchEvent(value), 'TypeError', 'non-Event argument'); + } + if (typeof document !== 'undefined') { + throws(() => recipient.signal.dispatchEvent(document.createEvent('Event')), 'InvalidStateError', 'uninitialized Event'); + } + } else { + errors.push('unknown scenario ' + scenario); + } + } catch (error) { + errors.push(String(error)); + } + return {errors, events, reads}; +} diff --git a/moli-core/tests/web_apis/abort_signal.rs b/moli-core/tests/web_apis/abort_signal.rs index b392735081..c2a892bb6a 100644 --- a/moli-core/tests/web_apis/abort_signal.rs +++ b/moli-core/tests/web_apis/abort_signal.rs @@ -49,3 +49,61 @@ async fn abort_signal_any_runs_listener_removal_before_dispatch_and_snapshots_ea ) .await } + +async fn assert_abort_signal_events(scenarios: &[&str], events: &[&str]) -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + for scenario in scenarios { + let source = format!( + "{}\nrunAbortSignalEventProbe({}).then(finish, error => finish({{error: String(error)}}));", + include_str!("../fixtures/runtime/abort_signal_events.js"), + serde_json::to_string(scenario)?, + ); + for target in ["window", "child", "worker"] { + let observed = tokio::time::timeout( + Duration::from_secs(10), + super::event_dispatch::run_probe(&browser, &server, target, &source), + ) + .await??; + assert_eq!( + observed, + serde_json::json!({"errors": [], "events": events, "reads": 0}), + "{scenario}/{target}" + ); + } + } + server.shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn abort_signal_native_events_use_intrinsic_constructors_and_preserve_trust() -> Result<()> { + assert_abort_signal_events( + &[ + "native-original", + "native-getter", + "native-replacement", + "native-missing", + ], + &["source", "composite", "stream", "timeout"], + ) + .await +} + +#[tokio::test(flavor = "multi_thread")] +async fn abort_signal_script_dispatch_validates_events_resets_trust_and_clears_dispatch_state() +-> Result<()> { + assert_abort_signal_events( + &["script"], + &[ + "native", + "replayed", + "passive", + "active", + "passive", + "tail", + "pre-stopped", + ], + ) + .await +} diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 14f52017a9..b7b7b4cda5 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -1,3 +1,4 @@ +pub(crate) mod abort_signal_events; mod animation_runtime; mod assets; pub(crate) mod bridge_descriptor; diff --git a/moli-renderer-v8/src/context_bootstrap/abort_signal_events.rs b/moli-renderer-v8/src/context_bootstrap/abort_signal_events.rs new file mode 100644 index 0000000000..e45bf84e31 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/abort_signal_events.rs @@ -0,0 +1,62 @@ +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, '_>, + value: v8::Local<'s, v8::Value>, +) -> Option<(v8::Local<'s, v8::Object>, String)> { + let event = v8::Local::::try_from(value).ok(); + let Some((event, initialized)) = + event.and_then(|event| event_initialized(scope, event).map(|flag| (event, flag))) + else { + throw_type_error(scope, "AbortSignal.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; + } + 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)) +} + +// 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, '_>, + signal: v8::Local<'s, v8::Object>, + event: v8::Local<'s, v8::Object>, +) -> bool { + set_event_dispatch_fields(scope, signal, event); + let path = v8::Array::new_with_elements(scope, &[signal.into()]); + set_event_composed_path(scope, event, path); + !event_internal_bool_flag(scope, event, EVENT_STOP_PROPAGATION_SLOT) +} + +pub(crate) fn finish_dispatch<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, +) { + clear_event_dispatch_fields(scope, event); + clear_event_composed_path(scope, event); + for flag in [ + EVENT_STOP_PROPAGATION_SLOT, + EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, + EVENT_PASSIVE_SLOT, + ] { + set_event_internal_flag(scope, event, flag, false); + } +} diff --git a/moli-renderer-v8/src/native_bridge/abort.rs b/moli-renderer-v8/src/native_bridge/abort.rs index 2d268f2b39..788ff96f3a 100644 --- a/moli-renderer-v8/src/native_bridge/abort.rs +++ b/moli-renderer-v8/src/native_bridge/abort.rs @@ -2,7 +2,7 @@ use crate::web_api_interfaces; use std::collections::{HashMap, HashSet}; 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; @@ -151,19 +151,6 @@ 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( &mut self, scope: &mut v8::PinScope<'_, '_>, diff --git a/moli-renderer-v8/src/native_bridge/abort/event.rs b/moli-renderer-v8/src/native_bridge/abort/event.rs index 2672363767..a0dcedc5a0 100644 --- a/moli-renderer-v8/src/native_bridge/abort/event.rs +++ b/moli-renderer-v8/src/native_bridge/abort/event.rs @@ -1,4 +1,5 @@ -use super::{AbortDispatchSnapshot, AbortStore}; +use super::AbortDispatchSnapshot; +use crate::context_bootstrap::abort_signal_events; use crate::context_bootstrap::{ EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, construct_original_event, event_internal_bool_flag, set_event_internal_flag, @@ -47,9 +48,6 @@ pub(super) fn dispatch_abort<'s>( let Some(event) = construct_original_event(scope, "abort") 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, @@ -78,6 +76,10 @@ pub(super) fn invoke_abort_event_callbacks<'s>( event_type: &str, event: v8::Local<'s, v8::Object>, ) { + if !abort_signal_events::begin_dispatch(scope, signal, event) { + abort_signal_events::finish_dispatch(scope, event); + return; + } for listener in dispatch_snapshot.listeners { let Some(listener) = (unsafe { &mut *host_ptr }) .claim_abort_signal_event_listener_for_dispatch( @@ -105,10 +107,8 @@ pub(super) fn invoke_abort_event_callbacks<'s>( break; } } - if event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) { - return; - } - if event_type == "abort" + if !event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) + && event_type == "abort" && let Some(onabort) = dispatch_snapshot.onabort { let onabort = v8::Local::new(scope, &onabort); @@ -120,4 +120,5 @@ pub(super) fn invoke_abort_event_callbacks<'s>( &[event.into()], ); } + abort_signal_events::finish_dispatch(scope, event); } diff --git a/moli-renderer-v8/src/native_bridge/abort/signal.rs b/moli-renderer-v8/src/native_bridge/abort/signal.rs index a9597afe2f..7f32c1fe00 100644 --- a/moli-renderer-v8/src/native_bridge/abort/signal.rs +++ b/moli-renderer-v8/src/native_bridge/abort/signal.rs @@ -1,5 +1,6 @@ use super::AbortStore; use super::event::{invoke_abort_event_callbacks, local_object_in_scope}; +use crate::context_bootstrap::abort_signal_events; use crate::util::{context_host_ptr_from_global_bridge, v8str}; use crate::webidl; @@ -109,18 +110,12 @@ pub(crate) fn abort_signal_dispatch_event_callback<'s>( rv.set_bool(false); return; }; - let Ok(event) = v8::Local::::try_from(parsed.event) else { - rv.set_bool(false); - return; - }; let Some(signal_id) = AbortStore::signal_id_from_object(scope, signal) else { rv.set_bool(false); return; }; - let Some(event_type) = event - .get(scope, v8str(scope, "type").into()) - .and_then(|value| value.to_string(scope)) - .map(|s| s.to_rust_string_lossy(scope)) + let Some((event, event_type)) = + abort_signal_events::prepare_script_dispatch(scope, parsed.event) else { rv.set_bool(false); return; @@ -132,8 +127,6 @@ pub(crate) fn abort_signal_dispatch_event_callback<'s>( // 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 diff --git a/moli-renderer-v8/src/worker/abort.rs b/moli-renderer-v8/src/worker/abort.rs index 1c42f6a76a..2350ef59c8 100644 --- a/moli-renderer-v8/src/worker/abort.rs +++ b/moli-renderer-v8/src/worker/abort.rs @@ -78,19 +78,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>, diff --git a/moli-renderer-v8/src/worker/abort/event_listener.rs b/moli-renderer-v8/src/worker/abort/event_listener.rs index 0322b1fee6..495e1c0291 100644 --- a/moli-renderer-v8/src/worker/abort/event_listener.rs +++ b/moli-renderer-v8/src/worker/abort/event_listener.rs @@ -4,12 +4,13 @@ use moli_webidl_callback::{PreparedWebIdlCallbackInterface, WebIdlCallbackInterf use super::{WorkerAbortSignalState, WorkerAbortStore, local_object_in_scope, worker_abort_store}; use crate::callback_invocation::{CallbackInvocation, CallbackInvocationOutcome, CallbackInvoker}; +use crate::context_bootstrap::abort_signal_events; use crate::context_bootstrap::{ - EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, event_internal_bool_flag, - set_event_internal_flag, + EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, construct_original_event, + event_internal_bool_flag, set_event_internal_flag, }; use crate::exception_reporting::{CallbackExceptionLogLevel, invoke_callback}; -use crate::util::{v8_string, v8str}; +use crate::util::v8str; use crate::webidl; /// Identity of one EventListener registration inside a worker run. @@ -281,18 +282,12 @@ pub(crate) fn worker_abort_signal_dispatch_event_callback<'s>( rv.set_bool(false); return; }; - let Ok(event) = v8::Local::::try_from(parsed.event) else { - rv.set_bool(false); - return; - }; let Some(signal_id) = WorkerAbortStore::signal_id_from_object(scope, signal) else { rv.set_bool(false); return; }; - let Some(event_type) = event - .get(scope, v8str(scope, "type").into()) - .and_then(|value| value.to_string(scope)) - .map(|value| value.to_rust_string_lossy(scope)) + let Some((event, event_type)) = + abort_signal_events::prepare_script_dispatch(scope, parsed.event) else { rv.set_bool(false); return; @@ -301,8 +296,6 @@ pub(crate) fn worker_abort_signal_dispatch_event_callback<'s>( 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) @@ -331,21 +324,13 @@ pub(super) fn dispatch_abort<'s>( signal_id: u32, dispatch_snapshot: WorkerAbortDispatchSnapshot, ) { - let global = scope.get_current_context().global(scope); - let Some(event_ctor) = global - .get(scope, v8str(scope, "Event").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { + let context = signal + .get_creation_context(scope) + .unwrap_or_else(|| scope.get_current_context()); + let scope = &mut v8::ContextScope::new(scope, context); + let Some(event) = construct_original_event(scope, "abort") 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, @@ -367,6 +352,10 @@ fn dispatch_event_callbacks<'s>( dispatch_snapshot: WorkerAbortDispatchSnapshot, event: v8::Local<'s, v8::Object>, ) { + if !abort_signal_events::begin_dispatch(scope, signal, event) { + abort_signal_events::finish_dispatch(scope, event); + return; + } for listener_id in dispatch_snapshot.listener_ids { let listener = store @@ -389,10 +378,9 @@ fn dispatch_event_callbacks<'s>( } } - if event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) { - return; - } - if let Some(onabort) = dispatch_snapshot.onabort { + if !event_internal_bool_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT) + && let Some(onabort) = dispatch_snapshot.onabort + { let onabort = v8::Local::new(scope, &onabort); let _ = invoke_callback( scope, @@ -402,6 +390,7 @@ fn dispatch_event_callbacks<'s>( &[event.into()], ); } + abort_signal_events::finish_dispatch(scope, event); } fn invoke_worker_abort_event_listener<'s>(