fix(history): settle retired traversals outside host borrows

This commit is contained in:
ldm0
2026-09-24 19:29:26 +08:00
parent 26ad4f81bb
commit e5ad399b4f
24 changed files with 373 additions and 159 deletions
+1 -4
View File
@@ -67,10 +67,7 @@ impl<'s> ResolvedAbortSignal<'s> {
pub(crate) fn abort(self, scope: &mut v8::PinScope<'s, '_>, reason: v8::Local<'s, v8::Value>) {
match self.owner {
AbortSignalOwner::Window => {
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return;
};
unsafe { &mut *host_ptr }.abort_signal(scope, self.signal, reason);
crate::native_bridge::abort::abort_signal(scope, self.signal, reason);
}
AbortSignalOwner::Worker => {
let Some(signal_id) =
+4 -2
View File
@@ -139,7 +139,9 @@ pub(crate) use location_navigation::{
navigate_location_object_with_source_element, navigate_top_level_meta_refresh,
navigate_top_level_same_document_from_browser,
};
pub(crate) use navigation_cancellation::inform_about_canceled_navigation_for_window;
pub(crate) use navigation_cancellation::{
NavigationCancellationReason, inform_about_canceled_navigation_for_window,
};
pub(crate) use navigation_events::dispatch_cross_document_navigation_navigate_event_for_window_with_form_data;
pub(crate) use navigation_events::{
construct_original_hash_change_event, dispatch_beforeunload_for_runtime_owner,
@@ -152,7 +154,7 @@ pub(crate) use navigation_history_pruning::{
pub(crate) use navigation_result::{
NavigationFinishedResultApplication, apply_pending_navigation_finished_result,
};
pub(crate) use navigation_traversal_coordinator::cancel_history_traversals_for_retiring_window;
pub(crate) use navigation_traversal_coordinator::abort_history_traversal_admissions;
pub(crate) use navigation_traversal_execution::apply_authorized_history_traversal_task;
pub(crate) use performance_runtime::PERFORMANCE_TIME_ORIGIN_SLOT;
pub(crate) use performance_runtime::performance_slot_number;
@@ -243,10 +243,8 @@ pub(in crate::context_bootstrap) fn finish_history_participant<'s>(
if let Some(error) = error {
finish_navigation_error_events(scope, navigation, error, &applied.url);
reject_resolver_array(scope, finished_resolvers, error, true);
if let Some(signal) = outcome.signal
&& let Some(host) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host }.abort_signal(scope, signal, error);
if let Some(signal) = outcome.signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
return;
}
@@ -467,10 +465,8 @@ pub(in crate::context_bootstrap) fn cancel_active_history_traversal_intercept_se
};
set_traversal_intercept_inactive(scope, navigation, data.into());
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(signal) = signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, &url);
reject_resolver_array(scope, finished_resolvers, error, true);
@@ -491,10 +487,8 @@ fn traversal_intercept_fulfilled_callback<'s>(
let owner = runtime_window_owner(scope, navigation);
if !navigation_document_is_active(scope, owner) {
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(signal) = signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
let top_owner = runtime_top_window_owner(scope, owner);
let filename = window_location_for_holder(scope, top_owner)
@@ -526,10 +520,8 @@ fn traversal_intercept_rejected_callback<'s>(
.filter(|promise| promise.state() == v8::PromiseState::Rejected)
.map(|promise| promise.result(scope))
.unwrap_or_else(|| args.get(0));
if let Some(signal) = signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, &url);
reject_resolver_array(scope, finished_resolvers, error, true);
@@ -798,10 +798,8 @@ fn finish_location_navigation_canceled<'s>(
href: &str,
) {
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(signal) = outcome.signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = outcome.signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, href);
}
@@ -685,10 +685,8 @@ fn navigation_canceled_after_dispatch_result<'s>(
) -> v8::Local<'s, v8::Object> {
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
super::super::navigation_events::mark_navigation_outcome_default_prevented(scope, outcome);
if let Some(signal) = outcome.signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = outcome.signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, href);
navigation_rejected_value_result(scope, error)
@@ -1010,10 +1008,8 @@ pub(in crate::context_bootstrap) fn cancel_pending_precommit_same_document_navig
};
let error =
navigation_dom_exception(scope, "Navigation was canceled before commit", "AbortError");
if let Some(signal) = data.signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = data.signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, data.navigation, error, &data.current_href);
let receiver = v8::undefined(scope).into();
@@ -1294,10 +1290,8 @@ fn precommit_commit_rejected_callback<'s>(
.filter(|promise| promise.state() == v8::PromiseState::Rejected)
.map(|promise| promise.result(scope))
.unwrap_or_else(|| args.get(0));
if let Some(signal) = data.signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = data.signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, data.navigation, error, &data.current_href);
let receiver = v8::undefined(scope).into();
@@ -1594,10 +1588,8 @@ pub(in crate::context_bootstrap) fn cancel_active_intercepted_same_document_navi
return false;
};
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(signal) = signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, &filename);
let receiver = v8::undefined(scope).into();
@@ -1850,10 +1842,8 @@ fn finish_intercepted_navigation_rejected<'s>(
error: v8::Local<'s, v8::Value>,
filename: &str,
) {
if let Some(signal) = signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, filename);
if let Some(committed_resolve) = committed_resolve {
@@ -15,9 +15,17 @@ use super::{
};
use crate::util::context_host_ptr_from_global_bridge;
pub(crate) enum NavigationCancellationReason {
WindowStop,
/// Native retirement has already invalidated and extracted traversal
/// admissions; their JS settlement belongs to the owning ScriptVm.
LocalWindowRetirement,
}
pub(crate) fn inform_about_canceled_navigation_for_window<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
reason: NavigationCancellationReason,
) {
let owner = runtime_window_owner(scope, window);
let Some(navigation) = window_navigation_for_holder(scope, owner) else {
@@ -26,7 +34,9 @@ pub(crate) fn inform_about_canceled_navigation_for_window<'s>(
let _ = cancel_active_navigation_event(scope, navigation);
cancel_active_intercepted_same_document_navigation(scope, navigation);
cancel_active_cross_document_navigation(scope, navigation, None);
cancel_pending_precommit_history_traversal(scope, navigation);
if matches!(reason, NavigationCancellationReason::WindowStop) {
cancel_pending_precommit_history_traversal(scope, navigation);
}
cancel_pending_precommit_same_document_navigation_for_window_stop(scope, navigation);
cancel_pending_same_document_navigation_finishes_including_reentrant(scope, navigation);
if runtime_window_is_global(scope, owner)
@@ -563,10 +563,8 @@ pub(super) fn cancel_active_navigation_event<'s>(
}
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
set_private_value(scope, event, NAVIGATE_EVENT_ABORT_ERROR_SLOT, error);
if let Some(signal) = signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, &href);
Some(error)
@@ -292,18 +292,18 @@ pub(super) fn cancel_pending_same_document_navigation_finishes<'s>(
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return canceled;
};
let host = unsafe { &mut *host_ptr };
let pending = host.take_pending_navigation_finished_results_for_navigation(scope, navigation);
let pending = unsafe { &mut *host_ptr }
.take_pending_navigation_finished_results_for_navigation(scope, navigation);
if pending.is_empty() {
return canceled;
}
canceled = true;
for result in pending {
host.cancel_navigation_lifecycle_attempt(result.attempt_id);
unsafe { &mut *host_ptr }.cancel_navigation_lifecycle_attempt(result.attempt_id);
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(signal) = result.signal {
let signal = v8::Local::new(scope, signal);
host.abort_signal(scope, signal, error);
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, &result.href);
if let (Some(resolve), Some(value)) = (result.committed_resolve, result.resolved_value) {
@@ -354,13 +354,13 @@ pub(super) fn cancel_active_cross_document_navigation<'s>(
clear_active_cross_document_navigation(scope, navigation);
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
let host = unsafe { &mut *host_ptr };
// Remove the old load before abort listeners can start a successor.
unsafe { &mut *host_ptr }.clear_pending_location_navigation();
if let Some(signal) = get_private_value(scope, data, CROSS_DOCUMENT_PENDING_SIGNAL_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
host.abort_signal(scope, signal, error);
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
host.clear_pending_location_navigation();
}
finish_navigation_error_events(scope, navigation, error, &href);
let receiver = v8::undefined(scope).into();
@@ -621,13 +621,13 @@ fn navigation_result_with_task_finished<'s>(
let task = host
.take_pending_navigation_api_task(task_id)
.expect("a rejected Navigation API route must retain its Host-local payload");
reject_pending_navigation_api_task(scope, host, task.action);
reject_pending_navigation_api_task(scope, task.action);
}
} else {
host.cancel_navigation_lifecycle_attempt(attempt_id);
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(signal) = signal {
host.abort_signal(scope, signal, error);
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, href);
settle_navigation_finished_rejected(scope, finished_reject, error);
@@ -688,17 +688,18 @@ pub(crate) fn apply_pending_navigation_finished_result<'s>(
fn reject_pending_navigation_api_task<'s>(
scope: &mut v8::PinScope<'s, '_>,
host: &mut JsContextHost,
action: crate::native_bridge::PendingNavigationApiTaskAction,
) {
match action {
crate::native_bridge::PendingNavigationApiTaskAction::FinishResult(result) => {
host.cancel_navigation_lifecycle_attempt(result.attempt_id);
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
unsafe { &mut *host_ptr }.cancel_navigation_lifecycle_attempt(result.attempt_id);
}
let navigation = v8::Local::new(scope, result.navigation);
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
if let Some(signal) = result.signal {
let signal = v8::Local::new(scope, signal);
host.abort_signal(scope, signal, error);
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, &result.href);
if let Some(reject) = result.finished_reject {
@@ -390,10 +390,8 @@ fn reload_canceled_after_dispatch_result<'s>(
) -> v8::Local<'s, v8::Object> {
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
super::navigation_events::mark_navigation_outcome_default_prevented(scope, outcome);
if let Some(signal) = outcome.signal
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
{
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
if let Some(signal) = outcome.signal {
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
finish_navigation_error_events(scope, navigation, error, href);
navigation_rejected_value_result(scope, error)
@@ -310,16 +310,22 @@ fn abort<'s>(
return;
}
deactivate(scope, admission);
settle_aborted_admission(scope, admission, error);
}
fn settle_aborted_admission<'s>(
scope: &mut v8::PinScope<'s, '_>,
admission: &PendingHistoryTraversalAdmission,
error: Option<v8::Local<'s, v8::Value>>,
) {
let error = error.unwrap_or_else(|| {
navigation_dom_exception(scope, "History traversal was canceled", "AbortError")
});
results::reject_pending_navigation_results(scope, &admission.results, error);
for participant in &admission.participants {
if let Some(signal) = &participant.outcome.signal
&& let Some(host) = context_host_ptr_from_global_bridge(scope)
{
if let Some(signal) = &participant.outcome.signal {
let signal = v8::Local::new(scope, signal);
unsafe { &mut *host }.abort_signal(scope, signal, error);
crate::native_bridge::abort::abort_signal(scope, signal, error);
}
if let Some(navigation) = &participant.navigation {
let navigation = v8::Local::new(scope, navigation);
@@ -352,20 +358,15 @@ fn callback_id(value: v8::Local<'_, v8::Value>) -> Option<HistoryTraversalId> {
Some(HistoryTraversalId::from_raw(value.u64_value().0))
}
/// LocalWindow retirement must settle surviving callers before releasing handles.
/// Drain first: rejecting promises and dispatching abort/error events can reenter.
pub(crate) fn cancel_history_traversals_for_retiring_window(
/// The caller has removed and invalidated the entire batch under a short native
/// borrow. Neither extraction nor further native retirement happens around JS.
pub(crate) fn abort_history_traversal_admissions(
scope: &mut v8::PinScope<'_, '_>,
owner: crate::native_bridge::WindowExecutionContextOwner,
admissions: Vec<std::rc::Rc<PendingHistoryTraversalAdmission>>,
) {
let Some(host) = context_host_ptr_from_global_bridge(scope) else {
return;
};
let admissions = unsafe { &mut *host }
.pending_history_traversal_admissions
.take_for_owner(owner);
for admission in admissions {
abort(scope, &admission, None);
debug_assert!(!admission.active.get());
settle_aborted_admission(scope, &admission, None);
}
}
@@ -73,7 +73,11 @@ pub(crate) fn window_stop_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
inform_about_canceled_navigation_for_window(scope, args.this());
inform_about_canceled_navigation_for_window(
scope,
args.this(),
super::super::NavigationCancellationReason::WindowStop,
);
}
pub(in crate::context_bootstrap) fn window_confirm_callback<'s>(
+58 -37
View File
@@ -51,6 +51,12 @@ struct AbortLinkedTargetListener {
capture: bool,
}
struct AbortSignalDispatch {
algorithms: Vec<v8::Global<v8::Function>>,
linked_target_listeners: Vec<AbortLinkedTargetListener>,
dependent_signals: Vec<u32>,
}
impl AbortStore {
fn alloc_signal_id(&mut self) -> u32 {
self.next_signal_id = self
@@ -257,48 +263,24 @@ impl AbortStore {
}
}
pub(super) fn abort_signal<'s>(
fn take_signal_abort<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
host: &mut super::JsContextHost,
signal: v8::Local<'s, v8::Object>,
reason: v8::Local<'s, v8::Value>,
) {
let Some(signal_id) = Self::signal_id_from_object(scope, signal) else {
return;
};
let Some((abort_algorithms, linked_target_listeners, dependent_signals)) = ({
let Some(state) = self.signal_state_mut(signal_id) else {
return;
};
if state.aborted {
return;
}
state.aborted = true;
state.reason = Some(v8::Global::new(scope, reason));
let abort_algorithms = std::mem::take(&mut state.abort_algorithms);
let linked_target_listeners = std::mem::take(&mut state.linked_target_listeners);
let dependent_signals = state.dependent_signals.clone();
Some((abort_algorithms, linked_target_listeners, dependent_signals))
}) else {
return;
};
event::invoke_abort_algorithms(scope, signal, reason, abort_algorithms);
abort_signal_events::dispatch_abort(scope, signal);
for linked in linked_target_listeners {
host.remove_registered_event_listener_by_id(
linked.target,
&linked.event_type,
linked.callback_id,
linked.capture,
);
}
for dependent_signal_id in dependent_signals {
let Some(dependent_signal) = self.signal_object(scope, dependent_signal_id) else {
continue;
};
host.abort_signal(scope, dependent_signal, reason);
) -> Option<AbortSignalDispatch> {
let signal_id = Self::signal_id_from_object(scope, signal)?;
let state = self.signal_state_mut(signal_id)?;
if state.aborted {
return None;
}
state.aborted = true;
state.reason = Some(v8::Global::new(scope, reason));
Some(AbortSignalDispatch {
algorithms: std::mem::take(&mut state.abort_algorithms),
linked_target_listeners: std::mem::take(&mut state.linked_target_listeners),
dependent_signals: state.dependent_signals.clone(),
})
}
pub(super) fn link_dependent_signal(
@@ -315,6 +297,45 @@ impl AbortStore {
}
}
/// Aborting a signal invokes author algorithms and listeners synchronously.
/// Only owned dispatch data may cross those calls; neither the host nor its
/// AbortStore can remain borrowed, including while aborting dependent signals.
pub(crate) fn abort_signal<'s>(
scope: &mut v8::PinScope<'s, '_>,
signal: v8::Local<'s, v8::Object>,
reason: v8::Local<'s, v8::Value>,
) {
let Some(host_ptr) = crate::util::context_host_ptr_from_global_bridge(scope) else {
return;
};
let dispatch = unsafe { &mut *host_ptr }
.native_bridge_mut()
.abort
.take_signal_abort(scope, signal, reason);
let Some(dispatch) = dispatch else {
return;
};
event::invoke_abort_algorithms(scope, signal, reason, dispatch.algorithms);
abort_signal_events::dispatch_abort(scope, signal);
for linked in dispatch.linked_target_listeners {
unsafe { &mut *host_ptr }.remove_registered_event_listener_by_id(
linked.target,
&linked.event_type,
linked.callback_id,
linked.capture,
);
}
for dependent_signal_id in dispatch.dependent_signals {
let signal = unsafe { &mut *host_ptr }
.native_bridge_mut()
.abort
.signal_object(scope, dependent_signal_id);
if let Some(signal) = signal {
abort_signal(scope, signal, reason);
}
}
}
pub(crate) fn dom_exception_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
message: &str,
@@ -87,6 +87,6 @@ pub(crate) fn abort_controller_abort_callback<'s>(
rv.set_undefined();
return;
}
host.abort_signal(scope, signal, reason);
crate::native_bridge::abort::abort_signal(scope, signal, reason);
rv.set_undefined();
}
@@ -105,7 +105,7 @@ pub(crate) fn abort_signal_any_callback<'s>(
else {
continue;
};
host.abort_signal(scope, signal, reason);
crate::native_bridge::abort::abort_signal(scope, signal, reason);
rv.set(signal.into());
return;
}
@@ -150,6 +150,6 @@ fn abort_signal_timeout_fire_native_callback(
return;
};
let reason = timeout_error_value(scope);
host.abort_signal(scope, signal, reason);
crate::native_bridge::abort::abort_signal(scope, signal, reason);
rv.set_undefined();
}
@@ -135,10 +135,8 @@ impl JsContextHost {
}
let stale = pending_contexts.borrow_mut().remove(&handle);
if let Some(stale) = stale {
crate::context_bootstrap::cancel_history_traversals_for_retiring_window(
scope,
super::super::WindowExecutionContextOwner::Frame(stale.local_window_id),
);
self.pending_history_traversal_admissions
.retire_owner(WindowExecutionContextOwner::Frame(stale.local_window_id));
self.retire_window_execution_contexts_for_context_token(
stale.runtime_observable_context_token,
);
@@ -636,6 +636,14 @@ impl JsContextHost {
scope: &mut v8::PinScope<'_, '_>,
handle: DomHandle,
) {
// Pending traversal admission is settled by ScriptVm after this native
// detach finishes. Other navigation cancellation keeps its synchronous
// event/committed-entry semantics.
if let Some(owner) = self.current_child_document_task_owner(handle) {
self.pending_history_traversal_admissions.retire_owner(
super::super::WindowExecutionContextOwner::Frame(owner.local_window_id),
);
}
let Some(window) = self.child_window_proxy_records.live_window(scope, handle) else {
return;
};
@@ -647,7 +655,11 @@ impl JsContextHost {
let context = v8::Local::new(scope, &context);
let child_scope = &mut v8::ContextScope::new(scope, context);
let window = v8::Local::new(child_scope, &window);
crate::context_bootstrap::inform_about_canceled_navigation_for_window(child_scope, window);
crate::context_bootstrap::inform_about_canceled_navigation_for_window(
child_scope,
window,
crate::context_bootstrap::NavigationCancellationReason::LocalWindowRetirement,
);
}
pub(in crate::native_bridge::context_host) fn refresh_child_window_access_surfaces_after_origin_mutation(
@@ -1905,7 +1905,7 @@ impl JsContextHost {
);
}
if let Some(retired_local_window_id) = transition.retired_local_window_id {
self.retire_lightweight_popup_local_window(scope, popup_id, retired_local_window_id);
self.retire_lightweight_popup_local_window(popup_id, retired_local_window_id);
}
self.refresh_lightweight_popup_indexed_db_factory(scope, popup_id, window);
tracing::debug!(
@@ -1929,7 +1929,6 @@ impl JsContextHost {
fn retire_lightweight_popup_local_window(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
popup_id: u64,
local_window_id: LightweightPopupLocalWindowId,
) {
@@ -1937,10 +1936,8 @@ impl JsContextHost {
popup_id,
local_window_id,
};
crate::context_bootstrap::cancel_history_traversals_for_retiring_window(
scope,
execution_context_owner,
);
self.pending_history_traversal_admissions
.retire_owner(execution_context_owner);
let retired_timer_count = unsafe { &mut *self.runtime }
.cancel_window_execution_context_timers(execution_context_owner);
let retired_webcrypto_count =
@@ -4321,7 +4318,7 @@ fn lightweight_popup_close_callback<'s>(
host.clear_custom_element_registry_associations_for_document(document_handle);
}
host.retire_lightweight_popup_document_owner(transition.retired_owner);
host.retire_lightweight_popup_local_window(scope, popup_id, transition.retired_local_window_id);
host.retire_lightweight_popup_local_window(popup_id, transition.retired_local_window_id);
host.lightweight_popup_window_names
.retain(|_, named_popup_id| *named_popup_id != popup_id);
}
@@ -94,19 +94,4 @@ impl JsContextHost {
self.release_event_callback(callback_id);
}
}
pub(crate) fn abort_signal<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
signal: v8::Local<'s, v8::Object>,
reason: v8::Local<'s, v8::Value>,
) {
let host_ptr = self as *mut Self;
unsafe {
(*host_ptr)
.bridge
.abort
.abort_signal(scope, &mut *host_ptr, signal, reason);
}
}
}
@@ -63,11 +63,13 @@ pub(crate) struct PendingHistoryTraversalAdmission {
#[derive(Default)]
pub(crate) struct PendingHistoryTraversalAdmissions {
entries: HashMap<HistoryTraversalId, Rc<PendingHistoryTraversalAdmission>>,
retired: Vec<Rc<PendingHistoryTraversalAdmission>>,
}
impl PendingHistoryTraversalAdmissions {
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
self.entries.is_empty() && self.retired.is_empty()
}
pub(crate) fn insert(
@@ -127,15 +129,32 @@ impl PendingHistoryTraversalAdmissions {
.iter()
.any(|participant| participant.execution_owner == owner)
})
.map(|(_, admission)| admission)
.map(|(_, admission)| {
// Invalidate even an Rc already held by an executing callback.
admission.active.set(false);
admission
})
.collect()
}
/// Native retirement cannot run author script while borrowing JsContextHost.
/// Remove callbacks' tokens now; the VM settles the owned batch after the
/// native stack has returned. No cleanup of a successor runs after events.
pub(crate) fn retire_owner(&mut self, owner: WindowExecutionContextOwner) {
let admissions = self.take_for_owner(owner);
self.retired.extend(admissions);
}
pub(crate) fn take_retired(&mut self) -> Vec<Rc<PendingHistoryTraversalAdmission>> {
std::mem::take(&mut self.retired)
}
pub(crate) fn clear(&mut self) {
for admission in self.entries.values() {
admission.active.set(false);
}
self.entries.clear();
self.retired.clear();
}
#[cfg(test)]
+11 -1
View File
@@ -3595,7 +3595,9 @@ impl ScriptVm {
context.detach_global();
Ok(())
})?;
None
// Cancellation can synchronously install a successor realm or
// detach the frame. Resolve both owner and pending context anew.
return self.create_new_child_default_world(frame_id, child_handle);
}
None => None,
};
@@ -3742,6 +3744,14 @@ impl ScriptVm {
Ok(())
});
}
let refreshed_live;
let live = if stale_prebootstrapped_contexts.is_empty() {
live
} else {
// Retirement callbacks may have materialized successor realms.
refreshed_live = self.live_child_default_context_entries();
&refreshed_live
};
let stale_context_ids = self
.child_frame_realm_store
.iter_by_execution_context_id()
@@ -8,20 +8,39 @@ use super::perform_microtask_checkpoint_and_report_pending_promise_rejections;
use crate::{frame_owner_model::FrameRealmId, native_bridge::JsContextHost};
impl ScriptVm {
pub(super) fn settle_retired_history_traversals(&mut self) {
loop {
let admissions = self
._context_host
.borrow_mut()
.pending_history_traversal_admissions
.take_retired();
if admissions.is_empty() {
return;
}
let _ = self.with_default_context_scope(|scope, _| {
// Neither a host borrow nor native retirement stack crosses JS.
// Reentrant retirement is extracted by the next loop iteration.
crate::context_bootstrap::abort_history_traversal_admissions(scope, admissions);
Self::perform_microtask_checkpoints(scope, None)
});
}
}
pub(super) fn cancel_history_traversals_for_retiring_window(
&mut self,
owner: crate::native_bridge::WindowExecutionContextOwner,
) {
if self
let admissions = self
._context_host
.borrow()
.borrow_mut()
.pending_history_traversal_admissions
.is_empty()
{
.take_for_owner(owner);
if admissions.is_empty() {
return;
}
let _ = self.with_default_context_scope(|scope, _| {
crate::context_bootstrap::cancel_history_traversals_for_retiring_window(scope, owner);
crate::context_bootstrap::abort_history_traversal_admissions(scope, admissions);
Ok(())
});
}
@@ -1254,6 +1254,7 @@ impl ScriptVm {
) -> T {
self.apply_pending_main_document_owner_transitions();
self.apply_pending_child_document_owner_retirements();
self.settle_retired_history_traversals();
self.drain_pending_style_invalidations_for_turn_exit(boundary);
let runtime_continuation_is_ready =
self.runtime_script_work_should_signal_immediate_progress();
@@ -1,5 +1,164 @@
use super::*;
#[tokio::test]
async fn history_traversal_retirement_reentry_preserves_reattached_frame_and_sibling_state() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm =
new_storage_page_task_executor_test_vm_with_loader("https://example.com/base", &loader);
vm.eval(r#"
globalThis.frame = document.createElement('iframe');
globalThis.sibling = document.createElement('iframe');
const container = document.body || document.documentElement || document;
container.appendChild(frame); container.appendChild(sibling);
globalThis.oldChild = frame.contentWindow;
history.replaceState(0, ''); oldChild.history.replaceState(0, '');
history.pushState(1, ''); oldChild.history.pushState(1, '');
globalThis.log = [];
navigation.onnavigate = event => {
event.signal.addEventListener('abort', () => {
sibling.contentWindow.history.pushState('from-abort', '');
log.push('abort');
});
event.intercept({precommitHandler: () => new Promise(resolve => globalThis.release = resolve)});
};
navigation.addEventListener('navigateerror', () => {
log.push('error');
sibling.contentWindow.history.pushState('from-error', '');
container.appendChild(frame);
frame.contentWindow.history.replaceState('successor', '');
release();
}, {once:true});
const result = navigation.back();
result.committed.catch(error => log.push('committed:' + error.name));
result.finished.catch(error => log.push('finished:' + error.name));
"#).unwrap();
assert!(
vm.run_one_history_traversal_executor_turn(&loader)
.await
.unwrap()
);
assert_eq!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.len(),
1
);
// Detach during a Promise reaction also exercises retirement discovered
// during a checkpoint, not only the outer script body's retirement batch.
vm.eval("Promise.resolve().then(() => frame.remove());")
.unwrap();
let snapshot = r#"JSON.stringify([
history.state, sibling.contentWindow.history.state,
frame.isConnected, frame.contentWindow.history.state, log
])"#;
let expected = r#"[1,"from-error",true,"successor",["abort","error","committed:AbortError","finished:AbortError"]]"#;
assert_eq!(vm.eval(snapshot).unwrap(), expected);
assert!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.is_empty()
);
vm.eval("release();").unwrap();
assert_eq!(vm.eval(snapshot).unwrap(), expected);
assert!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.is_empty()
);
assert_eq!(
vm.eval("frame.contentWindow.document.readyState").unwrap(),
"complete"
);
}
#[tokio::test]
async fn history_traversal_retirement_error_can_start_successor_admission() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
let mut vm =
new_storage_page_task_executor_test_vm_with_loader("https://example.com/base", &loader);
vm.eval(
r#"
globalThis.frame = document.createElement('iframe');
(document.body || document.documentElement || document).appendChild(frame);
history.replaceState(0, ''); frame.contentWindow.history.replaceState(0, '');
history.pushState(1, ''); frame.contentWindow.history.pushState(1, '');
globalThis.gates = []; globalThis.log = [];
navigation.onnavigate = event => {
if (event.navigationType === 'traverse') event.intercept({
precommitHandler: () => new Promise(resolve => gates.push(resolve))
});
};
navigation.addEventListener('navigateerror', () => {
history.pushState(2, '');
const next = navigation.back();
next.committed.then(() => log.push('newCommitted'));
next.finished.then(() => log.push('newFinished'));
}, {once:true});
const result = navigation.back();
result.committed.catch(error => log.push('oldCommitted:' + error.name));
result.finished.catch(error => log.push('oldFinished:' + error.name));
"#,
)
.unwrap();
assert!(
vm.run_one_history_traversal_executor_turn(&loader)
.await
.unwrap()
);
assert_eq!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.len(),
1
);
vm.eval("frame.remove();").unwrap();
assert!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.is_empty()
);
assert!(
vm.run_one_history_traversal_executor_turn(&loader)
.await
.unwrap()
);
assert_eq!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.len(),
1
);
vm.eval("gates[0]();").unwrap();
assert_eq!(
vm.eval("JSON.stringify([history.state, log])").unwrap(),
r#"[2,["oldCommitted:AbortError","oldFinished:AbortError"]]"#
);
assert_eq!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.len(),
1
);
vm.eval("gates[1]();").unwrap();
assert_eq!(
vm.eval("JSON.stringify([history.state, log])").unwrap(),
r#"[1,["oldCommitted:AbortError","oldFinished:AbortError","newCommitted","newFinished"]]"#
);
assert!(
vm._context_host
.borrow()
.pending_history_traversal_admissions
.is_empty()
);
}
#[tokio::test]
async fn history_traversal_canceled_precommit_callback_cannot_complete_successor() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
@@ -38,7 +38,9 @@ const RAW_GLOBAL_FUNCTION_ALLOWLIST: &[(&str, usize)] = &[
("host/timers.rs", 1),
// 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 abort algorithms reside in the store or its extracted dispatch
// batch. Dispatch moves the roots; it does not add a Web IDL callback store.
("native_bridge/abort.rs", 2),
("native_bridge/abort/event.rs", 1),
("native_bridge/history_queue.rs", 3),
("script_vm/frame_script_jobs.rs", 3),