diff --git a/moli-core/src/page/mod.rs b/moli-core/src/page/mod.rs index 64e794556b..869e8862a1 100644 --- a/moli-core/src/page/mod.rs +++ b/moli-core/src/page/mod.rs @@ -71,7 +71,7 @@ pub use moli_page_types::{ RendererDomDebuggerDomBreakpointType, RendererDomDebuggerEventListenerBreakpoint, RendererDomDebuggerXhrBreakpoint, RendererInspectorProtocolConfiguration, RendererInspectorProtocolConfigurationCommand, RendererInspectorSessionRestoreSnapshot, - SameDocumentHistoryUpdate, V8InspectorSessionAttach, V8InspectorSessionState, + SessionHistoryUpdateKind, V8InspectorSessionAttach, V8InspectorSessionState, renderer_inspector_protocol_configuration_command_from_message, renderer_inspector_protocol_configuration_command_from_method, }; diff --git a/moli-page-types/src/lib.rs b/moli-page-types/src/lib.rs index 035c47e11e..23d007dd60 100644 --- a/moli-page-types/src/lib.rs +++ b/moli-page-types/src/lib.rs @@ -11,6 +11,12 @@ mod layout; mod navigation_history; mod navigator_overrides; mod renderer_transport_memory; +mod session_history; + +pub use session_history::{ + JointSessionHistory, SessionHistoryCommit, SessionHistoryContextId, SessionHistoryEntry, + SessionHistoryPosition, SessionHistorySeed, SessionHistoryStepId, SessionHistoryUpdate, +}; use std::{ borrow::Cow, @@ -188,7 +194,7 @@ pub use inspector_state::{ pub use navigation_history::{ NavigationActivationSeed, NavigationHistoryDocumentId, NavigationHistoryEntryId, NavigationHistoryEntryKey, NavigationHistoryEntrySeed, NavigationHistoryMutation, - NavigationHistorySerializedEntry, NavigationTraversalSeedCandidate, SameDocumentHistoryUpdate, + NavigationHistorySerializedEntry, NavigationTraversalSeedCandidate, SessionHistoryUpdateKind, apply_child_browsing_context_javascript_url_navigation_to_entry_seed, apply_child_browsing_context_navigation_to_entry_seed, child_browsing_context_single_entry_seed, cross_document_navigation_seed, diff --git a/moli-page-types/src/navigation_history.rs b/moli-page-types/src/navigation_history.rs index e63ae91714..b2144fef92 100644 --- a/moli-page-types/src/navigation_history.rs +++ b/moli-page-types/src/navigation_history.rs @@ -140,6 +140,7 @@ pub struct NavigationHistoryEntrySeed { pub entries: Vec, pub current_index: u32, pub activation: Option, + pub session_history: crate::SessionHistorySeed, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -162,17 +163,18 @@ pub enum NavigationHistoryMutation { Replace, } -/// The browser-side session-history effect of a renderer-completed -/// same-document navigation. +/// The browser-side effect of a committed traversable mutation, including +/// navigations in child Documents. /// /// Traversal is deliberately represented as a delta rather than another URL /// insertion: URLs are not stable entry identities and may repeat in a /// session-history list. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SameDocumentHistoryUpdate { +pub enum SessionHistoryUpdateKind { Push, Replace, Traverse { delta: i64 }, + PruneAllButCurrent, } impl NavigationHistoryMutation { @@ -214,6 +216,7 @@ pub fn initial_navigation_history_seed( ), ]; return NavigationHistoryEntrySeed { + session_history: Default::default(), current_index: 1, activation: Some(NavigationActivationSeed { entry: entries[1].clone(), @@ -234,6 +237,7 @@ pub fn initial_navigation_history_seed( None, )]; NavigationHistoryEntrySeed { + session_history: Default::default(), current_index: 0, activation: (href != "about:blank").then(|| NavigationActivationSeed { entry: entries[0].clone(), @@ -272,6 +276,7 @@ pub fn child_browsing_context_single_entry_seed(url: Option<&Url>) -> Navigation ), ]; return NavigationHistoryEntrySeed { + session_history: Default::default(), activation: Some(NavigationActivationSeed { entry: entries[1].clone(), from: None, @@ -292,6 +297,7 @@ pub fn child_browsing_context_single_entry_seed(url: Option<&Url>) -> Navigation None, )]; NavigationHistoryEntrySeed { + session_history: Default::default(), activation: None, entries, current_index: 0, @@ -304,6 +310,7 @@ pub fn apply_child_browsing_context_navigation_to_entry_seed( history_state_json: Option, navigation_state_json: Option, ) { + seed.session_history.commit = crate::SessionHistoryCommit::Push; let next_index = seed.current_index + 1; let current_navigation_index = seed .entries @@ -361,6 +368,7 @@ pub fn replace_child_browsing_context_navigation_in_entry_seed( history_state_json: Option, navigation_state_json: Option, ) { + seed.session_history.commit = crate::SessionHistoryCommit::Replace; let current_index = seed.current_index; let current_navigation_index = seed .entries @@ -404,6 +412,7 @@ pub fn replace_child_browsing_context_navigation_in_entry_seed( pub fn apply_child_browsing_context_javascript_url_navigation_to_entry_seed( seed: &mut NavigationHistoryEntrySeed, ) { + seed.session_history.commit = crate::SessionHistoryCommit::Replace; let current_index = seed.current_index; let Some(previous_entry) = seed .entries @@ -486,6 +495,13 @@ pub fn cross_document_navigation_seed( }; NavigationHistoryEntrySeed { + session_history: crate::SessionHistorySeed { + commit: match mutation { + NavigationHistoryMutation::Push => crate::SessionHistoryCommit::Push, + NavigationHistoryMutation::Replace => crate::SessionHistoryCommit::Replace, + }, + ..Default::default() + }, entries, current_index: destination_index, activation: Some(NavigationActivationSeed { @@ -505,6 +521,10 @@ pub fn reload_navigation_seed( .find(|entry| entry.history_index == current_index) .cloned()?; Some(NavigationHistoryEntrySeed { + session_history: crate::SessionHistorySeed { + commit: crate::SessionHistoryCommit::Replace, + ..Default::default() + }, entries, current_index, activation: Some(NavigationActivationSeed { @@ -538,6 +558,10 @@ pub fn traversal_navigation_seed_candidate( current_url, target_url: target_url.clone(), seed: NavigationHistoryEntrySeed { + session_history: crate::SessionHistorySeed { + commit: crate::SessionHistoryCommit::Traverse, + ..Default::default() + }, entries, current_index: target_index, activation: Some(NavigationActivationSeed { diff --git a/moli-page-types/src/session_history.rs b/moli-page-types/src/session_history.rs new file mode 100644 index 0000000000..e091d11262 --- /dev/null +++ b/moli-page-types/src/session_history.rs @@ -0,0 +1,446 @@ +//! The history of a traversable, independent of any Document's Navigation API +//! view. A step records the active entry of each participating browsing context. +//! Indices are positions, never identities: pushing after traversal discards the +//! forward steps, whereas replacement preserves the current step. + +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::{NavigationHistoryDocumentId, NavigationHistoryEntryKey}; + +static NEXT_CONTEXT: AtomicU64 = AtomicU64::new(1); +static NEXT_STEP: AtomicU64 = AtomicU64::new(1); + +fn allocate(counter: &AtomicU64) -> u64 { + counter + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| { + value.checked_add(1) + }) + .expect("session history identity allocator exhausted") +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SessionHistoryContextId(u64); + +impl SessionHistoryContextId { + pub const ROOT: Self = Self(0); + + pub fn allocate() -> Self { + Self(allocate(&NEXT_CONTEXT)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SessionHistoryStepId(u64); + +impl SessionHistoryStepId { + pub fn raw(self) -> u64 { + self.0 + } + + /// Restore an opaque identity held by a renderer's private task slot. + pub fn from_raw(raw: u64) -> Self { + Self(raw) + } +} + +/// Browser supplied position. It includes steps outside the current renderer's +/// Navigation API view, including entries in other Documents and origins. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SessionHistoryPosition { + index: usize, + length: usize, +} + +impl SessionHistoryPosition { + pub const INITIAL: Self = Self { + index: 0, + length: 1, + }; + + pub fn new(index: usize, length: usize) -> Option { + (index < length).then_some(Self { index, length }) + } + + pub fn index(self) -> usize { + self.index + } + + pub fn length(self) -> usize { + self.length + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SessionHistoryEntry { + pub key: NavigationHistoryEntryKey, + pub document: NavigationHistoryDocumentId, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SessionHistoryCommit { + #[default] + Attach, + Push, + Replace, + Traverse, +} + +/// The history effect of a document commit, carried separately from the +/// document's entry view. A top-level renderer replacement also carries its +/// traversable; subframe commits keep using the page's existing owner. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SessionHistorySeed { + pub commit: SessionHistoryCommit, + pub traversable: Option>, + pub target_step: Option, +} + +/// One already-committed traversable mutation, published in renderer FIFO +/// order before navigation observers can initiate another mutation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SessionHistoryUpdate { + pub position: SessionHistoryPosition, + pub update: crate::SessionHistoryUpdateKind, + pub root_url: String, + /// Joint steps that share the current root entry. A root replacement also + /// changes the URL exposed by steps introduced by child navigations. + pub root_entry_steps: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct SessionHistoryStep { + id: SessionHistoryStepId, + entries: BTreeMap, +} + +/// Page-owned authority for joint history. No URL, Navigation API index, or +/// per-Window cached length participates in history bookkeeping. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct JointSessionHistory { + steps: Vec, + current: usize, + parents: BTreeMap, +} + +impl Default for JointSessionHistory { + fn default() -> Self { + Self::new(SessionHistoryPosition::INITIAL) + } +} + +impl JointSessionHistory { + pub fn new(position: SessionHistoryPosition) -> Self { + Self { + steps: (0..position.length) + .map(|_| SessionHistoryStep { + id: SessionHistoryStepId(allocate(&NEXT_STEP)), + entries: BTreeMap::new(), + }) + .collect(), + current: position.index, + parents: BTreeMap::new(), + } + } + + pub fn position(&self) -> SessionHistoryPosition { + SessionHistoryPosition { + index: self.current, + length: self.steps.len(), + } + } + + pub fn current_step(&self) -> SessionHistoryStepId { + self.steps[self.current].id + } + + pub fn entry(&self, context: SessionHistoryContextId) -> Option<&SessionHistoryEntry> { + self.steps[self.current].entries.get(&context) + } + + /// Attaching an initial document adds no step. Its entry is also active at + /// other steps in the same parent Document, including its forward history. + pub fn attach( + &mut self, + context: SessionHistoryContextId, + parent: Option, + entry: SessionHistoryEntry, + ) { + if self.entry(context).is_some() { + return; + } + let parent_document = + parent.and_then(|parent| self.entry(parent).map(|e| e.document.clone())); + if let Some(parent) = parent { + self.parents.insert(context, parent); + } + for (index, step) in self.steps.iter_mut().enumerate() { + let same_parent_document = + parent + .zip(parent_document.as_ref()) + .is_some_and(|(parent, document)| { + step.entries + .get(&parent) + .is_some_and(|e| &e.document == document) + }); + if index == self.current || same_parent_document { + step.entries.entry(context).or_insert_with(|| entry.clone()); + } + } + } + + pub fn push(&mut self, context: SessionHistoryContextId, entry: SessionHistoryEntry) { + let mut next = self.steps[self.current].clone(); + next.id = SessionHistoryStepId(allocate(&NEXT_STEP)); + self.remove_replaced_document_children(&mut next, context, &entry); + next.entries.insert(context, entry); + self.steps.truncate(self.current + 1); + self.steps.push(next); + self.current += 1; + } + + pub fn replace(&mut self, context: SessionHistoryContextId, entry: SessionHistoryEntry) { + let old_key = self.entry(context).map(|entry| entry.key.clone()); + let descendants = self.descendants(context); + for (index, step) in self.steps.iter_mut().enumerate() { + if index == self.current + || old_key + .as_ref() + .is_some_and(|key| step.entries.get(&context).is_some_and(|e| &e.key == key)) + { + if step + .entries + .get(&context) + .is_some_and(|old| old.document != entry.document) + { + step.entries.retain(|id, _| !descendants.contains(id)); + } + step.entries.insert(context, entry.clone()); + } + } + } + + pub fn step_by_delta(&self, delta: i64) -> Option { + let index = i64::try_from(self.current).ok()?.checked_add(delta)?; + self.steps + .get(usize::try_from(index).ok()?) + .map(|step| step.id) + } + + pub fn delta_to(&self, target: SessionHistoryStepId) -> Option { + let next = self.steps.iter().position(|step| step.id == target)?; + Some(i64::try_from(next).ok()? - i64::try_from(self.current).ok()?) + } + + pub fn steps_for_entry( + &self, + context: SessionHistoryContextId, + key: &NavigationHistoryEntryKey, + ) -> Vec { + self.steps + .iter() + .enumerate() + .filter_map(|(index, step)| { + step.entries + .get(&context) + .is_some_and(|entry| &entry.key == key) + .then_some(index) + }) + .collect() + } + + /// An entry can span several joint steps made by other frames. Traversal + /// chooses the nearest such step, preserving intervening frame history. + pub fn step_for_entry( + &self, + context: SessionHistoryContextId, + key: &NavigationHistoryEntryKey, + ) -> Option { + let matches = |step: &&SessionHistoryStep| { + step.entries + .get(&context) + .is_some_and(|entry| &entry.key == key) + }; + self.steps[..=self.current] + .iter() + .rev() + .find(matches) + .or_else(|| self.steps[self.current + 1..].iter().find(matches)) + .map(|step| step.id) + } + + pub fn entries_at( + &self, + target: SessionHistoryStepId, + ) -> Option<&BTreeMap> { + self.steps + .iter() + .find(|step| step.id == target) + .map(|step| &step.entries) + } + + /// Commit by identity so queued traversals cannot land in a newly pushed + /// entry that happens to reuse an index after forward-history pruning. + pub fn traverse(&mut self, target: SessionHistoryStepId) -> Option { + let next = self.steps.iter().position(|step| step.id == target)?; + let delta = i64::try_from(next).ok()? - i64::try_from(self.current).ok()?; + self.current = next; + Some(delta) + } + + pub fn contains_entry( + &self, + context: SessionHistoryContextId, + key: &NavigationHistoryEntryKey, + ) -> bool { + self.steps.iter().any(|step| { + step.entries + .get(&context) + .is_some_and(|entry| &entry.key == key) + }) + } + + pub fn prune_all_but_current(&mut self) { + let current = self.steps[self.current].clone(); + self.steps = vec![current]; + self.current = 0; + } + + pub fn detach(&mut self, context: SessionHistoryContextId) { + let mut removed = self.descendants(context); + removed.push(context); + for step in &mut self.steps { + step.entries.retain(|id, _| !removed.contains(id)); + } + self.parents.retain(|id, _| !removed.contains(id)); + } + + fn descendants(&self, context: SessionHistoryContextId) -> Vec { + let mut found = vec![context]; + let mut index = 0; + while index < found.len() { + let parent = found[index]; + found.extend( + self.parents + .iter() + .filter_map(|(child, candidate)| (*candidate == parent).then_some(*child)), + ); + index += 1; + } + found.remove(0); + found + } + + fn remove_replaced_document_children( + &self, + step: &mut SessionHistoryStep, + context: SessionHistoryContextId, + entry: &SessionHistoryEntry, + ) { + if step + .entries + .get(&context) + .is_some_and(|old| old.document != entry.document) + { + let descendants = self.descendants(context); + step.entries.retain(|id, _| !descendants.contains(id)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entry(key: &str, document: &str) -> SessionHistoryEntry { + SessionHistoryEntry { + key: NavigationHistoryEntryKey::from_serialized(key.into()), + document: NavigationHistoryDocumentId::from_serialized(document.into()), + } + } + + #[test] + fn branch_truncates_forward_steps_and_invalidates_queued_targets() { + let mut history = JointSessionHistory::default(); + let root = SessionHistoryContextId::ROOT; + history.attach(root, None, entry("a", "top")); + for key in ["b", "c", "d"] { + history.push(root, entry(key, "top")); + } + let stale = history.current_step(); + let b = history.step_by_delta(-2).unwrap(); + assert_eq!(history.traverse(b), Some(-2)); + history.push(root, entry("e", "top")); + assert_eq!( + history.position(), + SessionHistoryPosition::new(2, 3).unwrap() + ); + assert_eq!(history.traverse(stale), None); + assert_eq!(history.entry(root), Some(&entry("e", "top"))); + } + + #[test] + fn joint_steps_follow_commit_order_and_navigation_traversal_uses_nearest_step() { + let mut history = JointSessionHistory::default(); + let root = SessionHistoryContextId::ROOT; + let a = SessionHistoryContextId::allocate(); + let b = SessionHistoryContextId::allocate(); + history.attach(root, None, entry("top", "top")); + history.attach(a, Some(root), entry("a0", "a")); + history.attach(b, Some(root), entry("b0", "b")); + history.push(a, entry("a1", "a")); + history.push(b, entry("b1", "b")); + history.push(a, entry("a2", "a")); + let target = history.step_for_entry(a, &entry("a1", "a").key).unwrap(); + assert_eq!(history.traverse(target), Some(-1)); + assert_eq!(history.entry(b), Some(&entry("b1", "b"))); + history.traverse(history.step_by_delta(-1).unwrap()); + assert_eq!(history.entry(b), Some(&entry("b0", "b"))); + history.push(b, entry("b2", "b")); + assert_eq!( + history.position(), + SessionHistoryPosition::new(2, 3).unwrap() + ); + assert!(!history.contains_entry(a, &entry("a2", "a").key)); + assert!(!history.contains_entry(b, &entry("b1", "b").key)); + } + + #[test] + fn replace_preserves_steps_and_updates_shared_entry_references() { + let mut history = JointSessionHistory::default(); + let root = SessionHistoryContextId::ROOT; + let child = SessionHistoryContextId::allocate(); + history.attach(root, None, entry("top", "top")); + history.attach(child, Some(root), entry("child0", "child")); + history.push(child, entry("child1", "child")); + let position = history.position(); + history.replace(root, entry("top-replaced", "top")); + assert_eq!(history.position(), position); + history.traverse(history.step_by_delta(-1).unwrap()); + assert_eq!(history.entry(root), Some(&entry("top-replaced", "top"))); + history.replace(root, entry("new-document", "new-document")); + assert!(history.entry(child).is_none()); + assert_eq!(history.position().length(), 2); + } + + #[test] + fn browser_position_includes_opaque_steps_and_push_uses_the_cursor() { + let mut history = JointSessionHistory::new(SessionHistoryPosition::new(1, 4).unwrap()); + let root = SessionHistoryContextId::ROOT; + history.attach(root, None, entry("current", "document")); + history.push(root, entry("next", "document")); + assert_eq!( + history.position(), + SessionHistoryPosition::new(2, 3).unwrap() + ); + assert_eq!( + history + .entries_at(history.step_by_delta(-2).unwrap()) + .unwrap() + .len(), + 0 + ); + assert!(SessionHistoryPosition::new(0, 0).is_none()); + assert!(SessionHistoryPosition::new(3, 3).is_none()); + } +} diff --git a/moli-protocol/src/conn/browser_context/target_session_owner.rs b/moli-protocol/src/conn/browser_context/target_session_owner.rs index 1488e05fe2..09b9978a80 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -180,7 +180,7 @@ pub(crate) struct TargetNavigationLoadInputs { (bool, Option), pub(crate) permission_overrides: Vec, main_document_commit_seed: Option, - session_history_length: Option, + session_history_position: Option, } impl TargetNavigationLoadInputs { @@ -199,7 +199,7 @@ impl TargetNavigationLoadInputs { ) -> Option { self.main_document_commit_seed.as_ref().map(|seed| { let mut commit = seed.resolve(final_url, network_error_page); - commit.session_history_length = self.session_history_length; + commit.session_history_position = self.session_history_position; commit }) } @@ -340,11 +340,11 @@ impl TargetNavigationLoadInputs { .subresource_interception_config(), permission_overrides: Vec::new(), main_document_commit_seed: None, - session_history_length: Some( + session_history_position: Some( target .owner_state .navigation_history_state - .length_after_navigation(), + .position_after_navigation(), ), } } @@ -411,7 +411,7 @@ impl TargetNavigationLoadInputs { fetch_subresource_interception: (false, None), permission_overrides: Vec::new(), main_document_commit_seed: None, - session_history_length: None, + session_history_position: None, } } @@ -862,15 +862,6 @@ impl<'a> TargetSessionOwnerMut<'a> { .navigation_history_entry_url(page_snapshot, entry_id) } - pub(super) fn reset_navigation_history(&mut self) -> Option { - let page_snapshot = self.page_snapshot(); - Some( - self.target_mut() - .owner_state - .reset_navigation_history(page_snapshot), - ) - } - pub(super) fn can_reset_navigation_history(&mut self) -> Option { let page_snapshot = self.page_snapshot(); Some( @@ -897,10 +888,10 @@ impl<'a> TargetSessionOwnerMut<'a> { Some(()) } - pub(super) fn record_same_document_navigation( + pub(super) fn record_session_history_update( &mut self, url: &Url, - history_update: moli_core::page::SameDocumentHistoryUpdate, + history_update: moli_core::page::SessionHistoryUpdateKind, ) -> Option { let next_url = url.to_string(); let security_origin = url.origin().ascii_serialization(); @@ -910,7 +901,7 @@ impl<'a> TargetSessionOwnerMut<'a> { .map(|(_, title)| title.clone()) .unwrap_or_default(); let target = self.target_mut(); - target.owner_state.record_same_document_navigation_history( + target.owner_state.record_session_history_update( page_snapshot, next_url.clone(), title, @@ -2352,14 +2343,6 @@ impl CdpConnection { .navigation_history_entry_url(entry_id) } - pub(crate) fn reset_navigation_history_for_owner( - &mut self, - owner: &CommandOwnerScope, - ) -> Option { - self.target_session_owner_mut_for_owner(owner)? - .reset_navigation_history() - } - pub(crate) fn can_reset_navigation_history_for_session_owner( &mut self, session_id: Option<&str>, @@ -2385,14 +2368,33 @@ impl CdpConnection { .mark_next_navigation_history_traverse_to_entry(entry_id) } - pub(crate) fn record_same_document_navigation_for_owner( + pub(crate) fn record_session_history_update_for_owner( &mut self, owner: &CommandOwnerScope, - url: &Url, - history_update: moli_core::page::SameDocumentHistoryUpdate, - ) -> Option { - self.target_session_owner_mut_for_owner(owner)? - .record_same_document_navigation(url, history_update) + update: &moli_page_types::SessionHistoryUpdate, + ) { + let Ok(url) = Url::parse(&update.root_url) else { + return; + }; + let Some(mut target_owner) = self.target_session_owner_mut_for_owner(owner) else { + return; + }; + target_owner.record_session_history_update(&url, update.update); + target_owner + .target_mut() + .owner_state + .navigation_history_state + .synchronize_root_entry_url(&update.root_entry_steps, &update.root_url); + let (index, entries) = target_owner + .target_mut() + .owner_state + .navigation_history_state + .snapshot(); + debug_assert_eq!( + (index, entries.len()), + (update.position.index(), update.position.length()), + "browser and renderer joint history positions must agree" + ); } pub(super) fn target_session_owner_mut( @@ -3290,7 +3292,7 @@ mod tests { security_origin: "https://nav.example".to_owned(), secure_context_type: "Secure".to_owned(), timestamp: 0.0, - session_history_length: None, + session_history_position: None, }; { let mut owner = TargetSessionOwnerMut { diff --git a/moli-protocol/src/conn/dispatch_tests.rs b/moli-protocol/src/conn/dispatch_tests.rs index 059c4aab2c..90733366fa 100644 --- a/moli-protocol/src/conn/dispatch_tests.rs +++ b/moli-protocol/src/conn/dispatch_tests.rs @@ -2414,7 +2414,7 @@ async fn stale_initial_document_page_build_does_not_overwrite_committed_page() { security_origin: "null".to_owned(), secure_context_type: "InsecureScheme".to_owned(), timestamp: 0.0, - session_history_length: None, + session_history_position: None, }; conn.commit_loaded_navigation_target_identity_for_owner( &owner, diff --git a/moli-protocol/src/conn/state/navigation.rs b/moli-protocol/src/conn/state/navigation.rs index 0574546da4..db5d1b67c4 100644 --- a/moli-protocol/src/conn/state/navigation.rs +++ b/moli-protocol/src/conn/state/navigation.rs @@ -1,4 +1,4 @@ -use moli_core::page::SameDocumentHistoryUpdate; +use moli_core::page::SessionHistoryUpdateKind; #[derive(Debug, Clone, PartialEq, Eq)] pub struct PageNavigationHistoryEntry { @@ -47,19 +47,27 @@ impl TargetNavigationHistoryState { self.entries.is_empty() } - pub(crate) fn length_after_navigation(&self) -> usize { - match self.pending_update { + pub(crate) fn position_after_navigation(&self) -> moli_page_types::SessionHistoryPosition { + let (index, length) = match self.pending_update { Some( PendingNavigationHistoryUpdate::ReplaceCurrent | PendingNavigationHistoryUpdate::ReplaceInitialEmptyDocument, - ) => self.entries.len().max(1), - Some(PendingNavigationHistoryUpdate::TraverseToEntry(entry_id)) - if self.entries.iter().any(|entry| entry.id == entry_id) => - { - self.entries.len() + ) => (self.current_index.unwrap_or(0), self.entries.len().max(1)), + Some(PendingNavigationHistoryUpdate::TraverseToEntry(entry_id)) => { + if let Some(index) = self.entries.iter().position(|entry| entry.id == entry_id) { + (index, self.entries.len()) + } else { + let index = self.current_index.map_or(0, |index| index + 1); + (index, index + 1) + } } - _ => self.current_index.map_or(1, |index| index + 2), - } + None => { + let index = self.current_index.map_or(0, |index| index + 1); + (index, index + 1) + } + }; + moli_page_types::SessionHistoryPosition::new(index, length) + .expect("browser session history position must be valid") } pub(crate) fn allocate_entry_id(&mut self) -> i32 { @@ -165,6 +173,14 @@ impl TargetNavigationHistoryState { (self.current_index.unwrap_or(0), self.entries.clone()) } + pub(crate) fn synchronize_root_entry_url(&mut self, steps: &[usize], url: &str) { + for &index in steps { + if let Some(entry) = self.entries.get_mut(index) { + entry.url = url.to_owned(); + } + } + } + pub(crate) fn can_prune_all_but_current(&self) -> bool { !matches!( self.pending_update, @@ -222,14 +238,14 @@ impl TargetNavigationHistoryState { } } - pub(crate) fn record_same_document_update( + pub(crate) fn record_session_history_update( &mut self, url: String, title: String, - history_update: SameDocumentHistoryUpdate, + history_update: SessionHistoryUpdateKind, ) -> bool { match history_update { - SameDocumentHistoryUpdate::Push | SameDocumentHistoryUpdate::Replace => { + SessionHistoryUpdateKind::Push | SessionHistoryUpdateKind::Replace => { let mut entry = PageNavigationHistoryEntry { id: self.allocate_entry_id(), url, @@ -244,13 +260,15 @@ impl TargetNavigationHistoryState { }; self.assign_current_document_sequence_number(&mut entry); match history_update { - SameDocumentHistoryUpdate::Push => self.push_entry(entry), - SameDocumentHistoryUpdate::Replace => self.replace_current_entry(entry), - SameDocumentHistoryUpdate::Traverse { .. } => unreachable!(), + SessionHistoryUpdateKind::Push => self.push_entry(entry), + SessionHistoryUpdateKind::Replace => self.replace_current_entry(entry), + SessionHistoryUpdateKind::Traverse { .. } + | SessionHistoryUpdateKind::PruneAllButCurrent => unreachable!(), } true } - SameDocumentHistoryUpdate::Traverse { delta } => { + SessionHistoryUpdateKind::PruneAllButCurrent => self.prune_all_but_current(), + SessionHistoryUpdateKind::Traverse { delta } => { let Some(current_index) = self.current_index else { return false; }; @@ -285,21 +303,21 @@ mod tests { #[test] fn same_document_traversal_moves_cursor_without_allocating_or_appending() { let mut history = TargetNavigationHistoryState::default(); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/a".to_owned(), "A".to_owned(), - SameDocumentHistoryUpdate::Push, + SessionHistoryUpdateKind::Push, )); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/b".to_owned(), "B".to_owned(), - SameDocumentHistoryUpdate::Push, + SessionHistoryUpdateKind::Push, )); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/a".to_owned(), "ignored during traversal".to_owned(), - SameDocumentHistoryUpdate::Traverse { delta: -1 }, + SessionHistoryUpdateKind::Traverse { delta: -1 }, )); let (current_index, entries) = history.snapshot(); assert_eq!(current_index, 0); @@ -309,10 +327,10 @@ mod tests { vec![1, 2] ); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/c".to_owned(), "C".to_owned(), - SameDocumentHistoryUpdate::Push, + SessionHistoryUpdateKind::Push, )); let (current_index, entries) = history.snapshot(); assert_eq!(current_index, 1); @@ -329,25 +347,25 @@ mod tests { #[test] fn same_document_replace_preserves_entry_id_and_out_of_range_traverse_is_atomic() { let mut history = TargetNavigationHistoryState::default(); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/a".to_owned(), "A".to_owned(), - SameDocumentHistoryUpdate::Push, + SessionHistoryUpdateKind::Push, )); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/replaced".to_owned(), "Replaced".to_owned(), - SameDocumentHistoryUpdate::Replace, + SessionHistoryUpdateKind::Replace, )); let before = history.snapshot(); assert_eq!(before.0, 0); assert_eq!(before.1[0].id, 1); assert_eq!(before.1[0].url, "https://example.test/replaced"); - assert!(!history.record_same_document_update( + assert!(!history.record_session_history_update( "https://example.test/missing".to_owned(), String::new(), - SameDocumentHistoryUpdate::Traverse { delta: -1 }, + SessionHistoryUpdateKind::Traverse { delta: -1 }, )); assert_eq!(history.snapshot(), before); } diff --git a/moli-protocol/src/conn/state/navigation_outcome.rs b/moli-protocol/src/conn/state/navigation_outcome.rs index 2045ee1e55..f59a48cf62 100644 --- a/moli-protocol/src/conn/state/navigation_outcome.rs +++ b/moli-protocol/src/conn/state/navigation_outcome.rs @@ -145,7 +145,7 @@ impl RendererMainDocumentCommitSeed { security_origin, secure_context_type, timestamp: self.timestamp, - session_history_length: None, + session_history_position: None, } } } diff --git a/moli-protocol/src/conn/state/target_state.rs b/moli-protocol/src/conn/state/target_state.rs index 0ecc7de055..d17f6b7889 100644 --- a/moli-protocol/src/conn/state/target_state.rs +++ b/moli-protocol/src/conn/state/target_state.rs @@ -749,14 +749,6 @@ impl TargetOwnerState { self.navigation_history_state.snapshot() } - pub(crate) fn reset_navigation_history( - &mut self, - page_snapshot: Option<(String, String)>, - ) -> bool { - self.reconcile_navigation_history_page_snapshot(page_snapshot); - self.navigation_history_state.prune_all_but_current() - } - pub(crate) fn can_reset_navigation_history( &mut self, page_snapshot: Option<(String, String)>, @@ -783,12 +775,12 @@ impl TargetOwnerState { self.clear_loaded_document_context_state(); } - pub(crate) fn record_same_document_navigation_history( + pub(crate) fn record_session_history_update( &mut self, page_snapshot: Option<(String, String)>, url: String, mut title: String, - history_update: moli_core::page::SameDocumentHistoryUpdate, + history_update: moli_core::page::SessionHistoryUpdateKind, ) { self.reconcile_navigation_history_page_snapshot(page_snapshot); if let Some(committed_title) = self.committed_document_title() { @@ -796,7 +788,7 @@ impl TargetOwnerState { } let _ = self.navigation_history_state - .record_same_document_update(url, title, history_update); + .record_session_history_update(url, title, history_update); } pub(crate) fn clear_observable_output_state(&mut self) { diff --git a/moli-protocol/src/conn/state/tests.rs b/moli-protocol/src/conn/state/tests.rs index 38a98bbbb5..a8c8896225 100644 --- a/moli-protocol/src/conn/state/tests.rs +++ b/moli-protocol/src/conn/state/tests.rs @@ -1,6 +1,6 @@ use crate::devtools_runtime::DevToolsNetworkResourceType; use moli_cookie_jar::new_shared_browser_cookie_store; -use moli_core::page::{SameDocumentHistoryUpdate, SubresourceResourceType}; +use moli_core::page::{SessionHistoryUpdateKind, SubresourceResourceType}; use super::super::fetch_support::{ FetchAuthChallenge, PendingFetchAuthNavigation, PendingFetchNavigation, @@ -1130,10 +1130,10 @@ fn navigation_history_prune_rejects_only_pending_existing_entry_traversal() { transition_type: "typed".to_owned(), document_sequence_number: None, }); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/pushed".to_owned(), "pushed".to_owned(), - SameDocumentHistoryUpdate::Push, + SessionHistoryUpdateKind::Push, )); let pushed_id = history.snapshot().1[1].id; @@ -1168,10 +1168,10 @@ fn navigation_history_traversal_reuses_same_document_entries() { transition_type: "typed".to_owned(), document_sequence_number: None, }); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/page?state=pushed".to_owned(), "page".to_owned(), - SameDocumentHistoryUpdate::Push, + SessionHistoryUpdateKind::Push, )); let (_, entries) = history.snapshot(); @@ -1183,10 +1183,10 @@ fn navigation_history_traversal_reuses_same_document_entries() { assert_eq!(entries[1].user_typed_url, "https://example.test/page"); assert_eq!(entries[1].transition_type, "link"); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/page".to_owned(), "page".to_owned(), - SameDocumentHistoryUpdate::Traverse { delta: -1 }, + SessionHistoryUpdateKind::Traverse { delta: -1 }, )); let (current_index, entries) = history.snapshot(); assert_eq!(current_index, 0); @@ -1194,10 +1194,10 @@ fn navigation_history_traversal_reuses_same_document_entries() { assert_eq!(entries[0].id, initial_id); assert_eq!(entries[1].id, pushed_id); - assert!(history.record_same_document_update( + assert!(history.record_session_history_update( "https://example.test/page?state=pushed".to_owned(), "page".to_owned(), - SameDocumentHistoryUpdate::Traverse { delta: 1 }, + SessionHistoryUpdateKind::Traverse { delta: 1 }, )); let (current_index, entries) = history.snapshot(); assert_eq!(current_index, 1); diff --git a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs index 831db52169..5da79f55a7 100644 --- a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs +++ b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs @@ -278,6 +278,18 @@ impl PreparedProtocolOutputs { ) .append_to_child_frame_output_sink(&mut prepared); } + RendererOwnerAction::SessionHistoryUpdate { + source_document, + update, + } => { + crate::domains::page::PagePreparedOutputs::from_renderer_session_history_update( + conn, + owner, + source_document, + update, + ) + .append_to_session_history_output_sink(&mut prepared); + } RendererOwnerAction::SameDocumentNavigation(navigation) => { crate::domains::page::PagePreparedOutputs::from_renderer_same_document_navigation( conn, owner, navigation, diff --git a/moli-protocol/src/domains/activity/output_slot.rs b/moli-protocol/src/domains/activity/output_slot.rs index 3f6e5c3219..fb547312b9 100644 --- a/moli-protocol/src/domains/activity/output_slot.rs +++ b/moli-protocol/src/domains/activity/output_slot.rs @@ -57,6 +57,7 @@ pub(in crate::domains) enum ProtocolOutputSlot { DomStorage, ChildFrameActivity, SameDocumentNavigation, + SessionHistoryUpdate, } impl ProtocolOutputSlot { @@ -74,6 +75,7 @@ impl ProtocolOutputSlot { | Self::RuntimeInspectorPostResponseMessages | Self::RuntimeObservable | Self::ChildFrameActivity + | Self::SessionHistoryUpdate | Self::SameDocumentNavigation | Self::TopLevelLocationNavigation | Self::TopLevelHistoryTraversal => ProtocolOutputDelivery::OwnerAction, @@ -134,6 +136,7 @@ impl ProtocolOutputSlot { | Self::DocumentLifecycle | Self::RuntimeObservable | Self::DomStorage + | Self::SessionHistoryUpdate | Self::SameDocumentNavigation => ProtocolOutputResponseOrder::BeforeResponse, } } @@ -228,6 +231,7 @@ impl ProtocolOutputSlot { | Self::DocumentTitleChanged | Self::DocumentLifecycle | Self::ChildFrameActivity + | Self::SessionHistoryUpdate | Self::SameDocumentNavigation | Self::TopLevelLocationNavigation | Self::TopLevelHistoryTraversal => { @@ -305,6 +309,7 @@ mod tests { (DomStorage, ProtocolObservation, BeforeResponse), (ChildFrameActivity, OwnerAction, AfterResponse), (SameDocumentNavigation, OwnerAction, BeforeResponse), + (SessionHistoryUpdate, OwnerAction, BeforeResponse), ]; let unique = cases diff --git a/moli-protocol/src/domains/activity/runtime_command_barrier.rs b/moli-protocol/src/domains/activity/runtime_command_barrier.rs index 42cce5b2e8..2aec795ba0 100644 --- a/moli-protocol/src/domains/activity/runtime_command_barrier.rs +++ b/moli-protocol/src/domains/activity/runtime_command_barrier.rs @@ -484,7 +484,6 @@ mod tests { RendererDocumentSourcedTopLevelLocationNavigation, RendererDocumentToken, RendererFrameToken, RendererLifecycleEpoch, RendererPendingSameDocumentNavigation, RendererRuntimeInspectorMessage, RendererRuntimeInspectorMessageBatch, - SameDocumentHistoryUpdate, }, }; use serde_json::Value; @@ -589,7 +588,6 @@ mod tests { RendererPendingSameDocumentNavigation { url: format!("data:text/html,runtime-command-barrier#{fragment}"), navigation_type: "fragment".to_owned(), - history_update: SameDocumentHistoryUpdate::Push, }, ), ), diff --git a/moli-protocol/src/domains/page.rs b/moli-protocol/src/domains/page.rs index a4a9979d3a..6991122edb 100644 --- a/moli-protocol/src/domains/page.rs +++ b/moli-protocol/src/domains/page.rs @@ -531,6 +531,7 @@ enum PageOutputProjectionStep { DocumentLifecycle, ChildFrameActivity, SameDocumentNavigation, + SessionHistoryUpdate, TopLevelLocationNavigation, TopLevelHistoryTraversal, } @@ -544,6 +545,11 @@ pub(crate) struct PagePreparedOutputs { document_lifecycle_events: Vec, child_frame_activities: Vec, same_document_navigations: Vec, + session_history_updates: Vec<( + crate::conn::TargetPageResidenceIdentity, + RendererDocumentLifecycleIdentity, + moli_page_types::SessionHistoryUpdate, + )>, top_level_location_navigation: Option, top_level_history_traversal: Option, } @@ -616,6 +622,31 @@ impl PagePreparedOutputs { } } + pub(crate) fn from_renderer_session_history_update( + conn: &CdpConnection, + owner: &CommandOwnerScope, + source_document: RendererDocumentLifecycleIdentity, + update: moli_page_types::SessionHistoryUpdate, + ) -> Self { + let Some(residence) = conn.target_page_residence_identity_for_owner(owner) else { + return Self::default(); + }; + Self { + session_history_updates: vec![(residence, source_document, update)], + ..Self::default() + } + } + + pub(in crate::domains) fn append_to_session_history_output_sink( + self, + sink: &mut (impl ProtocolOutputSink + ?Sized), + ) { + if !self.session_history_updates.is_empty() { + sink.push_produced_slot(ProtocolOutputSlot::SessionHistoryUpdate); + sink.push_prepared_payload(PagePreparedOutputSlot::from_outputs(self).into()); + } + } + pub(crate) fn from_renderer_same_document_navigation( conn: &CdpConnection, owner: &CommandOwnerScope, @@ -972,6 +1003,7 @@ impl PagePreparedOutputs { document_lifecycle_events: Vec::new(), child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), + session_history_updates: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -998,6 +1030,7 @@ impl PagePreparedOutputs { document_lifecycle_events: Vec::new(), child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), + session_history_updates: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -1045,6 +1078,7 @@ impl PagePreparedOutputs { document_lifecycle_events: Vec::new(), child_frame_activities: vec![activity], same_document_navigations: Vec::new(), + session_history_updates: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -1068,6 +1102,7 @@ impl PagePreparedOutputs { PagePreparedSameDocumentNavigation::new(owner.clone(), navigation) }) .collect(), + session_history_updates: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -1086,6 +1121,7 @@ impl PagePreparedOutputs { document_lifecycle_events: Vec::new(), child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), + session_history_updates: Vec::new(), top_level_location_navigation: navigation .map(|navigation| PagePreparedTopLevelLocationNavigation::new(owner, navigation)), top_level_history_traversal: None, @@ -1336,6 +1372,19 @@ impl PageOutputProjectionStep { context.command.protocol_events_mut().extend(events); } } + PageOutputProjectionStep::SessionHistoryUpdate => { + if let Some(slot) = prepared_outputs.and_then(ProtocolOutputPayloads::page_mut) { + for (residence, _source_document, update) in + std::mem::take(&mut slot.outputs.session_history_updates) + { + // document.open() does not undo committed history. Page + // residence, not current Document identity, is authority. + if conn.target_page_residence_identity_is_current(&residence) { + conn.record_session_history_update_for_owner(&owner, &update); + } + } + } + } PageOutputProjectionStep::SameDocumentNavigation => { let mut events = Vec::new(); emit_same_document_navigation_activity_background_events_async( @@ -1384,6 +1433,7 @@ pub(in crate::domains) async fn project_page_output_async( ProtocolOutputSlot::DocumentTitleChanged => PageOutputProjectionStep::DocumentTitleChanged, ProtocolOutputSlot::DocumentLifecycle => PageOutputProjectionStep::DocumentLifecycle, ProtocolOutputSlot::ChildFrameActivity => PageOutputProjectionStep::ChildFrameActivity, + ProtocolOutputSlot::SessionHistoryUpdate => PageOutputProjectionStep::SessionHistoryUpdate, ProtocolOutputSlot::SameDocumentNavigation => { PageOutputProjectionStep::SameDocumentNavigation } @@ -2646,7 +2696,6 @@ mod producer_tests { RendererPendingSameDocumentNavigation { url: url.to_owned(), navigation_type: "fragment".to_owned(), - history_update: moli_core::page::SameDocumentHistoryUpdate::Push, }, ) } @@ -4445,6 +4494,7 @@ mod producer_tests { document, )], same_document_navigations: Vec::new(), + session_history_updates: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, }; @@ -4902,6 +4952,47 @@ mod producer_tests { ); } + async fn emit_committed_history_and_navigation_for_test( + conn: &mut CdpConnection, + out: &mut Vec, + owner: &CommandOwnerScope, + mut prepared: Option<&mut ProtocolOutputPayloads>, + ) { + // Production sends a separate committed-history action before the + // observer notification. Exercise both projections and their authority. + if let Some(slot) = prepared + .as_deref_mut() + .and_then(ProtocolOutputPayloads::page_mut) + { + slot.outputs.session_history_updates = slot + .outputs + .same_document_navigations + .iter() + .map(|navigation| { + ( + navigation.owner().clone(), + navigation.source_document(), + moli_page_types::SessionHistoryUpdate { + position: moli_page_types::SessionHistoryPosition::INITIAL, + update: moli_page_types::SessionHistoryUpdateKind::Push, + root_url: navigation.clone().into_navigation().url, + root_entry_steps: vec![0], + }, + ) + }) + .collect(); + } + let mut command = crate::conn::CommandDispatchContext::default(); + let mut context = ProtocolOutputProjectionContext::new(owner, &mut command); + super::PageOutputProjectionStep::SessionHistoryUpdate + .project_async(conn, &mut context, prepared.as_deref_mut()) + .await; + super::emit_same_document_navigation_activity_background_events_async( + conn, out, owner, prepared, + ) + .await; + } + #[tokio::test(flavor = "multi_thread")] async fn same_document_drain_consumes_prepared_navigations_without_page_readback() { let mut conn = CdpConnection::default(); @@ -4924,7 +5015,7 @@ mod producer_tests { ), )); - super::emit_same_document_navigation_activity_background_events_async( + emit_committed_history_and_navigation_for_test( &mut conn, &mut out, &CommandOwnerScope::for_session("SID-1"), @@ -5012,7 +5103,7 @@ mod producer_tests { )); let mut out = Vec::new(); - super::emit_same_document_navigation_activity_background_events_async( + emit_committed_history_and_navigation_for_test( &mut conn, &mut out, &CommandOwnerScope::for_session("SID-document-open-same-document"), @@ -5064,7 +5155,7 @@ mod producer_tests { )); let mut out = Vec::new(); - super::emit_same_document_navigation_activity_background_events_async( + emit_committed_history_and_navigation_for_test( &mut conn, &mut out, &CommandOwnerScope::for_session("SID-stale-page-same-document"), diff --git a/moli-protocol/src/domains/page/navigation.rs b/moli-protocol/src/domains/page/navigation.rs index 03607573b5..2e50aba8bf 100644 --- a/moli-protocol/src/domains/page/navigation.rs +++ b/moli-protocol/src/domains/page/navigation.rs @@ -10,10 +10,7 @@ use crate::devtools_runtime::{ use chromiumoxide_cdp::cdp::browser_protocol::page::{ NavigateParams, NavigateToHistoryEntryParams, ReloadParams, }; -use moli_core::page::{ - ChildFrameDocumentOpenedSnapshot, CompletedPageCommand, PendingPageCommand, - SameDocumentHistoryUpdate, -}; +use moli_core::page::{ChildFrameDocumentOpenedSnapshot, CompletedPageCommand, PendingPageCommand}; use moli_fetch::NET_ERR_ABORTED_ERROR_TEXT; use moli_url_policy::{LocalFileNavigationAccess, route_navigation_url}; use serde_json::{Value, json}; @@ -2523,14 +2520,9 @@ pub(super) fn complete_reset_navigation_history_command( )); } } - match conn.reset_navigation_history_for_owner(owner) { - Some(true) => PageCommandTaskStep::Complete(CommandOutputPlan::success()), - Some(false) => PageCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "History cannot be pruned", - )), - None => PageCommandTaskStep::Complete(CommandOutputPlan::error(-32000, "NoDocumentLoaded")), - } + // The renderer publishes the prune before disposal callbacks run. Its + // owner-action FIFO also preserves any history pushes from those callbacks. + PageCommandTaskStep::Complete(CommandOutputPlan::success()) } fn reloaded_after_crash_session_ids( @@ -3427,11 +3419,8 @@ fn emit_same_document_navigation_background_event( owner: &CommandOwnerScope, url: Url, navigation_type: &str, - history_update: SameDocumentHistoryUpdate, ) { - let Some(frame_id) = - conn.record_same_document_navigation_for_owner(owner, &url, history_update) - else { + let Some(frame_id) = conn.runtime_session_owner_frame_id_for_owner(owner) else { return; }; for event_session_id in conn.page_event_session_ids_for_owner(owner) { @@ -3487,7 +3476,6 @@ pub(crate) async fn emit_same_document_navigation_background_events_async( owner, url, &navigation.navigation_type, - navigation.history_update, ); } } diff --git a/moli-protocol/src/domains/page/tests/document_content.rs b/moli-protocol/src/domains/page/tests/document_content.rs index 612165ce2c..633dcd81bf 100644 --- a/moli-protocol/src/domains/page/tests/document_content.rs +++ b/moli-protocol/src/domains/page/tests/document_content.rs @@ -35,6 +35,12 @@ async fn install_document_content_test_page(ctx: &mut TestContext, url: &str) { .active_page_target_mut() .runtime_slot .set_loaded_page_for_test(navigation.page); + // Match the browser commit paired with the position supplied to the + // renderer; this fixture bypasses the production Page.navigate path. + browser_context + .active_page_target_mut() + .owner_state + .record_loaded_page_navigation_history((url.to_owned(), String::new())); assert!( browser_context .active_page_target_mut() diff --git a/moli-protocol/src/domains/page/tests/navigation.rs b/moli-protocol/src/domains/page/tests/navigation.rs index e5652c5d37..b1f8667826 100644 --- a/moli-protocol/src/domains/page/tests/navigation.rs +++ b/moli-protocol/src/domains/page/tests/navigation.rs @@ -359,6 +359,131 @@ async fn renderer_fragment_navigation_preserves_initial_document_residence() { ); } +#[tokio::test(flavor = "multi_thread")] +async fn joint_session_history_syncs_child_steps_cursor_and_reload_bootstrap() { + async fn page() -> impl axum::response::IntoResponse { + ( + [(axum::http::header::CONTENT_TYPE.as_str(), "text/html")], + "", + ) + } + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + axum::serve( + listener, + axum::Router::new().route("/page", axum::routing::get(page)), + ) + .await + .unwrap(); + }); + let mut ctx = TestContext::new(); + load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank"); + ctx.process_async(json!({"id": 9280, "method":"Page.navigate", "sessionId":"SID-1", "params":{"url":format!("http://{addr}/page")}})).await; + assert!(take_response_by_id(&mut ctx, 9280)["error"].is_null()); + wait_until_message( + &mut ctx, + Some("SID-1"), + "joint history page load", + |message| message["method"] == json!("Page.domContentEventFired"), + ) + .await; + + let operations = [ + ( + r#"(async () => { + for (const id of ['a', 'b']) { + const f = document.createElement('iframe'); f.id = id; f.srcdoc = '

child

'; + await new Promise(resolve => { f.onload = resolve; document.body.append(f); }); + } + globalThis.a = document.getElementById('a').contentWindow; + globalThis.b = document.getElementById('b').contentWindow; + globalThis.held = [history, a.history, b.history]; + history.replaceState('top', ''); a.history.replaceState('a0', ''); b.history.replaceState('b0', ''); + a.history.pushState('a1', ''); b.history.pushState('b1', ''); a.history.pushState('a2', ''); + return held.map(h => h.length); + })()"#, + json!([5, 5, 5]), + 4, + 5, + ), + ( + r#"new Promise(resolve => { + b.addEventListener('popstate', () => resolve(held.map(h => h.length)), {once:true}); + history.go(-2); + })"#, + json!([5, 5, 5]), + 2, + 5, + ), + ( + "b.history.pushState('b2', ''); held.map(h => h.length)", + json!([4, 4, 4]), + 3, + 4, + ), + ]; + for (expression, expected, index, length) in operations { + ctx.process_async(json!({"id":9281,"method":"Runtime.evaluate","sessionId":"SID-1", "params":{"expression":expression,"awaitPromise":true,"returnByValue":true}})).await; + wait_until_message( + &mut ctx, + Some("SID-1"), + "joint history operation", + |message| message["id"] == json!(9281), + ) + .await; + let value = take_response_by_id(&mut ctx, 9281); + assert!(value["error"].is_null(), "{value}"); + assert_eq!(value["result"]["result"]["value"], expected, "{value}"); + ctx.process_async( + json!({"id":9282,"method":"Page.getNavigationHistory","sessionId":"SID-1"}), + ) + .await; + let browser = take_response_by_id(&mut ctx, 9282); + assert_eq!(browser["result"]["currentIndex"], json!(index), "{browser}"); + assert_eq!( + browser["result"]["entries"].as_array().unwrap().len(), + length, + "{browser}" + ); + } + ctx.process_async(json!({"id":9285,"method":"Runtime.evaluate","sessionId":"SID-1", "params":{"expression":r#" + history.pushState('before-reset', ''); + navigation.entries()[0].addEventListener('dispose', () => history.pushState('from-dispose', '')); + 'armed' + "#}})).await; + assert!(take_response_by_id(&mut ctx, 9285)["result"]["exceptionDetails"].is_null()); + ctx.process_async( + json!({"id":9286,"method":"Page.resetNavigationHistory","sessionId":"SID-1"}), + ) + .await; + assert!(take_response_by_id(&mut ctx, 9286)["error"].is_null()); + ctx.process_async(json!({"id":9287,"method":"Runtime.evaluate","sessionId":"SID-1", "params":{"expression":"[...held.map(h => h.length), a.navigation.entries().length, b.navigation.entries().length, history.state]","returnByValue":true}})).await; + assert_eq!( + take_response_by_id(&mut ctx, 9287)["result"]["result"]["value"], + json!([2, 2, 2, 1, 1, "from-dispose"]) + ); + ctx.process_async(json!({"id":9288,"method":"Page.getNavigationHistory","sessionId":"SID-1"})) + .await; + let reset = take_response_by_id(&mut ctx, 9288); + assert_eq!(reset["result"]["currentIndex"], json!(1), "{reset}"); + assert_eq!(reset["result"]["entries"].as_array().unwrap().len(), 2); + ctx.sent.clear(); + ctx.process_async(json!({"id":9283,"method":"Page.reload","sessionId":"SID-1"})) + .await; + assert!(take_response_by_id(&mut ctx, 9283)["error"].is_null()); + wait_until_message(&mut ctx, Some("SID-1"), "joint history reload", |message| { + message["method"] == json!("Page.domContentEventFired") + }) + .await; + ctx.process_async(json!({"id":9284,"method":"Runtime.evaluate","sessionId":"SID-1", "params":{"expression":"[initialHistoryLength, history.length]","returnByValue":true}})).await; + assert_eq!( + take_response_by_id(&mut ctx, 9284)["result"]["result"]["value"], + json!([2, 2]) + ); + server.abort(); +} + #[tokio::test(flavor = "multi_thread")] async fn navigation_bootstraps_browser_history_length_before_author_scripts() { async fn page() -> impl axum::response::IntoResponse { @@ -1284,7 +1409,7 @@ async fn reset_navigation_history_prunes_browser_and_renderer_history() { assert_eq!( reentrant_renderer_state["result"]["result"]["value"], json!({ - "historyLength": 1, + "historyLength": 2, "navigationLength": 2, "currentIndex": 1, "currentUrl": format!("{page_url}#during-dispose"), diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 3c6bf677d9..215aaafe3b 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -70,10 +70,12 @@ mod navigation_traversal_execution; mod navigation_traversal_plan; mod navigation_window; mod navigator_runtime; +mod session_history; #[cfg(test)] pub(crate) use navigator_runtime::{ materialized_navigator_subobject_keys, navigator_storage_wrapper_diagnostics, }; +pub(crate) use session_history::prune_joint_session_history; mod notification_runtime; mod observer_template; mod opfs; @@ -251,7 +253,6 @@ pub(crate) use self::file_api::{ }; pub(crate) use self::form_data_runtime::form_data_request_body; use self::geometry_runtime::{build_dom_point_object, optional_dom_point_init_arg}; -pub(crate) use self::history_runtime::increment_top_level_history_length_for_runtime_owner; pub(crate) use self::image_data::{ ImageDataClonePayload, build_image_data_object_from_clone_payload, image_data_clone_payload_from_object, @@ -324,7 +325,7 @@ pub(crate) use self::navigation_events::dispatch_srcdoc_navigation_navigate_even pub(crate) use self::navigation_mutation::apply_local_window_location_navigation; pub(crate) use self::navigation_restore::{ install_navigation_bootstrap_entry, install_navigation_bootstrap_entry_for_holder, - install_session_history_length, + install_navigation_entry_view_for_holder, }; pub(crate) use self::navigation_traversal::queue_top_level_history_traversal_by_delta; pub(crate) use self::navigator_runtime::install_worker_navigator_runtime_state; @@ -992,3 +993,7 @@ pub(crate) fn live_ranges_detached_text_split<'s>( } pub(crate) use self::window_accessors::current_window_style_viewport; + +pub(crate) use session_history::{ + initialize_main_session_history, install_session_history_position, +}; diff --git a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs index 2dd2c519a3..f56900314a 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs @@ -16,7 +16,6 @@ use super::navigation_events::{ run_navigation_precommit_deferred_handlers, }; use super::navigation_lifecycle::finish_navigation_error_events; -use super::navigation_projection::set_history_length_after_push; use super::navigation_result::{ cancel_pending_same_document_navigation_finishes, cancel_pending_same_document_navigation_finishes_including_reentrant, @@ -28,7 +27,6 @@ use super::navigation_window::{ }; use super::*; use crate::webidl; -use moli_page_types::SameDocumentHistoryUpdate; struct ParsedHistoryMutationArgs<'s> { state: v8::Local<'s, v8::Value>, @@ -184,8 +182,17 @@ fn mutate_history_object<'s>( let current_index = history_index(scope, history); let current_navigation_index = navigation_current_entry_index(scope, owner).unwrap_or(0); let previous_entry = navigation_current_entry(scope, owner); + let mut pruned = Vec::new(); let entry = match kind { HistoryMutationKind::Push => { + for index in current_index + 1..entries.length() { + if let Some(entry) = entries + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + pruned.push(entry); + } + } let next_entries = v8::Array::new(scope, (current_index + 2) as i32); for index in 0..=current_index { if let Some(entry) = entries.get_index(scope, index) { @@ -211,7 +218,6 @@ fn mutate_history_object<'s>( let _ = next_entries.set_index(scope, next_index, entry.into()); set_history_entries(scope, history, next_entries); set_history_index(scope, history, next_index); - set_history_length_after_push(scope, history, entries, next_entries); entry } HistoryMutationKind::Replace => { @@ -247,6 +253,16 @@ fn mutate_history_object<'s>( set_history_state(scope, history, current_state); sync_location_object(scope, location, url.as_str()); sync_navigation_current_entry_from_history_entry(scope, owner, entry); + super::session_history::commit( + scope, + owner, + entry, + match kind { + HistoryMutationKind::Push => moli_page_types::SessionHistoryCommit::Push, + HistoryMutationKind::Replace => moli_page_types::SessionHistoryCommit::Replace, + }, + ); + pruned.extend(super::session_history::prune_views(scope, owner)); if let Some(navigation) = window_navigation_for_holder(scope, owner) { refresh_navigation_destination_indexes(scope, navigation, history); dispatch_navigation_currententrychange( @@ -290,6 +306,9 @@ fn mutate_history_object<'s>( ); } } + for entry in pruned { + dispatch_navigation_entry_dispose(scope, entry); + } sync_child_navigation_entry_seed_from_owner(scope, owner); if runtime_window_is_global(scope, owner) { let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { @@ -297,11 +316,7 @@ fn mutate_history_object<'s>( }; let host = unsafe { &mut *host_ptr }; host.set_document_url(url.clone()); - let history_update = match kind { - HistoryMutationKind::Push => SameDocumentHistoryUpdate::Push, - HistoryMutationKind::Replace => SameDocumentHistoryUpdate::Replace, - }; - host.record_same_document_navigation(&url, "historyApi", history_update); + host.record_same_document_navigation(&url, "historyApi"); } else if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime.rs index 8d9ec68bd8..cd5fb52ee0 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime.rs @@ -1,12 +1,10 @@ mod admission; mod apply; -mod length; mod results; mod traversal; pub(super) use self::admission::require_fully_active_history_owner; pub(super) use self::apply::apply_history_entry; -pub(crate) use self::length::increment_top_level_history_length_for_runtime_owner; pub(super) use self::results::reject_pending_navigation_results; pub(super) use self::traversal::{ apply_pending_history_traversal, cancel_active_history_traversal_intercept_settlement, diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs index 061b365c32..a3243314e1 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/apply.rs @@ -2,30 +2,25 @@ use super::super::location_runtime::{ is_same_document_fragment_navigation, location_href_slot, sync_location_object, }; use super::super::navigation_entry::{ - history_entries, history_index, navigation_current_entry, navigation_current_entry_index, - navigation_entries_share_document, navigation_entry_initial_index, - navigation_entry_joint_top_index, navigation_entry_url_value, - restore_current_navigation_entry_scroll_position, set_history_index, set_history_state, - sync_navigation_current_entry_from_history_entry, + history_entries, history_index, navigation_current_entry, navigation_entries_share_document, + navigation_entry_url_value, restore_current_navigation_entry_scroll_position, + set_history_index, set_history_state, sync_navigation_current_entry_from_history_entry, }; use super::super::navigation_events::{ dispatch_navigation_currententrychange, dispatch_navigation_success, dispatch_popstate_event, navigation_has_active_scroll_event, queue_hash_change_for_runtime_owner, }; use super::super::navigation_mutation::sync_local_document_front_from_window; -use super::super::navigation_projection::build_visible_navigation_entries_array; use super::super::navigation_result::perform_navigation_scroll_if_needed; use super::super::navigation_serialize::sync_child_navigation_entry_seed_from_owner; use super::super::navigation_window::{ - navigation_document_has_opaque_origin, runtime_top_window_owner, runtime_window_is_global, - runtime_window_owner, window_history_for_holder, window_location_for_holder, - window_navigation_for_holder, + navigation_document_has_opaque_origin, runtime_window_is_global, runtime_window_owner, + window_location_for_holder, window_navigation_for_holder, }; use super::super::*; use super::results::{resolve_pending_navigation_committed, resolve_pending_navigation_finished}; use crate::native_bridge::PendingNavigationResult; use crate::script_vm::perform_microtask_checkpoint_and_report_pending_promise_rejections; -use moli_page_types::SameDocumentHistoryUpdate; pub(in crate::context_bootstrap) struct AppliedHistoryEntry<'s> { pub(in crate::context_bootstrap) owner: v8::Local<'s, v8::Object>, @@ -38,16 +33,26 @@ pub(in crate::context_bootstrap) struct AppliedHistoryEntry<'s> { history_index: u32, entry: v8::Local<'s, v8::Object>, previous_entry: Option>, + additional: Vec>, } pub(in crate::context_bootstrap) fn apply_history_entry<'s>( scope: &mut v8::PinScope<'s, '_>, history: v8::Local<'s, v8::Object>, index: u32, + joint_step: Option, dispatch_popstate: bool, pending_results: Option<&[PendingNavigationResult]>, ) { - let Some(applied) = apply_history_entry_commit(scope, history, index) else { + let Some(applied) = apply_history_entry_commit(scope, history, index, joint_step) else { + if let Some(results) = pending_results { + let error = super::super::navigation_result::navigation_dom_exception( + scope, + "Navigation was canceled", + "AbortError", + ); + super::results::reject_pending_navigation_results(scope, results, error); + } return; }; if let Some(results) = pending_results { @@ -81,6 +86,99 @@ pub(in crate::context_bootstrap) fn apply_history_entry_commit<'s>( scope: &mut v8::PinScope<'s, '_>, history: v8::Local<'s, v8::Object>, index: u32, + joint_step: Option, +) -> Option> { + let owner = runtime_window_owner(scope, history); + let entry = history_entries(scope, history)? + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let step = + joint_step.or_else(|| super::super::session_history::step_for_entry(scope, owner, entry)); + let targets = match step { + Some(step) => super::super::session_history::targets_at(scope, owner, step)?, + None => Vec::new(), + }; + let mut additional = Vec::new(); + let mut cross_document = Vec::new(); + for target in targets { + if target.history.strict_equals(history.into()) { + continue; + } + let entries = history_entries(scope, target.history)?; + let current = entries + .get_index(scope, target.current_index) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let next = entries + .get_index(scope, target.target_index) + .and_then(|value| v8::Local::::try_from(value).ok())?; + if !navigation_entries_share_document(scope, current, next) { + cross_document.push(target); + continue; + } + if let Some(navigation) = window_navigation_for_holder(scope, target.owner) + && !super::super::navigation_events::dispatch_navigation_traverse_event( + scope, + navigation, + target.history, + target.target_index, + ) + { + return None; + } + additional.push(target); + } + // Admission can run script, so validate the stable step again before any + // local entry is changed. Forward pruning invalidates it atomically. + if let Some(step) = step { + let host = unsafe { &mut *context_host_ptr_from_global_bridge(scope)? }; + let binding = super::super::session_history::binding(scope, host, owner); + let expected = host + .session_histories + .get_mut(binding.popup) + .entries_at(step)? + .get(&binding.context)? + .clone(); + let live = history_entries(scope, history)? + .get_index(scope, index) + .and_then(|entry| v8::Local::::try_from(entry).ok()) + .and_then(|entry| super::super::session_history::entry_reference(scope, entry))?; + if live != expected { + return None; + } + } + let delta = match step { + Some(step) => Some(super::super::session_history::commit_traversal( + scope, owner, step, + )?), + None => None, + }; + let mut applied = apply_local_history_entry_commit(scope, history, index)?; + for target in additional { + if let Some(entry) = + apply_local_history_entry_commit(scope, target.history, target.target_index) + { + applied.additional.push(entry); + } + } + if let Some(delta) = delta.filter(|delta| *delta != 0) { + super::super::session_history::publish( + scope, + owner, + moli_page_types::SessionHistoryUpdateKind::Traverse { delta }, + ); + } + for target in cross_document { + super::super::navigation_traversal_execution::queue_history_traversal_without_result( + scope, target, + ); + } + Some(applied) +} + +fn apply_local_history_entry_commit<'s>( + scope: &mut v8::PinScope<'s, '_>, + history: v8::Local<'s, v8::Object>, + index: u32, ) -> Option> { let owner = runtime_window_owner(scope, history); let previous_history_index = history_index(scope, history); @@ -103,7 +201,6 @@ pub(in crate::context_bootstrap) fn apply_history_entry_commit<'s>( return None; }; sync_navigation_current_entry_from_history_entry(scope, owner, entry); - sync_top_history_after_child_traversal(scope, owner, entry, previous_entry); let resolved_entry = navigation_current_entry(scope, owner) .map(v8::Local::::from) .unwrap_or_else(|| v8::undefined(scope).into()); @@ -119,78 +216,7 @@ pub(in crate::context_bootstrap) fn apply_history_entry_commit<'s>( history_index: index, entry, previous_entry, - }) -} - -fn sync_top_history_after_child_traversal<'s>( - scope: &mut v8::PinScope<'s, '_>, - owner: v8::Local<'s, v8::Object>, - child_entry: v8::Local<'s, v8::Object>, - previous_child_entry: Option>, -) { - if runtime_window_is_global(scope, owner) { - return; - } - let Some(previous_child_index) = - previous_child_entry.and_then(|entry| navigation_entry_initial_index(scope, entry)) - else { - return; - }; - let Some(target_index) = navigation_entry_joint_top_index(scope, child_entry) - .or_else(|| navigation_entry_initial_index(scope, child_entry)) - else { - return; - }; - let top_owner = runtime_top_window_owner(scope, owner); - if top_owner.strict_equals(owner.into()) { - return; - } - if navigation_current_entry_index(scope, top_owner) - .is_none_or(|top_index| top_index <= previous_child_index) - { - return; - } - let Some(top_history) = window_history_for_holder(scope, top_owner) else { - return; - }; - let Some(top_entries) = history_entries(scope, top_history) else { - return; - }; - let top_current_entry = navigation_current_entry(scope, top_owner); - let visible_entries = - build_visible_navigation_entries_array(scope, top_entries, top_current_entry); - let Some(top_entry) = visible_entries - .get_index(scope, target_index) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { - return; - }; - let Some(raw_index) = raw_index_for_entry(scope, top_entries, top_entry) else { - return; - }; - - set_history_index(scope, top_history, raw_index); - let state = super::super::navigation_entry_state::clone_history_entry_state(scope, top_entry) - .unwrap_or_else(|| v8::null(scope).into()); - set_history_state(scope, top_history, state); - if let Some(url) = navigation_entry_url_value(scope, top_entry) - && let Some(location) = window_location_for_holder(scope, top_owner) - { - sync_location_object(scope, location, &url); - } - sync_navigation_current_entry_from_history_entry(scope, top_owner, top_entry); -} - -fn raw_index_for_entry<'s>( - scope: &mut v8::PinScope<'s, '_>, - entries: v8::Local<'s, v8::Array>, - target: v8::Local<'s, v8::Object>, -) -> Option { - (0..entries.length()).find(|index| { - entries - .get_index(scope, *index) - .and_then(|value| v8::Local::::try_from(value).ok()) - .is_some_and(|entry| entry.strict_equals(target.into())) + additional: Vec::new(), }) } @@ -198,6 +224,9 @@ pub(in crate::context_bootstrap) fn dispatch_history_entry_currententrychange<'s scope: &mut v8::PinScope<'s, '_>, applied: &AppliedHistoryEntry<'s>, ) { + for other in &applied.additional { + dispatch_history_entry_currententrychange(scope, other); + } if !navigation_document_has_opaque_origin(scope, applied.owner) && let Some(navigation) = window_navigation_for_holder(scope, applied.owner) { @@ -215,6 +244,9 @@ pub(in crate::context_bootstrap) fn dispatch_history_entry_post_commit_events<'s applied: &AppliedHistoryEntry<'s>, dispatch_popstate: bool, ) { + for other in &applied.additional { + dispatch_history_entry_post_commit_events(scope, other, dispatch_popstate); + } if runtime_window_is_global(scope, applied.owner) { let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return; @@ -222,14 +254,7 @@ pub(in crate::context_bootstrap) fn dispatch_history_entry_post_commit_events<'s let host = unsafe { &mut *host_ptr }; host.set_document_url(applied.parsed_url.clone()); if dispatch_popstate && applied.previous_history_index != applied.history_index { - host.record_same_document_navigation( - &applied.parsed_url, - "fragment", - SameDocumentHistoryUpdate::Traverse { - delta: i64::from(applied.history_index) - - i64::from(applied.previous_history_index), - }, - ); + host.record_same_document_navigation(&applied.parsed_url, "fragment"); } if dispatch_popstate { dispatch_popstate_event(scope, host_ptr, None, applied.state); diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/length.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/length.rs deleted file mode 100644 index 707adab0de..0000000000 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/length.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::super::navigation_entry::{history_length_number, set_history_length}; -use super::super::navigation_window::{ - runtime_top_window_owner, runtime_window_uses_top_level_history_model, - window_history_for_holder, -}; - -pub(crate) fn increment_top_level_history_length_for_runtime_owner<'s>( - scope: &mut v8::PinScope<'s, '_>, - owner: v8::Local<'s, v8::Object>, -) { - if !runtime_window_uses_top_level_history_model(scope, owner) - && let Some(child_history) = window_history_for_holder(scope, owner) - && let Some(child_length) = history_length_number(scope, child_history) - { - set_top_level_history_length_at_least_for_runtime_owner( - scope, - owner, - child_length.max(0.0), - ); - return; - } - let top_window = runtime_top_window_owner(scope, owner); - let Some(history) = window_history_for_holder(scope, top_window) else { - return; - }; - let current_length = history_length_number(scope, history) - .unwrap_or(0.0) - .max(0.0); - set_history_length(scope, history, current_length + 1.0); -} - -pub(crate) fn set_top_level_history_length_at_least_for_runtime_owner<'s>( - scope: &mut v8::PinScope<'s, '_>, - owner: v8::Local<'s, v8::Object>, - length: f64, -) { - let top_window = runtime_top_window_owner(scope, owner); - let Some(history) = window_history_for_holder(scope, top_window) else { - return; - }; - let current_length = history_length_number(scope, history) - .unwrap_or(0.0) - .max(0.0); - set_history_length(scope, history, current_length.max(length.max(0.0))); -} diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs index 2e28acb55a..a086f65ecc 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/traversal.rs @@ -30,6 +30,7 @@ const TRAVERSAL_PRECOMMIT_NAVIGATION_SLOT: &str = "__lmTraversalPrecommitNavigat const TRAVERSAL_PRECOMMIT_HISTORY_SLOT: &str = "__lmTraversalPrecommitHistory"; const TRAVERSAL_PRECOMMIT_EVENT_SLOT: &str = "__lmTraversalPrecommitEvent"; const TRAVERSAL_PRECOMMIT_SIGNAL_SLOT: &str = "__lmTraversalPrecommitSignal"; +const TRAVERSAL_PRECOMMIT_JOINT_STEP_SLOT: &str = "__lmTraversalPrecommitJointStep"; const TRAVERSAL_PRECOMMIT_TARGET_INDEX_SLOT: &str = "__lmTraversalPrecommitTargetIndex"; const TRAVERSAL_PRECOMMIT_PROMISE_SLOT: &str = "__lmTraversalPrecommitPromise"; const TRAVERSAL_PRECOMMIT_COMMITTED_RESOLVERS_SLOT: &str = @@ -63,6 +64,9 @@ struct TraversalPrecommitDataDeclaration<'scope> { #[webapi(slot = TRAVERSAL_PRECOMMIT_SIGNAL_SLOT)] signal: Option>, + #[webapi(slot = TRAVERSAL_PRECOMMIT_JOINT_STEP_SLOT)] + joint_step: Option>, + #[webapi(slot = TRAVERSAL_PRECOMMIT_TARGET_INDEX_SLOT)] target_index: u32, @@ -230,8 +234,24 @@ pub(in crate::context_bootstrap) fn route_history_traversal_task( pub(in crate::context_bootstrap) fn apply_pending_history_traversal( scope: &mut v8::PinScope<'_, '_>, host: &mut JsContextHost, - traversal: PendingHistoryTraversal, + mut traversal: PendingHistoryTraversal, ) { + // Earlier queued work can already have installed this frame's entry while + // other participants still need to move to the requested joint step. + if traversal.results.is_empty() + && let Some(step) = traversal.joint_step + && let Some(owner) = history_traversal_target_window(scope, host, traversal.target) + && let Some(target) = super::super::session_history::targets_at(scope, owner, step) + .and_then(|targets| targets.into_iter().next()) + && let Some(exact) = window_task_target_for_runtime_owner(scope, host, target.owner) + { + traversal.target = exact; + traversal.target_index = target.target_index; + traversal.target_key = history_entries(scope, target.history) + .and_then(|entries| entries.get_index(scope, target.target_index)) + .and_then(|entry| v8::Local::::try_from(entry).ok()) + .and_then(|entry| navigation_entry_key_value(scope, entry)); + } let results = traversal.results; let history = history_traversal_target_window(scope, host, traversal.target) .and_then(|window| window_history_for_holder(scope, window)); @@ -317,6 +337,7 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal( navigation, history, traversal.target_index, + traversal.joint_step, outcome, &results, ); @@ -328,8 +349,12 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal( } return; } - let Some(applied) = apply_history_entry_commit(scope, history, traversal.target_index) - else { + let Some(applied) = apply_history_entry_commit( + scope, + history, + traversal.target_index, + traversal.joint_step, + ) else { let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError"); finish_navigation_error_events(scope, navigation, error, ""); @@ -400,6 +425,7 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal( scope, history, traversal.target_index, + traversal.joint_step, true, pending_results, ); @@ -419,6 +445,21 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal( return; } let owner = runtime_window_owner(scope, history); + if let Some(step) = traversal.joint_step { + let Some(delta) = super::super::session_history::commit_traversal(scope, owner, step) + else { + let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError"); + reject_pending_navigation_results(scope, &results, error); + return; + }; + if delta != 0 { + super::super::session_history::publish( + scope, + owner, + moli_page_types::SessionHistoryUpdateKind::Traverse { delta }, + ); + } + } let resolved_entry = navigation_current_entry(scope, owner) .map(v8::Local::::from) .unwrap_or_else(|| v8::undefined(scope).into()); @@ -468,6 +509,7 @@ fn queue_pending_precommit_history_traversal<'s>( navigation: v8::Local<'s, v8::Object>, history: v8::Local<'s, v8::Object>, target_index: u32, + joint_step: Option, outcome: NavigationDispatchOutcome<'s>, results: &[crate::native_bridge::PendingNavigationResult], ) -> bool { @@ -496,6 +538,7 @@ fn queue_pending_precommit_history_traversal<'s>( history, event, signal: outcome.signal, + joint_step: joint_step.map(|step| v8::BigInt::new_from_u64(scope, step.raw()).into()), target_index, promise, committed_resolvers, @@ -671,7 +714,12 @@ fn traversal_precommit_fulfilled_callback<'s>( if let Ok(data) = v8::Local::::try_from(args.data()) { set_traversal_precommit_inactive(scope, data); } - let Some(applied) = apply_history_entry_commit(scope, history, target_index) else { + let joint_step = v8::Local::::try_from(args.data()) + .ok() + .and_then(|data| get_private_value(scope, data, TRAVERSAL_PRECOMMIT_JOINT_STEP_SLOT)) + .and_then(|value| v8::Local::::try_from(value).ok()) + .map(|value| moli_page_types::SessionHistoryStepId::from_raw(value.u64_value().0)); + let Some(applied) = apply_history_entry_commit(scope, history, target_index, joint_step) else { let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError"); finish_navigation_error_events(scope, navigation, error, ""); reject_resolver_array(scope, committed_resolvers, error, false); diff --git a/moli-renderer-v8/src/context_bootstrap/location_history_storage.rs b/moli-renderer-v8/src/context_bootstrap/location_history_storage.rs index ea93610f4d..6fd828b7df 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_history_storage.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_history_storage.rs @@ -1,6 +1,5 @@ pub(super) const HISTORY_ENTRIES_SLOT: &str = "__lmHistoryEntries"; pub(super) const HISTORY_INDEX_SLOT: &str = "__lmHistoryIndex"; -pub(super) const HISTORY_LENGTH_SLOT: &str = "__lmHistoryLength"; pub(super) const HISTORY_STATE_SLOT: &str = "__lmHistoryState"; pub(super) const HISTORY_SCROLL_RESTORATION_SLOT: &str = "__lmHistoryScrollRestoration"; pub(super) const HISTORY_ENTRY_STATE_SNAPSHOT_SLOT: &str = "__lmHistoryEntryStateSnapshot"; diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index f999070e0d..d53b2d1f17 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -39,9 +39,7 @@ use super::*; use crate::native_bridge::NavigationHistoryEntrySeed; use crate::util::context_host_ptr_from_window_object; use crate::webidl; -use moli_page_types::{ - NavigationHistoryMutation, SameDocumentHistoryUpdate, cross_document_navigation_seed, -}; +use moli_page_types::{NavigationHistoryMutation, cross_document_navigation_seed}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum LocationNavigationKind { @@ -424,13 +422,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { let host = unsafe { &mut *host_ptr }; host.set_document_url(resolved.clone()); - let history_update = match effective_kind { - LocationNavigationKind::Assign => SameDocumentHistoryUpdate::Push, - LocationNavigationKind::Replace | LocationNavigationKind::Reload => { - SameDocumentHistoryUpdate::Replace - } - }; - host.record_same_document_navigation(&resolved, "fragment", history_update); + host.record_same_document_navigation(&resolved, "fragment"); } } else { sync_local_document_front_from_window(scope, owner); @@ -549,13 +541,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( { let host = unsafe { &mut *host_ptr }; host.set_document_url(url.clone()); - let history_update = match effective_kind { - LocationNavigationKind::Assign => SameDocumentHistoryUpdate::Push, - LocationNavigationKind::Replace | LocationNavigationKind::Reload => { - SameDocumentHistoryUpdate::Replace - } - }; - host.record_same_document_navigation(&url, "fragment", history_update); + host.record_same_document_navigation(&url, "fragment"); } settle_location_intercepted_same_document_navigation( scope, @@ -608,9 +594,6 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( } if let Some(host_ptr) = host_ptr { let host = unsafe { &mut *host_ptr }; - if matches!(kind, LocationNavigationKind::Assign) && !is_javascript_url { - host.mark_child_browsing_context_top_level_history_increment(handle); - } if matches!(kind, LocationNavigationKind::Reload) { host.queue_child_browsing_context_reload_from_existing_seed( handle, @@ -861,13 +844,15 @@ fn history_entry_seed_for_cross_document_location<'s>( LocationNavigationKind::Replace => NavigationHistoryMutation::Replace, LocationNavigationKind::Reload => return None, }; - Some(cross_document_navigation_seed( + let mut seed = cross_document_navigation_seed( serialize_history_entries(scope, history), current_index, current_navigation_index, resolved, mutation, - )) + ); + super::session_history::capture_for_navigation(scope, owner, &mut seed); + Some(seed) } fn context_host_ptr_for_navigation_owner( diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs index b7ab37a8d7..6ed86d9c83 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs @@ -7,7 +7,6 @@ use super::navigation_activation::{ install_navigation_activation_runtime_state, set_navigation_current_entry, }; use super::navigation_entry::{set_history_entries, set_history_index}; -use super::navigation_projection::set_history_length_from_visible_entries; use super::navigation_result::clear_active_cross_document_navigation_if_matches; use super::navigation_seed::{ build_current_navigation_entry_from_seed, build_history_entries_array_from_seed, @@ -62,6 +61,9 @@ pub(crate) fn install_window_location_history_navigation_runtime_state<'s>( let navigation = build_navigation_runtime_state(scope, window, &initial_seed)?; set_runtime_window_owner(scope, navigation, window); set_private_value(scope, window, WINDOW_NAVIGATION_SLOT, navigation.into()); + if crate::native_bridge::lightweight_popup_id_from_window(scope, window).is_some() { + super::session_history::initialize(scope, window, &initial_seed); + } if !window.strict_equals(scope.get_current_context().global(scope).into()) { sync_window_location_history_navigation_runtime_surface(scope, window); } @@ -108,7 +110,7 @@ pub(crate) fn reset_window_location_history_navigation_runtime_state<'s>( }); set_history_entries(scope, history, entries); set_history_index(scope, history, initial_seed.current_index); - set_history_length_from_visible_entries(scope, history, entries); + super::session_history::initialize(scope, window, &initial_seed); set_private_value(scope, window, WINDOW_HISTORY_SLOT, history.into()); let navigation = match window_runtime_object(scope, window, WINDOW_NAVIGATION_SLOT) { diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs b/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs index fa0787e0e1..0bdfabfebf 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs @@ -66,13 +66,14 @@ pub(super) fn handle_navigation_navigate_cross_document<'s>( }; if runtime_window_is_global(scope, owner) { - let entry_seed = cross_document_navigation_seed( + let mut entry_seed = cross_document_navigation_seed( entries, current_index, current_navigation_index, next_url, mutation, ); + super::session_history::capture_for_navigation(scope, owner, &mut entry_seed); dispatch_beforeunload_for_runtime_owner(scope, owner); dispatch_pagehide_for_runtime_owner(scope, owner); dispatch_unload_for_runtime_owner(scope, owner); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs index f97711eca3..16993d01e3 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs @@ -1,8 +1,8 @@ use super::location_history_storage::{ HISTORY_ENTRIES_SLOT, HISTORY_ENTRY_STATE_SNAPSHOT_SLOT, HISTORY_INDEX_SLOT, - HISTORY_LENGTH_SLOT, HISTORY_SCROLL_RESTORATION_SLOT, HISTORY_STATE_SLOT, - NAVIGATION_CURRENT_ENTRY_SLOT, NAVIGATION_ENTRY_DOCUMENT_ID_SLOT, - NAVIGATION_ENTRY_EVENT_LISTENERS_SLOT, NAVIGATION_ENTRY_STATE_SNAPSHOT_SLOT, + HISTORY_SCROLL_RESTORATION_SLOT, HISTORY_STATE_SLOT, NAVIGATION_CURRENT_ENTRY_SLOT, + NAVIGATION_ENTRY_DOCUMENT_ID_SLOT, NAVIGATION_ENTRY_EVENT_LISTENERS_SLOT, + NAVIGATION_ENTRY_STATE_SNAPSHOT_SLOT, }; use super::location_runtime::urls_refer_to_same_document; use super::navigation_activation::set_navigation_current_entry; @@ -19,7 +19,6 @@ use moli_page_types::{NavigationHistoryEntryId, NavigationHistoryEntryKey}; use moli_webapi_declare::WebApiObject; const NAVIGATION_ENTRY_INITIAL_INDEX_SLOT: &str = "__lmNavigationEntryInitialIndex"; -const NAVIGATION_ENTRY_JOINT_TOP_INDEX_SLOT: &str = "__lmNavigationEntryJointTopIndex"; const NAVIGATION_ENTRY_URL_SLOT: &str = "__lmNavigationEntryUrl"; const NAVIGATION_ENTRY_REFERRER_POLICY_SLOT: &str = "__lmNavigationEntryReferrerPolicy"; const NAVIGATION_ENTRY_ID_SLOT: &str = "__lmNavigationEntryId"; @@ -54,9 +53,6 @@ struct NavigationHistoryEntryObjectDeclaration<'scope, 'value> { #[webapi(slot = NAVIGATION_ENTRY_INITIAL_INDEX_SLOT)] initial_index: f64, - #[webapi(slot = NAVIGATION_ENTRY_JOINT_TOP_INDEX_SLOT, init = "undefined")] - joint_top_index: (), - #[webapi(slot = NAVIGATION_ENTRY_SCROLL_X_SLOT, init = "undefined")] scroll_x: (), @@ -263,29 +259,6 @@ pub(super) fn set_navigation_entry_initial_index<'s>( ); } -pub(super) fn navigation_entry_joint_top_index<'s>( - scope: &mut v8::PinScope<'s, '_>, - entry: v8::Local<'s, v8::Object>, -) -> Option { - navigation_entry_slot_value(scope, entry, NAVIGATION_ENTRY_JOINT_TOP_INDEX_SLOT) - .and_then(|value| value.integer_value(scope)) - .filter(|value| *value >= 0) - .map(|value| value as u32) -} - -pub(super) fn set_navigation_entry_joint_top_index<'s>( - scope: &mut v8::PinScope<'s, '_>, - entry: v8::Local<'s, v8::Object>, - index: u32, -) { - set_navigation_entry_number_slot( - scope, - entry, - NAVIGATION_ENTRY_JOINT_TOP_INDEX_SLOT, - index as f64, - ); -} - pub(super) fn navigation_entry_referrer_policy_value<'s>( scope: &mut v8::PinScope<'s, '_>, entry: v8::Local<'s, v8::Object>, @@ -624,22 +597,6 @@ fn set_history_slot_value<'s>( set_private_value(scope, history, slot, value); } -pub(super) fn history_length_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - history: v8::Local<'s, v8::Object>, -) -> Option> { - history_slot_value(scope, history, HISTORY_LENGTH_SLOT).filter(|value| !value.is_undefined()) -} - -pub(super) fn history_length_number<'s>( - scope: &mut v8::PinScope<'s, '_>, - history: v8::Local<'s, v8::Object>, -) -> Option { - history_length_value(scope, history) - .and_then(|value| value.number_value(scope)) - .filter(|value| value.is_finite()) -} - pub(super) fn history_scroll_restoration_value<'s>( scope: &mut v8::PinScope<'s, '_>, history: v8::Local<'s, v8::Object>, @@ -692,19 +649,6 @@ pub(super) fn set_history_state<'s>( set_history_slot_value(scope, history, HISTORY_STATE_SLOT, state); } -pub(super) fn set_history_length<'s>( - scope: &mut v8::PinScope<'s, '_>, - history: v8::Local<'s, v8::Object>, - length: f64, -) { - set_history_slot_value( - scope, - history, - HISTORY_LENGTH_SLOT, - v8::Number::new(scope, length).into(), - ); -} - pub(super) fn stringify_history_state<'s>( scope: &mut v8::PinScope<'s, '_>, state: v8::Local<'s, v8::Value>, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_history_pruning.rs b/moli-renderer-v8/src/context_bootstrap/navigation_history_pruning.rs index 604ac45614..1ce7808d45 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_history_pruning.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_history_pruning.rs @@ -1,6 +1,6 @@ use super::navigation_entry::{ history_entries, history_index, navigation_current_entry, navigation_entry_key_value, - set_history_entries, set_history_index, set_history_length, set_navigation_entry_initial_index, + set_history_entries, set_history_index, set_navigation_entry_initial_index, }; use super::navigation_events::dispatch_navigation_entry_dispose; use super::navigation_window::window_history_for_holder; @@ -86,7 +86,7 @@ pub(crate) fn apply_navigation_history_prune_plan( } set_history_entries(scope, history, retained_entries_array); set_history_index(scope, history, current_index as u32); - + super::navigation_serialize::sync_child_navigation_entry_seed_from_owner(scope, owner); for removed_key in &plan.removed_entry_keys { if let Some((_, entry)) = removed_entries.iter().find(|(key, _)| key == removed_key) { dispatch_navigation_entry_dispose(scope, *entry); @@ -97,11 +97,7 @@ pub(crate) fn apply_navigation_history_prune_plan( pub(crate) fn finalize_navigation_history_prune(scope: &mut v8::PinScope<'_, '_>) -> bool { let owner = scope.get_current_context().global(scope); - let Some(history) = window_history_for_holder(scope, owner) else { - return false; - }; - set_history_length(scope, history, 1.0); - true + window_history_for_holder(scope, owner).is_some() } fn navigation_history_prune_state<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs b/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs index de4c62a856..fbbd47d7c8 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_mutation.rs @@ -9,8 +9,7 @@ use super::navigation_entry::{ navigation_current_entry, navigation_current_entry_index, navigation_entry_key_value, navigation_entry_url_value, new_navigation_entry_id, new_navigation_entry_key, set_history_entries, set_history_index, set_history_state, set_navigation_entry_document_id, - set_navigation_entry_joint_top_index, stringify_history_state, - sync_navigation_current_entry_from_history_entry, + stringify_history_state, sync_navigation_current_entry_from_history_entry, }; use super::navigation_entry_state::{ clone_history_entry_state, clone_navigation_entry_state, set_navigation_entry_state, @@ -19,18 +18,14 @@ use super::navigation_events::{ dispatch_navigation_currententrychange, dispatch_navigation_entry_dispose, refresh_navigation_destination_indexes, }; -use super::navigation_projection::{ - build_visible_navigation_entries_array, set_history_length_after_push, - set_history_length_from_visible_entries, -}; use super::navigation_serialize::{ serialize_history_entries, serialize_navigation_entry_object, sync_child_navigation_entry_seed_from_owner, sync_child_pending_navigation_entry_seed_from_owner, }; use super::navigation_window::{ - runtime_top_window_owner, runtime_window_is_global, window_history_for_holder, - window_location_for_holder, window_navigation_for_holder, + runtime_window_is_global, window_history_for_holder, window_location_for_holder, + window_navigation_for_holder, }; use super::*; use crate::native_bridge::NavigationActivationSeed; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_mutation/local.rs b/moli-renderer-v8/src/context_bootstrap/navigation_mutation/local.rs index 97c3960211..17e56eb85a 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_mutation/local.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_mutation/local.rs @@ -44,7 +44,18 @@ pub(crate) fn apply_local_window_location_navigation<'s>( let _ = next_entries.set_index(scope, next_index, next_entry.into()); set_history_entries(scope, history, next_entries); set_history_index(scope, history, next_index); - set_history_length_after_push(scope, history, entries, next_entries); + if super::super::navigation_window::child_browsing_context_handle_for_runtime_owner( + scope, owner, + ) + .is_none() + { + super::super::session_history::commit( + scope, + owner, + next_entry, + moli_page_types::SessionHistoryCommit::Push, + ); + } set_history_state(scope, history, state); set_navigation_current_entry(scope, navigation, next_entry); dispatch_navigation_currententrychange(scope, navigation, previous_entry, Some("push")); @@ -72,6 +83,18 @@ pub(crate) fn apply_local_window_location_navigation<'s>( set_history_entries(scope, history, entries); set_history_state(scope, history, state); set_navigation_current_entry(scope, navigation, entry); + if super::super::navigation_window::child_browsing_context_handle_for_runtime_owner( + scope, owner, + ) + .is_none() + { + super::super::session_history::commit( + scope, + owner, + entry, + moli_page_types::SessionHistoryCommit::Replace, + ); + } dispatch_navigation_currententrychange( scope, navigation, @@ -108,7 +131,16 @@ pub(crate) fn apply_local_window_location_navigation<'s>( } } } - sync_child_pending_navigation_entry_seed_from_owner(scope, owner); + sync_child_pending_navigation_entry_seed_from_owner( + scope, + owner, + match kind { + LocationNavigationKind::Assign => moli_page_types::SessionHistoryCommit::Push, + LocationNavigationKind::Replace | LocationNavigationKind::Reload => { + moli_page_types::SessionHistoryCommit::Replace + } + }, + ); } fn replacement_keeps_navigation_key<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_mutation/same_document.rs b/moli-renderer-v8/src/context_bootstrap/navigation_mutation/same_document.rs index 71d60d4772..52924b8e12 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_mutation/same_document.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_mutation/same_document.rs @@ -40,7 +40,6 @@ pub(in crate::context_bootstrap) fn update_navigation_current_entry_for_same_doc let next_index = current_index + 1; let next_navigation_index = current_navigation_index + 1; pruned_entries = pruned_history_entries(scope, entries, next_index); - set_child_joint_top_index_for_entry(scope, owner, Some(current_entry)); let next_entry = create_navigation_entry( scope, href, @@ -52,7 +51,6 @@ pub(in crate::context_bootstrap) fn update_navigation_current_entry_for_same_doc &new_navigation_entry_key(), ); copy_navigation_entry_document_id(scope, current_entry, next_entry); - set_child_joint_top_index_for_entry(scope, owner, Some(next_entry)); let next_entries = v8::Array::new(scope, (next_index + 1) as i32); for index in 0..next_index { if let Some(entry) = entries.get_index(scope, index) { @@ -62,7 +60,12 @@ pub(in crate::context_bootstrap) fn update_navigation_current_entry_for_same_doc let _ = next_entries.set_index(scope, next_index, next_entry.into()); set_history_entries(scope, history, next_entries); set_history_index(scope, history, next_index); - set_history_length_after_push(scope, history, entries, next_entries); + super::super::session_history::commit( + scope, + owner, + next_entry, + moli_page_types::SessionHistoryCommit::Push, + ); set_history_state(scope, history, history_state); set_navigation_current_entry(scope, navigation, next_entry); } @@ -84,6 +87,12 @@ pub(in crate::context_bootstrap) fn update_navigation_current_entry_for_same_doc set_history_entries(scope, history, entries); set_history_state(scope, history, history_state); set_navigation_current_entry(scope, navigation, entry); + super::super::session_history::commit( + scope, + owner, + entry, + moli_page_types::SessionHistoryCommit::Replace, + ); } LocationNavigationKind::Reload => return, } @@ -92,6 +101,8 @@ pub(in crate::context_bootstrap) fn update_navigation_current_entry_for_same_doc LocationNavigationKind::Replace => Some("replace"), LocationNavigationKind::Reload => None, }; + let joint_pruned = super::super::session_history::prune_views(scope, owner); + dispatch_pruned_history_entry_disposes(scope, joint_pruned); dispatch_navigation_currententrychange( scope, navigation, @@ -126,7 +137,6 @@ pub(in crate::context_bootstrap) fn apply_navigation_navigate_same_document<'s>( let next_index = current_index + 1; let next_navigation_index = current_navigation_index + 1; let pruned_entries = pruned_history_entries(scope, entries, next_index); - set_child_joint_top_index_for_entry(scope, owner, previous_entry); let next_entries = v8::Array::new(scope, (current_index + 2) as i32); for index in 0..=current_index { if let Some(entry) = entries.get_index(scope, index) { @@ -147,22 +157,27 @@ pub(in crate::context_bootstrap) fn apply_navigation_navigate_same_document<'s>( copy_navigation_entry_document_id(scope, previous_entry, next_entry); } bind_navigation_entry_runtime_owner(scope, next_entry, owner); - set_child_joint_top_index_for_entry(scope, owner, Some(next_entry)); if let Some(state) = navigation_state { set_navigation_entry_state(scope, next_entry, state); } let _ = next_entries.set_index(scope, next_index, next_entry.into()); set_history_entries(scope, history, next_entries); set_history_index(scope, history, next_index); - set_history_length_after_push(scope, history, entries, next_entries); + super::super::session_history::commit( + scope, + owner, + next_entry, + moli_page_types::SessionHistoryCommit::Push, + ); set_history_state(scope, history, history_state); set_navigation_current_entry(scope, navigation, next_entry); if let Some(location) = window_location_for_holder(scope, owner) { sync_location_object(scope, location, href); } + let joint_pruned = super::super::session_history::prune_views(scope, owner); dispatch_navigation_currententrychange(scope, navigation, previous_entry, Some("push")); + dispatch_pruned_history_entry_disposes(scope, joint_pruned); dispatch_pruned_history_entry_disposes(scope, pruned_entries); - prune_top_forward_entries_after_child_same_document_push(scope, owner); } LocationNavigationKind::Replace => { let key = previous_entry @@ -189,6 +204,12 @@ pub(in crate::context_bootstrap) fn apply_navigation_navigate_same_document<'s>( set_history_entries(scope, history, entries); set_history_state(scope, history, history_state); set_navigation_current_entry(scope, navigation, entry); + super::super::session_history::commit( + scope, + owner, + entry, + moli_page_types::SessionHistoryCommit::Replace, + ); if let Some(location) = window_location_for_holder(scope, owner) { sync_location_object(scope, location, href); } @@ -227,65 +248,3 @@ fn dispatch_pruned_history_entry_disposes<'s>( dispatch_navigation_entry_dispose(scope, entry); } } - -fn prune_top_forward_entries_after_child_same_document_push<'s>( - scope: &mut v8::PinScope<'s, '_>, - owner: v8::Local<'s, v8::Object>, -) { - if runtime_window_is_global(scope, owner) { - return; - } - let top_owner = runtime_top_window_owner(scope, owner); - if top_owner.strict_equals(owner.into()) { - return; - } - let Some(top_history) = window_history_for_holder(scope, top_owner) else { - return; - }; - let Some(top_entries) = history_entries(scope, top_history) else { - return; - }; - let top_current_index = history_index(scope, top_history); - let first_pruned_index = top_current_index + 1; - if first_pruned_index >= top_entries.length() { - return; - } - - let first_visible_pruned_index = navigation_current_entry_index(scope, top_owner) - .map(|index| index + 1) - .unwrap_or(first_pruned_index); - let top_current_entry = navigation_current_entry(scope, top_owner); - let visible_entries = - build_visible_navigation_entries_array(scope, top_entries, top_current_entry); - let pruned_entries = pruned_history_entries(scope, visible_entries, first_visible_pruned_index); - let next_entries = v8::Array::new(scope, first_pruned_index as i32); - for index in 0..first_pruned_index { - if let Some(entry) = top_entries.get_index(scope, index) { - let _ = next_entries.set_index(scope, index, entry); - } - } - set_history_entries(scope, top_history, next_entries); - set_history_length_from_visible_entries(scope, top_history, next_entries); - dispatch_pruned_history_entry_disposes(scope, pruned_entries); -} - -fn set_child_joint_top_index_for_entry<'s>( - scope: &mut v8::PinScope<'s, '_>, - owner: v8::Local<'s, v8::Object>, - entry: Option>, -) { - if runtime_window_is_global(scope, owner) { - return; - } - let Some(entry) = entry else { - return; - }; - let top_owner = runtime_top_window_owner(scope, owner); - if top_owner.strict_equals(owner.into()) { - return; - } - let Some(top_index) = navigation_current_entry_index(scope, top_owner) else { - return; - }; - set_navigation_entry_joint_top_index(scope, entry, top_index); -} diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs index cf856996f8..9a3f816a29 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs @@ -1,11 +1,9 @@ use super::navigation_entry::{ - history_length_number, navigation_entry_initial_index, navigation_entry_key_value, - navigation_entry_url_value, set_history_length, + navigation_entry_initial_index, navigation_entry_key_value, navigation_entry_url_value, }; use super::navigation_window::{ - child_browsing_context_handle_for_runtime_owner, runtime_top_window_owner, - runtime_window_is_global, runtime_window_owner, runtime_window_uses_top_level_history_model, - window_history_for_holder, + child_browsing_context_handle_for_runtime_owner, runtime_window_is_global, + runtime_window_owner, runtime_window_uses_top_level_history_model, }; use crate::util::{context_host_ptr_from_global_bridge, serialize_v8_iter_array}; @@ -219,80 +217,3 @@ fn entry_origin_url<'s>( fn child_navigation_entry_url_inherits_origin(url: &url::Url) -> bool { url.scheme() == "about" && matches!(url.as_str(), "about:blank" | "about:srcdoc") } - -pub(super) fn set_history_length_from_visible_entries<'s>( - scope: &mut v8::PinScope<'s, '_>, - history: v8::Local<'s, v8::Object>, - entries: v8::Local<'s, v8::Array>, -) { - let length = history_length_floor_from_visible_entries(scope, history, entries); - set_history_length(scope, history, length); - set_top_history_length_at_least(scope, history, length); -} - -pub(super) fn set_history_length_after_push<'s>( - scope: &mut v8::PinScope<'s, '_>, - history: v8::Local<'s, v8::Object>, - previous_entries: v8::Local<'s, v8::Array>, - entries: v8::Local<'s, v8::Array>, -) { - let length = history_length_floor_from_visible_entries(scope, history, entries); - let current_length = history_length_number(scope, history) - .unwrap_or(0.0) - .max(0.0); - // Navigation entries can omit earlier Documents and other frames' steps. - // Add newly appended entries to the existing joint length; replacing a - // forward entry after traversal adds no step. - let previous_length = - history_length_floor_from_visible_entries(scope, history, previous_entries); - let added_entries = entries.length().saturating_sub(previous_entries.length()); - let length = (current_length.max(previous_length) + f64::from(added_entries)).max(length); - set_history_length(scope, history, length); - set_top_history_length_at_least(scope, history, length); -} - -fn history_length_floor_from_visible_entries<'s>( - scope: &mut v8::PinScope<'s, '_>, - history: v8::Local<'s, v8::Object>, - entries: v8::Local<'s, v8::Array>, -) -> f64 { - let visible_length = all_entries(scope, entries) - .into_iter() - .filter_map(|entry| navigation_entry_initial_index(scope, entry)) - .max() - .map(|index| index + 1) - .unwrap_or_else(|| entries.length()) as f64; - let owner = runtime_window_owner(scope, history); - if runtime_window_uses_top_level_history_model(scope, owner) { - return visible_length; - } - - // A child sees the joint session-history length. Creating its initial - // entry does not add a new joint-history step, so start from the current - // top-level length and only grow it when the child gains a visible entry. - let top_owner = runtime_top_window_owner(scope, owner); - let top_length = window_history_for_holder(scope, top_owner) - .and_then(|history| history_length_number(scope, history)) - .unwrap_or(visible_length) - .max(0.0); - visible_length.max(top_length) -} - -fn set_top_history_length_at_least<'s>( - scope: &mut v8::PinScope<'s, '_>, - history: v8::Local<'s, v8::Object>, - length: f64, -) { - let owner = runtime_window_owner(scope, history); - if runtime_window_uses_top_level_history_model(scope, owner) { - return; - } - let top_window = runtime_top_window_owner(scope, owner); - let Some(top_history) = window_history_for_holder(scope, top_window) else { - return; - }; - let current_length = history_length_number(scope, top_history) - .unwrap_or(0.0) - .max(0.0); - set_history_length(scope, top_history, current_length.max(length)); -} diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs b/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs index 7ea1f1ac76..8088243f4f 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_restore.rs @@ -3,11 +3,10 @@ use super::navigation_activation::{ set_navigation_current_entry, }; use super::navigation_entry::{ - create_navigation_entry, set_history_entries, set_history_index, set_history_length, - set_history_state, set_navigation_entry_document_id, + create_navigation_entry, set_history_entries, set_history_index, set_history_state, + set_navigation_entry_document_id, }; use super::navigation_entry_state::clone_history_entry_state; -use super::navigation_projection::set_history_length_from_visible_entries; use super::navigation_result::clear_active_cross_document_navigation_if_matches; use super::navigation_window::{window_history_for_holder, window_navigation_for_holder}; use crate::native_bridge::NavigationHistoryEntrySeed; @@ -15,13 +14,6 @@ use moli_page_types::{ NavigationHistoryDocumentId, NavigationHistoryEntryId, NavigationHistoryEntryKey, }; -pub(crate) fn install_session_history_length(scope: &mut v8::PinScope<'_, '_>, length: usize) { - let global = scope.get_current_context().global(scope); - if let Some(history) = window_history_for_holder(scope, global) { - set_history_length(scope, history, length as f64); - } -} - pub(crate) fn install_navigation_bootstrap_entry( scope: &mut v8::PinScope<'_, '_>, entry_seed: &NavigationHistoryEntrySeed, @@ -34,6 +26,17 @@ pub(crate) fn install_navigation_bootstrap_entry_for_holder<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, entry_seed: &NavigationHistoryEntrySeed, +) { + install_navigation_entry_view_for_holder(scope, owner, entry_seed, true); +} + +/// A pending child navigation may expose its entry view before replacing the +/// live Document. Only a committed seed advances the shared traversable. +pub(crate) fn install_navigation_entry_view_for_holder<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + entry_seed: &NavigationHistoryEntrySeed, + committed: bool, ) { let Some(history) = window_history_for_holder(scope, owner) else { return; @@ -86,7 +89,9 @@ pub(crate) fn install_navigation_bootstrap_entry_for_holder<'s>( }); set_history_entries(scope, history, entries); set_history_index(scope, history, entry_seed.current_index); - set_history_length_from_visible_entries(scope, history, entries); + if committed { + super::session_history::restore(scope, owner, entry_seed); + } set_history_state( scope, history, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs index 97cb9edd6f..cb38c9ef3a 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs @@ -103,7 +103,9 @@ pub(super) fn history_entry_seed_for_reload<'s>( let history = window_history_for_holder(scope, owner)?; let entries = serialize_history_entries(scope, history); let current_index = history_index(scope, history); - reload_navigation_seed(entries, current_index) + let mut seed = reload_navigation_seed(entries, current_index)?; + super::session_history::capture_for_navigation(scope, owner, &mut seed); + Some(seed) } pub(super) fn history_entry_seed_for_traversal<'s>( @@ -120,11 +122,12 @@ pub(super) fn history_entry_seed_for_traversal<'s>( current_index, &mut entries, ); - let candidate = traversal_navigation_seed_candidate(entries, current_index, target_index)?; + let mut candidate = traversal_navigation_seed_candidate(entries, current_index, target_index)?; if is_same_document_fragment_navigation(Some(&candidate.current_url), &candidate.target_url) { return None; } + super::session_history::capture_for_navigation(scope, owner, &mut candidate.seed); Some((candidate.target_url, candidate.seed)) } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs b/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs index bffc8c533c..d0a41420d3 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_serialize.rs @@ -27,6 +27,7 @@ fn capture_navigation_entry_seed_for_holder<'s>( let history = window_history_for_holder(scope, owner)?; let navigation = window_navigation_for_holder(scope, owner)?; Some(NavigationHistoryEntrySeed { + session_history: Default::default(), entries: serialize_history_entries(scope, history), current_index: history_index(scope, history), activation: serialize_navigation_activation_seed(scope, navigation), @@ -37,20 +38,27 @@ pub(super) fn sync_child_navigation_entry_seed_from_owner<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) { - sync_child_navigation_entry_seed_from_owner_with_document_url(scope, owner, true); + sync_child_navigation_entry_seed_from_owner_with_document_url( + scope, + owner, + true, + moli_page_types::SessionHistoryCommit::Attach, + ); } pub(super) fn sync_child_pending_navigation_entry_seed_from_owner<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, + commit: moli_page_types::SessionHistoryCommit, ) { - sync_child_navigation_entry_seed_from_owner_with_document_url(scope, owner, false); + sync_child_navigation_entry_seed_from_owner_with_document_url(scope, owner, false, commit); } fn sync_child_navigation_entry_seed_from_owner_with_document_url<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, sync_document_url: bool, + commit: moli_page_types::SessionHistoryCommit, ) { if runtime_window_is_global(scope, owner) { return; @@ -58,9 +66,10 @@ fn sync_child_navigation_entry_seed_from_owner_with_document_url<'s>( let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) else { return; }; - let Some(entry_seed) = capture_navigation_entry_seed_for_holder(scope, owner) else { + let Some(mut entry_seed) = capture_navigation_entry_seed_for_holder(scope, owner) else { return; }; + entry_seed.session_history.commit = commit; let Some(host_ptr) = context_host_ptr_for_navigation_seed_owner(scope, owner) else { return; }; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_surface.rs b/moli-renderer-v8/src/context_bootstrap/navigation_surface.rs index cdbe7c2ca5..378323439f 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_surface.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_surface.rs @@ -1,7 +1,7 @@ use super::history_mutation::{history_push_state_callback, history_replace_state_callback}; use super::location_history_storage::{ - HISTORY_ENTRIES_SLOT, HISTORY_INDEX_SLOT, HISTORY_LENGTH_SLOT, HISTORY_SCROLL_RESTORATION_SLOT, - HISTORY_STATE_SLOT, NAVIGATION_CURRENT_ENTRY_SLOT, NAVIGATION_EVENT_LISTENERS_SLOT, + HISTORY_ENTRIES_SLOT, HISTORY_INDEX_SLOT, HISTORY_SCROLL_RESTORATION_SLOT, HISTORY_STATE_SLOT, + NAVIGATION_CURRENT_ENTRY_SLOT, NAVIGATION_EVENT_LISTENERS_SLOT, }; use super::navigation_activation::install_navigation_activation_runtime_state; use super::navigation_callbacks::{ @@ -9,7 +9,6 @@ use super::navigation_callbacks::{ navigation_update_current_entry_callback, }; use super::navigation_events::navigation_error_event_active; -use super::navigation_projection::set_history_length_from_visible_entries; use super::navigation_seed::{ build_current_navigation_entry_from_seed, build_history_entries_array_from_seed, }; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs b/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs index 13e558844f..46ddc66fd4 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_surface/accessors.rs @@ -3,8 +3,7 @@ use super::super::navigation_activation::{ navigation_activation_value, navigation_current_entry_value, navigation_transition_value, }; use super::super::navigation_entry::{ - history_length_value, history_scroll_restoration_value, history_state_value, - set_history_scroll_restoration, + history_scroll_restoration_value, history_state_value, set_history_scroll_restoration, }; use super::*; use moli_webapi_declare::WebApiObject; @@ -152,12 +151,10 @@ fn history_length_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if require_fully_active_history_owner(scope, args.this()).is_none() { + let Some(owner) = require_fully_active_history_owner(scope, args.this()) else { return; - } - let value = history_length_value(scope, args.this()) - .unwrap_or_else(|| v8::Number::new(scope, 0.0).into()); - rv.set(value); + }; + rv.set_double(super::super::session_history::length(scope, owner) as f64); } fn history_state_getter_function<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs b/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs index fbc3d562c9..b79c970b7f 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_surface/runtime.rs @@ -19,9 +19,6 @@ struct HistoryRuntimeObjectDeclaration<'scope> { #[webapi(slot = HISTORY_SCROLL_RESTORATION_SLOT)] scroll_restoration: &'static str, - #[webapi(slot = HISTORY_LENGTH_SLOT)] - length: f64, - #[webapi(slot = HISTORY_ENTRIES_SLOT)] entries: v8::Local<'scope, v8::Array>, @@ -60,13 +57,11 @@ pub(in crate::context_bootstrap) fn build_history_runtime_state<'s>( let history = HistoryRuntimeObjectDeclaration::new( v8::null(scope).into(), "auto", - 0.0, entries, initial_seed.current_index as f64, ) .bind(scope) .map_err(anyhow::Error::from)?; - set_history_length_from_visible_entries(scope, history, entries); Ok(history) } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs index 8670589b2a..a77d527a34 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs @@ -543,12 +543,15 @@ fn queue_browser_owned_top_level_history_traversal<'s>( delta: i64, ) { let owner = runtime_window_owner(scope, history); - if !runtime_window_is_global(scope, owner) { - return; - } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return; }; + if super::session_history::binding(scope, unsafe { &mut *host_ptr }, owner) + .popup + .is_some() + { + return; + } unsafe { &mut *host_ptr }.record_pending_top_level_history_traversal(delta); } @@ -563,7 +566,7 @@ pub(crate) fn queue_top_level_history_traversal_by_delta( let Some(target) = history_delta_traversal_target(scope, history, delta) else { return false; }; - let Some(entries) = history_entries(scope, history) else { + let Some(entries) = history_entries(scope, target.history) else { return false; }; let Some(current_entry) = entries @@ -578,7 +581,9 @@ pub(crate) fn queue_top_level_history_traversal_by_delta( else { return false; }; - if !navigation_entries_share_document(scope, current_entry, target_entry) { + if runtime_window_is_global(scope, target.owner) + && !navigation_entries_share_document(scope, current_entry, target_entry) + { return false; } queue_history_traversal_without_result(scope, target); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs index b9f2cb40aa..b5a2e53c81 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs @@ -1,24 +1,20 @@ use super::history_runtime::{ apply_history_entry, reject_pending_navigation_results, route_history_traversal_task, }; -use super::navigation_entry::{ - history_entries, history_index, navigation_entry_key_value, navigation_entry_url_value, -}; +use super::navigation_entry::{history_entries, history_index, navigation_entry_key_value}; use super::navigation_events::{ dispatch_beforeunload_for_runtime_owner, dispatch_navigation_traverse_event, dispatch_navigation_traverse_event_with_outcome, dispatch_pagehide_for_runtime_owner, dispatch_unload_for_runtime_owner, mark_navigation_outcome_default_prevented, }; -use super::navigation_lifecycle::finish_navigation_error_events; use super::navigation_result::{ navigation_dom_exception, navigation_immediate_current_entry_result, navigation_pending_result, navigation_rejected_dom_exception_result, }; use super::navigation_seed::history_entry_seed_for_traversal; use super::navigation_window::{ - child_browsing_context_handle_for_runtime_owner, runtime_top_window_owner, - runtime_window_is_global, window_history_for_holder, window_location_for_holder, - window_task_target_for_runtime_owner, + child_browsing_context_handle_for_runtime_owner, runtime_window_is_global, + window_history_for_holder, window_task_target_for_runtime_owner, }; use super::navigation_window::{navigation_document_is_active, window_navigation_for_holder}; use super::*; @@ -32,13 +28,18 @@ pub(super) struct TraversalTarget<'s> { pub(super) history: v8::Local<'s, v8::Object>, pub(super) current_index: u32, pub(super) target_index: u32, + pub(super) joint_step: Option, } pub(super) fn queue_navigation_traversal_with_result<'s>( scope: &mut v8::PinScope<'s, '_>, - target: TraversalTarget<'s>, + mut target: TraversalTarget<'s>, info: Option>, ) -> Option> { + target.joint_step = target.joint_step.or_else(|| { + traversal_target_entry(scope, &target) + .and_then(|entry| super::session_history::step_for_entry(scope, target.owner, entry)) + }); let child_handle = child_browsing_context_handle_for_traversal(scope, target.owner); let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { if !dispatch_traverse_event(scope, &target) { @@ -48,7 +49,14 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( "AbortError", )); } - apply_history_entry(scope, target.history, target.target_index, true, None); + apply_history_entry( + scope, + target.history, + target.target_index, + target.joint_step, + true, + None, + ); return Some(navigation_immediate_current_entry_result( scope, target.owner, @@ -62,12 +70,14 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( "AbortError", )); }; - if let Some((target_url, seed)) = history_entry_seed_for_traversal( + if let Some((target_url, mut seed)) = history_entry_seed_for_traversal( scope, target.owner, target.current_index, target.target_index, ) { + seed.session_history.target_step = target.joint_step; + super::session_history::capture_for_navigation(scope, target.owner, &mut seed); if runtime_window_is_global(scope, target.owner) { host.record_pending_location_navigation(target_url, Some(seed.clone())); return Some(navigation_pending_result(scope)); @@ -114,9 +124,6 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( "AbortError", )); } - if let Some(result) = cancel_child_traversal_if_parent_joint_traversal_cancels(scope, &target) { - return Some(result); - } let target_key = target_entry.and_then(|entry| navigation_entry_key_value(scope, entry)); let receiver_context = target .history @@ -127,6 +134,7 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( receiver_scope, exact_target, target.target_index, + target.joint_step, target_key, info, )?; @@ -138,31 +146,47 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( pub(super) fn queue_history_traversal_without_result<'s>( scope: &mut v8::PinScope<'s, '_>, - target: TraversalTarget<'s>, + mut target: TraversalTarget<'s>, ) { + target.joint_step = target.joint_step.or_else(|| { + traversal_target_entry(scope, &target) + .and_then(|entry| super::session_history::step_for_entry(scope, target.owner, entry)) + }); let child_handle = child_browsing_context_handle_for_traversal(scope, target.owner); let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { if !dispatch_traverse_event(scope, &target) { return; } - apply_history_entry(scope, target.history, target.target_index, true, None); + apply_history_entry( + scope, + target.history, + target.target_index, + target.joint_step, + true, + None, + ); return; }; let host = unsafe { &mut *host_ptr }; let Some(exact_target) = window_task_target_for_runtime_owner(scope, host, target.owner) else { return; }; - if let Some((target_url, seed)) = history_entry_seed_for_traversal( + if let Some((target_url, mut seed)) = history_entry_seed_for_traversal( scope, target.owner, target.current_index, target.target_index, ) { - if maybe_queue_joint_child_back_traversal(scope, host, &target) { - return; - } + seed.session_history.target_step = target.joint_step; + super::session_history::capture_for_navigation(scope, target.owner, &mut seed); if runtime_window_is_global(scope, target.owner) { - let delta = i64::from(target.target_index) - i64::from(target.current_index); + let binding = super::session_history::binding(scope, host, target.owner); + let Some(delta) = target + .joint_step + .and_then(|step| host.session_histories.get_mut(binding.popup).delta_to(step)) + else { + return; + }; host.record_pending_top_level_history_traversal(delta); return; } @@ -208,9 +232,15 @@ pub(super) fn queue_history_traversal_without_result<'s>( .get_creation_context(scope) .unwrap_or_else(|| scope.get_current_context()); let receiver_scope = &mut v8::ContextScope::new(scope, receiver_context); - if let Some(producer) = - host.queue_history_traversal(receiver_scope, exact_target, target.target_index) - { + let target_key = + target_entry.and_then(|entry| navigation_entry_key_value(receiver_scope, entry)); + if let Some(producer) = host.queue_history_traversal( + receiver_scope, + exact_target, + target.target_index, + target.joint_step, + target_key, + ) { route_history_traversal_task(receiver_scope, host, producer); } } @@ -310,6 +340,7 @@ pub(in crate::context_bootstrap) fn apply_pending_child_cross_document_traversal history, current_index: history_index(scope, history), target_index: traversal.target_index, + joint_step: traversal.seed.session_history.target_step, }; if !traversal_target_key_still_available( scope, @@ -365,152 +396,6 @@ fn reject_child_cross_document_traversal( reject_pending_navigation_results(scope, &traversal.results, error); } -fn cancel_child_traversal_if_parent_joint_traversal_cancels<'s>( - scope: &mut v8::PinScope<'s, '_>, - target: &TraversalTarget<'s>, -) -> Option> { - if runtime_window_is_global(scope, target.owner) || target.target_index >= target.current_index - { - return None; - } - let top_owner = runtime_top_window_owner(scope, target.owner); - if top_owner.strict_equals(target.owner.into()) { - return None; - } - let top_history = window_history_for_holder(scope, top_owner)?; - let top_current_index = history_index(scope, top_history); - let top_target_index = top_current_index.checked_sub(1)?; - let top_navigation = window_navigation_for_holder(scope, top_owner)?; - let outcome = dispatch_navigation_traverse_event_with_outcome( - scope, - top_navigation, - top_history, - top_target_index, - None, - ); - if outcome.proceed { - return None; - } - let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError"); - 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); - } - let href = window_location_for_holder(scope, top_owner) - .and_then(|location| super::location_runtime::location_href_slot(scope, location)) - .unwrap_or_default(); - finish_navigation_error_events(scope, top_navigation, error, &href); - Some(navigation_rejected_dom_exception_result( - scope, - "Navigation was canceled", - "AbortError", - )) -} - -fn maybe_queue_joint_child_back_traversal<'s>( - scope: &mut v8::PinScope<'s, '_>, - host: &mut JsContextHost, - target: &TraversalTarget<'s>, -) -> bool { - // The hidden initial about:blank predecessor is a top-level bookkeeping - // entry, not a fetchable destination. When back() reaches it, consume the - // most recent child joint-history entry instead. - if !runtime_window_is_global(scope, target.owner) { - return false; - } - if target.current_index != target.target_index.saturating_add(1) { - return false; - } - let Some(target_entry) = traversal_target_entry(scope, target) else { - return false; - }; - if navigation_entry_url_value(scope, target_entry).as_deref() != Some("about:blank") { - return false; - } - - for child_handle in host - .child_browsing_context_handles_in_document_order() - .into_iter() - .rev() - { - let Some(child_owner) = host.child_browsing_context_window_wrapper(scope, child_handle) - else { - continue; - }; - let Some(child_history) = window_history_for_holder(scope, child_owner) else { - continue; - }; - let child_current_index = history_index(scope, child_history); - let Some(child_target_index) = child_current_index.checked_sub(1) else { - continue; - }; - let child_target = TraversalTarget { - owner: child_owner, - history: child_history, - current_index: child_current_index, - target_index: child_target_index, - }; - if let Some((child_target_url, seed)) = history_entry_seed_for_traversal( - scope, - child_target.owner, - child_target.current_index, - child_target.target_index, - ) { - let Some(exact_target) = window_task_target_for_runtime_owner(scope, host, child_owner) - else { - continue; - }; - let target_key = traversal_target_entry(scope, &child_target) - .and_then(|entry| navigation_entry_key_value(scope, entry)); - let receiver_context = child_target - .history - .get_creation_context(scope) - .unwrap_or_else(|| scope.get_current_context()); - let receiver_scope = &mut v8::ContextScope::new(scope, receiver_context); - let Some(producer) = host.queue_child_cross_document_traversal( - receiver_scope, - exact_target, - child_handle, - child_target.target_index, - target_key, - child_target_url.as_str(), - seed, - ) else { - continue; - }; - route_history_traversal_task(receiver_scope, host, producer); - return true; - } - let target_entry = traversal_target_entry(scope, &child_target); - if !traversal_target_entry_still_available(scope, &child_target, target_entry) { - continue; - } - let Some(exact_target) = window_task_target_for_runtime_owner(scope, host, child_owner) - else { - continue; - }; - let receiver_context = child_target - .history - .get_creation_context(scope) - .unwrap_or_else(|| scope.get_current_context()); - let receiver_scope = &mut v8::ContextScope::new(scope, receiver_context); - if let Some(producer) = - host.queue_history_traversal(receiver_scope, exact_target, child_target.target_index) - { - route_history_traversal_task(receiver_scope, host, producer); - return true; - } - } - // No live child can consume this joint-history step. The hidden - // about:blank entry is renderer bookkeeping, so only the browser-side - // session-history controller can decide whether a real predecessor exists. - // Fall through to the top-level delta request instead of treating this as - // a renderer-local no-op. - false -} - fn child_browsing_context_handle_for_traversal<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_plan.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_plan.rs index cfc16c93c1..06f36e0040 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_plan.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_plan.rs @@ -67,6 +67,7 @@ pub(super) fn navigation_delta_traversal_plan<'s>( history, current_index: current_index as u32, target_index: next_index as u32, + joint_step: None, })) } @@ -98,6 +99,7 @@ pub(super) fn navigation_index_traversal_plan<'s>( history, current_index: history_index(scope, history), target_index, + joint_step: None, })); } let current_index = pending_target_index.unwrap_or_else(|| history_index(scope, history)); @@ -109,6 +111,7 @@ pub(super) fn navigation_index_traversal_plan<'s>( history, current_index, target_index, + joint_step: None, })) } @@ -117,18 +120,24 @@ pub(super) fn history_delta_traversal_target<'s>( history: v8::Local<'s, v8::Object>, delta: i64, ) -> Option> { - let current_index = pending_history_traversal_target_index(scope, history) - .unwrap_or_else(|| history_index(scope, history)) as i64; - let entries = history_entries(scope, history)?; - let next_index = current_index + delta; - if next_index < 0 || next_index >= entries.length() as i64 { - return None; - } let owner = runtime_window_owner(scope, history); - Some(TraversalTarget { - history, + let host = unsafe { &mut *crate::util::context_host_ptr_from_global_bridge(scope)? }; + let binding = super::session_history::binding(scope, host, owner); + let mut model = host.session_histories.get_mut(binding.popup).clone(); + if let Some(step) = host.pending_joint_history_step(&model) { + model.traverse(step); + } + let step = model.step_by_delta(delta)?; + let target = super::session_history::targets_at(scope, owner, step)? + .into_iter() + .next(); + // Steps whose only changed frame has been detached still advance the + // traversable asynchronously, without a synthetic popstate or reload. + Some(target.unwrap_or_else(|| TraversalTarget { owner, - current_index: current_index as u32, - target_index: next_index as u32, - }) + history, + current_index: history_index(scope, history), + target_index: history_index(scope, history), + joint_step: Some(step), + })) } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs index 656910275c..250f1b572c 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs @@ -106,7 +106,7 @@ pub(in crate::context_bootstrap) fn child_browsing_context_handle_for_runtime_ow }) } -fn runtime_window_dispatch_scope<'s>( +pub(super) fn runtime_window_dispatch_scope<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, ) -> Option { diff --git a/moli-renderer-v8/src/context_bootstrap/session_history.rs b/moli-renderer-v8/src/context_bootstrap/session_history.rs new file mode 100644 index 0000000000..a20cfa1ec9 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/session_history.rs @@ -0,0 +1,509 @@ +use super::navigation_entry::{ + history_entries, history_index, navigation_entry_document_id, navigation_entry_key_value, + set_history_entries, set_history_index, +}; +use super::navigation_window::{ + runtime_top_window_owner, runtime_window_dispatch_scope, window_history_for_holder, +}; +use crate::native_bridge::{JsContextHost, NavigationHistoryEntrySeed, OwnerDispatchScope}; +use crate::util::context_host_ptr_from_global_bridge; +use moli_page_types::{ + JointSessionHistory, NavigationHistoryDocumentId, NavigationHistoryEntryKey, + SessionHistoryCommit, SessionHistoryContextId, SessionHistoryEntry, SessionHistoryPosition, + SessionHistoryStepId, +}; + +#[derive(Clone, Copy)] +pub(super) struct HistoryBinding { + pub(super) popup: Option, + pub(super) context: SessionHistoryContextId, +} + +pub(super) fn binding<'s>( + scope: &mut v8::PinScope<'s, '_>, + host: &mut JsContextHost, + owner: v8::Local<'s, v8::Object>, +) -> HistoryBinding { + let top = runtime_top_window_owner(scope, owner); + let popup = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) + .or_else(|| crate::native_bridge::lightweight_popup_id_from_window(scope, top)); + let dispatch = runtime_window_dispatch_scope(scope, owner).unwrap_or(OwnerDispatchScope::Top); + HistoryBinding { + popup, + context: host.session_histories.context(dispatch), + } +} + +pub(super) fn entry_reference<'s>( + scope: &mut v8::PinScope<'s, '_>, + entry: v8::Local<'s, v8::Object>, +) -> Option { + Some(SessionHistoryEntry { + key: NavigationHistoryEntryKey::from_serialized(navigation_entry_key_value(scope, entry)?), + document: NavigationHistoryDocumentId::from_serialized(navigation_entry_document_id( + scope, entry, + )?), + }) +} + +pub(super) fn initialize<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + seed: &NavigationHistoryEntrySeed, +) { + let Some(snapshot) = seed + .entries + .iter() + .find(|entry| entry.history_index == seed.current_index) + else { + return; + }; + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let host = unsafe { &mut *host_ptr }; + let binding = binding(scope, host, owner); + let parent = match runtime_window_dispatch_scope(scope, owner) { + Some(OwnerDispatchScope::Child(handle)) => { + let parent = host + .child_browsing_context_parent_handle(handle) + .map_or(OwnerDispatchScope::Top, OwnerDispatchScope::Child); + Some(host.session_histories.context(parent)) + } + _ => None, + }; + host.session_histories.get_mut(binding.popup).attach( + binding.context, + parent, + SessionHistoryEntry { + key: NavigationHistoryEntryKey::from_serialized( + super::navigation_entry::navigation_entry_public_token(snapshot.key.as_str()), + ), + document: snapshot.document_id.clone(), + }, + ); +} + +pub(super) fn restore<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + seed: &NavigationHistoryEntrySeed, +) { + let Some(snapshot) = seed + .entries + .iter() + .find(|entry| entry.history_index == seed.current_index) + else { + return; + }; + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let host = unsafe { &mut *host_ptr }; + let binding = binding(scope, host, owner); + let history = host.session_histories.get_mut(binding.popup); + if binding.context == SessionHistoryContextId::ROOT + && let Some(snapshot) = &seed.session_history.traversable + { + *history = (**snapshot).clone(); + return; + } + let entry = SessionHistoryEntry { + key: NavigationHistoryEntryKey::from_serialized( + super::navigation_entry::navigation_entry_public_token(snapshot.key.as_str()), + ), + document: snapshot.document_id.clone(), + }; + // The seed may be installed repeatedly while a child navigation loads. + // Identity, rather than URL or list shape, makes the commit idempotent. + if history.entry(binding.context) == Some(&entry) { + return; + } + let update = match seed.session_history.commit { + SessionHistoryCommit::Push => { + history.push(binding.context, entry); + Some(moli_page_types::SessionHistoryUpdateKind::Push) + } + SessionHistoryCommit::Attach | SessionHistoryCommit::Replace => { + history.replace(binding.context, entry); + (seed.session_history.commit == SessionHistoryCommit::Replace) + .then_some(moli_page_types::SessionHistoryUpdateKind::Replace) + } + SessionHistoryCommit::Traverse => seed + .session_history + .target_step + .or_else(|| history.step_for_entry(binding.context, &entry.key)) + .and_then(|step| history.traverse(step)) + .filter(|delta| *delta != 0) + .map(|delta| moli_page_types::SessionHistoryUpdateKind::Traverse { delta }), + }; + if let Some(update) = update { + publish(scope, owner, update); + } +} + +pub(crate) fn install_session_history_position( + scope: &mut v8::PinScope<'_, '_>, + position: SessionHistoryPosition, +) { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let owner = scope.get_current_context().global(scope); + let entry = window_history_for_holder(scope, owner).and_then(|history| { + let index = history_index(scope, history); + history_entries(scope, history)? + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok()) + .and_then(|entry| entry_reference(scope, entry)) + }); + let history = unsafe { &mut *host_ptr }.session_histories.get_mut(None); + *history = JointSessionHistory::new(position); + if let Some(entry) = entry { + history.attach(SessionHistoryContextId::ROOT, None, entry); + } +} + +/// Called after the main realm is admitted. Child realms may be prebootstrapped +/// before it; creating their JS surfaces cannot register a main-frame entry. +pub(crate) fn initialize_main_session_history(scope: &mut v8::PinScope<'_, '_>) { + let owner = scope.get_current_context().global(scope); + let Some(history) = window_history_for_holder(scope, owner) else { + return; + }; + let index = history_index(scope, history); + let entry = history_entries(scope, history) + .and_then(|entries| entries.get_index(scope, index)) + .and_then(|value| v8::Local::::try_from(value).ok()) + .and_then(|entry| entry_reference(scope, entry)); + if let Some(entry) = entry + && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) + { + unsafe { &mut *host_ptr } + .session_histories + .get_mut(None) + .attach(SessionHistoryContextId::ROOT, None, entry); + } +} + +pub(super) fn length<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> usize { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return 1; + }; + let host = unsafe { &mut *host_ptr }; + let binding = binding(scope, host, owner); + host.session_histories + .get_mut(binding.popup) + .position() + .length() +} + +/// Commit the shared state before exposing currententrychange/dispose to JS. +pub(super) fn commit<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + entry: v8::Local<'s, v8::Object>, + kind: SessionHistoryCommit, +) { + let Some(entry) = entry_reference(scope, entry) else { + return; + }; + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let host = unsafe { &mut *host_ptr }; + let binding = binding(scope, host, owner); + let history = host.session_histories.get_mut(binding.popup); + match kind { + SessionHistoryCommit::Push => history.push(binding.context, entry), + SessionHistoryCommit::Attach | SessionHistoryCommit::Replace => { + history.replace(binding.context, entry) + } + SessionHistoryCommit::Traverse => { + if let Some(target) = history.step_for_entry(binding.context, &entry.key) { + history.traverse(target); + } + } + } + let update = match kind { + SessionHistoryCommit::Push => Some(moli_page_types::SessionHistoryUpdateKind::Push), + SessionHistoryCommit::Replace => Some(moli_page_types::SessionHistoryUpdateKind::Replace), + SessionHistoryCommit::Attach | SessionHistoryCommit::Traverse => None, + }; + if let Some(update) = update { + publish(scope, owner, update); + } +} + +/// Capture the next top-level Document's history without committing a pending +/// navigation to the old Document. Failed/canceled loads leave its cursor alone. +pub(super) fn capture_for_navigation<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + seed: &mut NavigationHistoryEntrySeed, +) { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let host = unsafe { &mut *host_ptr }; + let binding = binding(scope, host, owner); + if binding.context != SessionHistoryContextId::ROOT { + return; + } + let Some(entry) = seed + .entries + .iter() + .find(|entry| entry.history_index == seed.current_index) + else { + return; + }; + let entry = SessionHistoryEntry { + key: NavigationHistoryEntryKey::from_serialized( + super::navigation_entry::navigation_entry_public_token(entry.key.as_str()), + ), + document: entry.document_id.clone(), + }; + let mut history = host.session_histories.get_mut(binding.popup).clone(); + match seed.session_history.commit { + SessionHistoryCommit::Push => history.push(binding.context, entry), + SessionHistoryCommit::Attach | SessionHistoryCommit::Replace => { + history.replace(binding.context, entry) + } + SessionHistoryCommit::Traverse => { + if let Some(target) = seed + .session_history + .target_step + .or_else(|| history.step_for_entry(binding.context, &entry.key)) + { + history.traverse(target); + } + } + } + seed.session_history.traversable = Some(Box::new(history)); +} + +pub(super) fn owner_for_context<'s>( + scope: &mut v8::PinScope<'s, '_>, + host: &mut JsContextHost, + context: SessionHistoryContextId, + popup: Option, +) -> Option> { + match host.session_histories.owner(context, popup)? { + OwnerDispatchScope::Top => Some( + host.page_default_context(scope) + .unwrap_or_else(|| scope.get_current_context()) + .global(scope), + ), + OwnerDispatchScope::Child(handle) => { + host.child_browsing_context_window_wrapper(scope, handle) + } + OwnerDispatchScope::LightweightPopup(id) => host.lightweight_popup_window(scope, id), + } +} + +pub(super) fn step_for_entry<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + entry: v8::Local<'s, v8::Object>, +) -> Option { + let key = entry_reference(scope, entry)?.key; + let host = unsafe { &mut *context_host_ptr_from_global_bridge(scope)? }; + let binding = binding(scope, host, owner); + host.session_histories + .get_mut(binding.popup) + .step_for_entry(binding.context, &key) +} + +/// Remove forward entries in every live Document view, returning objects for +/// dispose delivery after the complete state change. The views never determine +/// the number or order of joint steps. +pub(super) fn prune_views<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> Vec> { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return Vec::new(); + }; + let host = unsafe { &mut *host_ptr }; + let binding = binding(scope, host, owner); + let model = host.session_histories.get_mut(binding.popup).clone(); + let mut owners = vec![runtime_top_window_owner(scope, owner)]; + if binding.popup.is_none() { + for handle in host.child_browsing_context_handles_in_document_order() { + if let Some(window) = host.child_browsing_context_window_wrapper(scope, handle) { + owners.push(window); + } + } + } + let mut removed = Vec::new(); + for owner in owners { + let context = self::binding(scope, host, owner).context; + let Some(history) = window_history_for_holder(scope, owner) else { + continue; + }; + let Some(entries) = history_entries(scope, history) else { + continue; + }; + let old_current = history_index(scope, history); + let mut retained = Vec::new(); + let mut current = 0; + for index in 0..entries.length() { + let Some(entry) = entries + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + continue; + }; + let keep = index <= old_current + || entry_reference(scope, entry) + .is_some_and(|entry| model.contains_entry(context, &entry.key)); + if keep { + if index == old_current { + current = retained.len() as u32; + } + retained.push(entry); + } else { + removed.push(entry); + } + } + if retained.len() == entries.length() as usize { + continue; + } + let array = v8::Array::new(scope, retained.len() as i32); + for (index, entry) in retained.into_iter().enumerate() { + let _ = array.set_index(scope, index as u32, entry.into()); + // Removing a suffix preserves Navigation's filtered indices. Raw + // history includes hidden entries and cannot supply these indices. + } + set_history_entries(scope, history, array); + set_history_index(scope, history, current); + super::navigation_serialize::sync_child_navigation_entry_seed_from_owner(scope, owner); + } + removed +} + +pub(crate) fn prune_joint_session_history(scope: &mut v8::PinScope<'_, '_>) { + if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { + unsafe { &mut *host_ptr } + .session_histories + .get_mut(None) + .prune_all_but_current(); + } + let owner = scope.get_current_context().global(scope); + publish( + scope, + owner, + moli_page_types::SessionHistoryUpdateKind::PruneAllButCurrent, + ); +} + +pub(super) fn targets_at<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + step: SessionHistoryStepId, +) -> Option>> { + let host = unsafe { &mut *context_host_ptr_from_global_bridge(scope)? }; + let binding = binding(scope, host, owner); + let entries = host + .session_histories + .get_mut(binding.popup) + .entries_at(step)? + .clone(); + // An opaque browser-owned step must go back to the browser controller. + if !entries.contains_key(&SessionHistoryContextId::ROOT) { + return None; + } + let mut targets = Vec::new(); + for (context, entry) in entries { + let Some(owner) = owner_for_context(scope, host, context, binding.popup) else { + continue; + }; + let Some(history) = window_history_for_holder(scope, owner) else { + continue; + }; + let local_entries = history_entries(scope, history)?; + let current_index = history_index(scope, history); + let index = (0..local_entries.length()).find(|index| { + local_entries + .get_index(scope, *index) + .and_then(|value| v8::Local::::try_from(value).ok()) + .and_then(|entry| navigation_entry_key_value(scope, entry)) + .is_some_and(|key| key == entry.key.as_str()) + }); + let Some(target_index) = index else { + if context == SessionHistoryContextId::ROOT { + return None; + } + continue; + }; + if current_index != target_index { + targets.push(super::navigation_traversal_execution::TraversalTarget { + owner, + history, + current_index, + target_index, + joint_step: Some(step), + }); + } + } + Some(targets) +} + +pub(super) fn commit_traversal<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + step: SessionHistoryStepId, +) -> Option { + let host = unsafe { &mut *context_host_ptr_from_global_bridge(scope)? }; + let binding = binding(scope, host, owner); + host.session_histories.get_mut(binding.popup).traverse(step) +} + +pub(super) fn publish<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + update: moli_page_types::SessionHistoryUpdateKind, +) { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let host = unsafe { &mut *host_ptr }; + let binding = binding(scope, host, owner); + if binding.popup.is_some() { + return; + } + let history = host.session_histories.get_mut(None); + let position = history.position(); + let root_key = history + .entry(SessionHistoryContextId::ROOT) + .map(|entry| entry.key.clone()); + let root_entry_steps = root_key.as_ref().map_or_else(Vec::new, |key| { + history.steps_for_entry(SessionHistoryContextId::ROOT, key) + }); + let top = runtime_top_window_owner(scope, owner); + let root_url = window_history_for_holder(scope, top) + .and_then(|history| history_entries(scope, history)) + .and_then(|entries| { + (0..entries.length()).find_map(|index| { + let entry = entries + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let key = navigation_entry_key_value(scope, entry)?; + if root_key.as_ref().is_some_and(|root| root.as_str() == key) { + super::navigation_entry::navigation_entry_url_value(scope, entry) + } else { + None + } + }) + }) + .unwrap_or_else(|| host.document_url().as_str().to_owned()); + host.record_session_history_update(moli_page_types::SessionHistoryUpdate { + position, + update, + root_url, + root_entry_steps, + }); +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs index 60b86fe205..48dce47a6a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/bootstrap_commit.rs @@ -191,7 +191,7 @@ impl JsContextHost { self.clear_child_browsing_context_pending_navigation(handle); if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) { entry.clear_pending_document_load(); - entry.clear_pending_top_level_history_length_increment(); + entry.restore_navigation_entry_seed_from_committed(); } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs index 84ec7cc11c..3d591f4d2e 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs @@ -464,7 +464,6 @@ impl JsContextHost { self.clear_child_browsing_context_pending_navigation(handle); if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) { entry.restore_navigation_entry_seed_from_committed(); - entry.clear_pending_top_level_history_length_increment(); } self.sync_existing_child_browsing_context_window_state(scope, handle); self.reject_replaced_service_worker_child_client_navigation( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs index edba37d62a..058280f383 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs @@ -104,7 +104,7 @@ impl JsContextHost { return false; }; if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) { - entry.replace_navigation_entry_seed_and_clear_pending_history_increment(entry_seed); + entry.replace_navigation_entry_seed(entry_seed); } if self .set_child_browsing_context_pending_navigation( @@ -174,15 +174,6 @@ impl JsContextHost { self.queue_child_browsing_context_navigation_commit(handle) } - pub(crate) fn mark_child_browsing_context_top_level_history_increment( - &mut self, - handle: DomHandle, - ) { - if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) { - entry.mark_pending_top_level_history_length_increment(); - } - } - pub(crate) fn queue_child_browsing_context_reload_from_existing_seed( &mut self, handle: DomHandle, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs index 1703ad3995..3d554e535f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs @@ -5,7 +5,6 @@ use super::super::{ }; use crate::native_bridge::context_host::child_documents::ChildDocumentNavigationInitiator; use crate::{ - context_bootstrap::increment_top_level_history_length_for_runtime_owner, document_runtime::DomHandle, document_script_scheduler::FrameDocumentClassicScriptSchedulerWork, frame_owner_model::{ @@ -416,10 +415,6 @@ impl JsContextHost { self.sync_existing_child_browsing_context_window_state(scope, handle); return None; } - let increment_top_level_history_length = self - .child_browsing_contexts - .get_mut(&handle) - .is_some_and(|entry| entry.take_pending_top_level_history_length_increment()); self.clear_child_browsing_context_pending_navigation(handle); self.clear_pending_form_submission_child_target(handle); let commit_result = self.commit_child_document_bootstrap_or_start_load( @@ -430,11 +425,6 @@ impl JsContextHost { ChildDocumentNavigationInitiator::BrowsingContext, ); self.sync_existing_child_browsing_context_window_state(scope, handle); - if increment_top_level_history_length - && let Some(window) = self.child_browsing_context_window_wrapper(scope, handle) - { - increment_top_level_history_length_for_runtime_owner(scope, window); - } commit_result } @@ -508,7 +498,7 @@ impl JsContextHost { return false; }; entry.clear_pending_navigation(); - entry.clear_pending_top_level_history_length_increment(); + entry.restore_navigation_entry_seed_from_committed(); self.clear_pending_form_submission_child_target(handle); self.reject_replaced_service_worker_child_client_navigation( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/sync.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/sync.rs index 3512d4669f..daca4b5a37 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/sync.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/sync.rs @@ -2,7 +2,7 @@ use super::super::JsContextHost; use super::document_slots::sync_child_document_window_slots; use crate::{ context_bootstrap::{ - bind_window_performance_seed, install_navigation_bootstrap_entry_for_holder, + bind_window_performance_seed, install_navigation_entry_view_for_holder, reset_window_location_history_navigation_runtime_state, set_window_origin_runtime_state, sync_window_location_history_navigation_runtime_surface, }, @@ -94,7 +94,12 @@ impl JsContextHost { } else { sync_window_location_history_navigation_runtime_surface(scope, wrapper); } - install_navigation_bootstrap_entry_for_holder(scope, wrapper, &visible_state.entry_seed); + install_navigation_entry_view_for_holder( + scope, + wrapper, + &visible_state.entry_seed, + visible_state.seed_is_committed, + ); let performance_navigation_type = self.child_performance_navigation_type(handle); let _ = bind_window_performance_seed( scope, @@ -159,7 +164,7 @@ impl JsContextHost { wrapper, ¤t_entry.url, ); - install_navigation_bootstrap_entry_for_holder(scope, wrapper, &entry_seed); + install_navigation_entry_view_for_holder(scope, wrapper, &entry_seed, false); let performance_navigation_type = self.child_performance_navigation_type(handle); let _ = bind_window_performance_seed( scope, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs index 908f556fe4..dc87a33dd2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs @@ -62,7 +62,6 @@ pub(super) struct ChildBrowsingContextEntry { service_worker_client_id: Option, pending_service_worker_client_id: Option, pending_service_worker_client_navigation: Option, - pending_top_level_history_length_increment: bool, } #[derive(Debug, Clone)] @@ -162,7 +161,18 @@ impl ChildBrowsingContextEntry { &self, bootstrap: &ChildBrowsingContextBootstrap, ) -> bool { - self.attribute_bootstrap != *bootstrap + match (&self.attribute_bootstrap, bootstrap) { + ( + ChildBrowsingContextBootstrap::Srcdoc { markup: before, .. }, + ChildBrowsingContextBootstrap::Srcdoc { markup: after, .. }, + ) => { + // The inherited base URL is input to document creation, not + // a navigation attribute. Parent pushState/replaceState must + // not replace the child Document or retire its queued tasks. + before != after + } + (before, after) => before != after, + } } pub(super) fn pending_attribute_bootstrap_commit(&self) -> bool { @@ -696,14 +706,6 @@ impl ChildBrowsingContextEntry { self.navigation_entry_seed = entry_seed; } - pub(super) fn replace_navigation_entry_seed_and_clear_pending_history_increment( - &mut self, - entry_seed: NavigationHistoryEntrySeed, - ) { - self.replace_navigation_entry_seed(entry_seed); - self.clear_pending_top_level_history_length_increment(); - } - pub(super) fn apply_navigation_to_entry_seed(&mut self, url: &Url) { apply_child_navigation_to_seed(&mut self.navigation_entry_seed, url, None, None); } @@ -732,7 +734,6 @@ impl ChildBrowsingContextEntry { pub(super) fn apply_deferred_navigation_to_entry_seed(&mut self, url: &Url) { self.apply_navigation_to_entry_seed(url); - self.clear_pending_top_level_history_length_increment(); } pub(super) fn clear_navigation_activation(&mut self) { @@ -884,27 +885,8 @@ impl ChildBrowsingContextEntry { self.pending_document_load_id == Some(load_id) } - pub(super) fn mark_pending_top_level_history_length_increment(&mut self) { - self.pending_top_level_history_length_increment = true; - } - - pub(super) fn pending_top_level_history_length_increment(&self) -> bool { - self.pending_top_level_history_length_increment - } - - pub(super) fn clear_pending_top_level_history_length_increment(&mut self) { - self.pending_top_level_history_length_increment = false; - } - - pub(super) fn take_pending_top_level_history_length_increment(&mut self) -> bool { - let increment = self.pending_top_level_history_length_increment; - self.clear_pending_top_level_history_length_increment(); - increment - } - pub(super) fn clear_pending_form_submission_navigation(&mut self) { self.pending_live_navigation = None; - self.clear_pending_top_level_history_length_increment(); } pub(super) fn has_pending_live_navigation(&self) -> bool { diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs index a36c85b836..c7cfde2912 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs @@ -8,6 +8,8 @@ impl JsContextHost { &mut self, handle: DomHandle, ) -> Option { + self.session_histories + .detach(super::super::OwnerDispatchScope::Child(handle)); self.child_browsing_contexts.shift_remove(&handle) } @@ -424,10 +426,6 @@ impl JsContextHost { pending_service_worker_client_navigation: existing .as_ref() .and_then(|entry| entry.pending_service_worker_client_navigation()), - pending_top_level_history_length_increment: existing - .as_ref() - .map(|entry| entry.pending_top_level_history_length_increment()) - .unwrap_or(false), }, ); if attribute_bootstrap_changed { diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 5a411d2b4b..4eeeabf7b8 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -195,6 +195,7 @@ impl JsContextHost { let visual_resource_generation = super::visual_resource_generation::VisualResourceGeneration::default(); let mut host = Self { + session_histories: Default::default(), runtime: runtime as *mut DocumentRuntime, layout_policy: moli_page_types::LayoutPolicy::default(), document_layout_state: RefCell::new(super::layout_state::DocumentLayoutState::default()), diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index c1a5ef18cf..65c1f01140 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -1,4 +1,5 @@ use super::{bridge::NativeDomBridge, history_queue::HistoryQueueState}; +mod session_history; use crate::{ network::context::DocumentResourceLoaderRegistry, { @@ -797,6 +798,7 @@ pub(crate) type JsContextHostPageTaskCapabilities = crate::page_task_queue::RendererPageJsContextTaskSenders; pub(crate) struct JsContextHost { + pub(crate) session_histories: session_history::RendererSessionHistories, runtime: *mut DocumentRuntime, layout_policy: moli_page_types::LayoutPolicy, document_layout_state: RefCell, diff --git a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs index 310a7853b7..67b2d6893e 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs @@ -7,7 +7,7 @@ use crate::runtime::{ }; use crate::service_worker_runtime::{ServiceWorkerClientId, ServiceWorkerClientNavigateError}; use moli_fetch::BrowserNavigationRequestKind; -use moli_page_types::{NavigationHistoryEntrySeed, SameDocumentHistoryUpdate}; +use moli_page_types::NavigationHistoryEntrySeed; use url::Url; pub(crate) struct PendingReservedServiceWorkerClient { @@ -92,6 +92,25 @@ pub(crate) enum PendingTopLevelNavigation { } impl JsContextHost { + pub(crate) fn record_session_history_update( + &mut self, + update: moli_page_types::SessionHistoryUpdate, + ) { + let Some(source_document) = self + .root_document_lifecycle + .as_ref() + .map(RendererDocumentLifecycleJournalHandle::identity) + else { + return; + }; + self.append_live_turn_owner_action( + crate::runtime::RendererOwnerAction::SessionHistoryUpdate { + source_document, + update, + }, + ); + } + /// Replace the dynamically scoped Runtime command cause and return the /// previous scope for exact restoration after V8 dispatch. /// @@ -337,12 +356,7 @@ impl JsContextHost { }) } - pub(crate) fn record_same_document_navigation( - &mut self, - url: &Url, - navigation_type: &str, - history_update: SameDocumentHistoryUpdate, - ) { + pub(crate) fn record_same_document_navigation(&mut self, url: &Url, navigation_type: &str) { let Some(source_document) = self .root_document_lifecycle .as_ref() @@ -364,7 +378,6 @@ impl JsContextHost { crate::runtime::RendererPendingSameDocumentNavigation { url: url.to_string(), navigation_type: navigation_type.to_owned(), - history_update, }, ); if !self.append_live_turn_owner_action( diff --git a/moli-renderer-v8/src/native_bridge/context_host/session_history.rs b/moli-renderer-v8/src/native_bridge/context_host/session_history.rs new file mode 100644 index 0000000000..7a1c77a781 --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/session_history.rs @@ -0,0 +1,57 @@ +use super::OwnerDispatchScope; +use moli_page_types::{JointSessionHistory, SessionHistoryContextId}; +use std::collections::HashMap; + +/// Each traversable owns one history. Lightweight popup Windows share a V8 +/// host with their opener, but have independent traversables. +#[derive(Default)] +pub(crate) struct RendererSessionHistories { + main: JointSessionHistory, + popups: HashMap, + contexts: HashMap, +} + +impl RendererSessionHistories { + pub(crate) fn detach(&mut self, owner: OwnerDispatchScope) { + if let Some(context) = self.contexts.remove(&owner) { + self.main.detach(context); + for history in self.popups.values_mut() { + history.detach(context); + } + } + } + pub(crate) fn get_mut(&mut self, popup: Option) -> &mut JointSessionHistory { + match popup { + None => &mut self.main, + Some(id) => self.popups.entry(id).or_default(), + } + } + + pub(crate) fn context(&mut self, owner: OwnerDispatchScope) -> SessionHistoryContextId { + match owner { + OwnerDispatchScope::Top | OwnerDispatchScope::LightweightPopup(_) => { + SessionHistoryContextId::ROOT + } + OwnerDispatchScope::Child(_) => *self + .contexts + .entry(owner) + .or_insert_with(SessionHistoryContextId::allocate), + } + } + + pub(crate) fn owner( + &self, + context: SessionHistoryContextId, + popup: Option, + ) -> Option { + if context == SessionHistoryContextId::ROOT { + return Some(popup.map_or( + OwnerDispatchScope::Top, + OwnerDispatchScope::LightweightPopup, + )); + } + self.contexts + .iter() + .find_map(|(owner, id)| (*id == context).then_some(*owner)) + } +} diff --git a/moli-renderer-v8/src/native_bridge/history_queue.rs b/moli-renderer-v8/src/native_bridge/history_queue.rs index 5d865e1c5a..1a781059ba 100644 --- a/moli-renderer-v8/src/native_bridge/history_queue.rs +++ b/moli-renderer-v8/src/native_bridge/history_queue.rs @@ -54,6 +54,7 @@ pub(crate) struct QueuedNavigationApiTask { } pub(crate) struct PendingHistoryTraversal { + pub(crate) joint_step: Option, pub(crate) target: WindowTaskTarget, pub(crate) target_index: u32, pub(crate) target_key: Option, @@ -134,6 +135,7 @@ impl HistoryQueueState { relevant_context: WindowExecutionContextBinding, target: WindowTaskTarget, target_index: u32, + joint_step: Option, target_key: Option, info: Option>, result: Option, @@ -152,6 +154,7 @@ impl HistoryQueueState { }) { pending.target_index = target_index; + pending.joint_step = joint_step; pending.target_key = target_key; pending.info = info; if let Some(result) = result { @@ -170,6 +173,7 @@ impl HistoryQueueState { execution_context, relevant_context, action: PendingHistoryTraversalAction::SameDocument(PendingHistoryTraversal { + joint_step, target, target_index, target_key, @@ -387,11 +391,32 @@ impl JsContextHost { .pending_history_traversal_target_index(target) } + pub(crate) fn pending_joint_history_step( + &self, + history: &moli_page_types::JointSessionHistory, + ) -> Option { + self.history_queue + .pending_history_traversal_tasks + .iter() + .rev() + .find_map(|queued| { + let step = match &queued.action { + PendingHistoryTraversalAction::SameDocument(pending) => pending.joint_step, + PendingHistoryTraversalAction::ChildCrossDocument(pending) => { + pending.seed.session_history.target_step + } + }?; + history.entries_at(step).map(|_| step) + }) + } + pub(crate) fn queue_history_traversal<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, target: WindowTaskTarget, target_index: u32, + joint_step: Option, + target_key: Option, ) -> Option { let execution_context = self.current_runtime_window_execution_context_identity(scope)?; let relevant_context = self.current_runtime_window_execution_context_binding(scope)?; @@ -401,7 +426,8 @@ impl JsContextHost { relevant_context, target, target_index, - None, + joint_step, + target_key, None, None, )?; @@ -499,6 +525,7 @@ impl JsContextHost { scope: &mut v8::PinScope<'s, '_>, target: WindowTaskTarget, target_index: u32, + joint_step: Option, target_key: Option, info: Option>, ) -> Option<( @@ -522,6 +549,7 @@ impl JsContextHost { &mut self.history_queue.pending_history_traversal_tasks[existing_index].action && !pending.results.is_empty() { + pending.joint_step = joint_step; pending.target_key = target_key; pending.info = info.map(|info| v8::Global::new(scope, info)); let result = &pending.results[0]; @@ -551,6 +579,7 @@ impl JsContextHost { relevant_context, target, target_index, + joint_step, target_key, info.map(|info| v8::Global::new(scope, info)), Some(PendingNavigationResult { diff --git a/moli-renderer-v8/src/runtime/page_surface.rs b/moli-renderer-v8/src/runtime/page_surface.rs index 5710a952d3..66c4f58482 100644 --- a/moli-renderer-v8/src/runtime/page_surface.rs +++ b/moli-renderer-v8/src/runtime/page_surface.rs @@ -14,7 +14,7 @@ pub use moli_page_types::{ RendererAgentAttachmentId, RendererDevToolsAgentToken, RendererDomDebuggerEventListenerBreakpoint, RendererDomDebuggerXhrBreakpoint, RendererInspectorProtocolConfiguration, RendererInspectorProtocolConfigurationCommand, - RendererInspectorSessionRestoreSnapshot, SameDocumentHistoryUpdate, V8InspectorSessionState, + RendererInspectorSessionRestoreSnapshot, V8InspectorSessionState, }; use moli_shared_worker::SharedWorkerInstanceId; use parking_lot::Mutex; @@ -244,7 +244,6 @@ impl RendererPendingFileChooserActivation { pub struct RendererPendingSameDocumentNavigation { pub url: String, pub navigation_type: String, - pub history_update: SameDocumentHistoryUpdate, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -813,8 +812,8 @@ pub struct RendererMainDocumentCommit { pub security_origin: String, pub secure_context_type: String, pub timestamp: f64, - /// Browser-owned session-history length at this document's commit. - pub session_history_length: Option, + /// Browser-owned session-history cursor and length at this document's commit. + pub session_history_position: Option, } #[derive(Debug, Clone, Default, PartialEq)] diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index f3205e425f..d7f620759b 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -4406,12 +4406,12 @@ impl PageVm { .vm_mut() .install_navigation_bootstrap_entry(env.navigation_bootstrap_entry.clone()); if env.navigation_bootstrap_entry.is_none() - && let Some(length) = env + && let Some(position) = env .main_document_commit .as_ref() - .and_then(|commit| commit.session_history_length) + .and_then(|commit| commit.session_history_position) { - page_vm.vm_mut().install_session_history_length(length); + page_vm.vm_mut().install_session_history_position(position); } Ok(page_vm) } diff --git a/moli-renderer-v8/src/runtime/protocol_output/item.rs b/moli-renderer-v8/src/runtime/protocol_output/item.rs index a5755adc55..553a22e3b5 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/item.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/item.rs @@ -52,6 +52,10 @@ pub enum RendererOwnerAction { event: ChildFrameNavigationSnapshot, }, SameDocumentNavigation(RendererDocumentSourcedSameDocumentNavigation), + SessionHistoryUpdate { + source_document: RendererDocumentLifecycleIdentity, + update: moli_page_types::SessionHistoryUpdate, + }, TopLevelLocationNavigation(RendererDocumentSourcedTopLevelLocationNavigation), TopLevelHistoryTraversal(RendererPendingTopLevelHistoryTraversal), SubresourceFetchPause { diff --git a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs index 87f0036834..51f94f8c74 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs @@ -197,6 +197,13 @@ fn owner_action_transport_charge_bytes(action: &RendererOwnerAction) -> usize { string_charge(&navigation.url) .saturating_add(string_charge(&navigation.navigation_type)) } + RendererOwnerAction::SessionHistoryUpdate { update, .. } => string_charge(&update.root_url) + .saturating_add( + update + .root_entry_steps + .len() + .saturating_mul(std::mem::size_of::()), + ), RendererOwnerAction::TopLevelLocationNavigation(event) => string_charge(event.url()) .saturating_add(string_charge(event.request_method())) .saturating_add( diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 9d852ead39..0027e75282 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -2093,6 +2093,8 @@ impl ScriptVmPageRealmBootstrap { context_host .borrow_mut() .install_page_default_context(scope, local_context); + let scope = &mut v8::ContextScope::new(scope, local_context); + super::context_bootstrap::initialize_main_session_history(scope); Ok(()) }) { @@ -5845,9 +5847,12 @@ impl ScriptVm { }) } - pub(super) fn install_session_history_length(&mut self, length: usize) { + pub(super) fn install_session_history_position( + &mut self, + position: moli_page_types::SessionHistoryPosition, + ) { let _ = self.with_default_context_scope(|scope, _runtime_ptr| { - super::context_bootstrap::install_session_history_length(scope, length); + super::context_bootstrap::install_session_history_position(scope, position); Ok(()) }); } diff --git a/moli-renderer-v8/src/script_vm/navigation_history.rs b/moli-renderer-v8/src/script_vm/navigation_history.rs index a41d952054..57d81a102d 100644 --- a/moli-renderer-v8/src/script_vm/navigation_history.rs +++ b/moli-renderer-v8/src/script_vm/navigation_history.rs @@ -70,6 +70,11 @@ impl ScriptVm { plans.push((realm, plan)); } + self.with_default_context_scope(|scope, _host_ptr| { + crate::context_bootstrap::prune_joint_session_history(scope); + Ok(()) + })?; + for (realm, plan) in plans { let Some(context_ptr) = self.navigation_history_realm_context_ptr(realm) else { continue; @@ -84,9 +89,8 @@ impl ScriptVm { } } - // Dispose handlers can create a new child realm. Re-enumerate before - // publishing the pruned joint session history length so those realms - // observe the same traversable state. + // Dispose handlers can create a new child realm. Check its view without + // pruning again: callbacks may have appended valid new joint steps. for realm in self.live_navigation_history_realms() { let Some(context_ptr) = self.navigation_history_realm_context_ptr(realm) else { continue; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index c521be6096..df68fbfec2 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -4130,14 +4130,10 @@ async fn reset_navigation_history_updates_all_live_window_realms() { (() => { const child = document.querySelector("iframe").contentWindow; globalThis.__lmResetRealmOrder = []; - globalThis.__lmResetTopLengthBeforeDispose = history.length; - globalThis.__lmResetChildLengthBeforeDispose = child.history.length; __lmResetRealmEntries[0].addEventListener("dispose", () => { __lmResetRealmOrder.push({ listener: "top", - lengthsUnchanged: - history.length === __lmResetTopLengthBeforeDispose && - child.history.length === __lmResetChildLengthBeforeDispose, + sharedLengthPruned: history.length === 1 && child.history.length === 1, topEntries: navigation.entries().length, childEntries: child.navigation.entries().length }); @@ -4152,9 +4148,7 @@ async fn reset_navigation_history_updates_all_live_window_realms() { __lmResetRealmEntries[0].addEventListener("dispose", () => { parent.__lmResetRealmOrder.push({ listener: "child", - lengthsUnchanged: - history.length === parent.__lmResetChildLengthBeforeDispose && - parent.history.length === parent.__lmResetTopLengthBeforeDispose, + sharedLengthPruned: history.length === 1 && parent.history.length === 1, topEntries: parent.navigation.entries().length, childEntries: navigation.entries().length }); @@ -4208,7 +4202,7 @@ JSON.stringify({ assert_eq!( vm.eval("JSON.stringify(__lmResetRealmOrder)") .expect("cross-realm reset order should evaluate"), - r#"[{"listener":"top","lengthsUnchanged":true,"topEntries":1,"childEntries":2},{"listener":"child","lengthsUnchanged":true,"topEntries":1,"childEntries":1}]"# + r#"[{"listener":"top","sharedLengthPruned":true,"topEntries":1,"childEntries":2},{"listener":"child","sharedLengthPruned":true,"topEntries":1,"childEntries":1}]"# ); } @@ -4286,7 +4280,7 @@ JSON.stringify({ "#, ) .expect("reentrant child reset history state should evaluate"), - r#"{"historyLength":1,"navigationLength":2,"currentIndex":1,"retainedPreviousCurrent":true,"appendedCurrent":true,"stateRealm":"child-during-top-dispose","disposed":[0]}"# + r#"{"historyLength":2,"navigationLength":2,"currentIndex":1,"retainedPreviousCurrent":true,"appendedCurrent":true,"stateRealm":"child-during-top-dispose","disposed":[0]}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index 36654d1058..3ca93e4dcd 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -10352,6 +10352,184 @@ fn no_src_iframe_initial_about_blank_load_is_synchronous_at_connection() { ); } +#[tokio::test] +async fn joint_history_branch_shrinks_after_traversal() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = + new_storage_page_task_executor_test_vm_with_loader("https://joint-history.test/", &loader); + vm.eval("history.replaceState('A', ''); for (const state of ['B', 'C', 'D']) history.pushState(state, ''); history.go(-2); 'queued'") + .expect("queue traversal to B"); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .expect("traverse"); + assert_eq!( + vm.eval("[history.state, history.length].join('|')") + .unwrap(), + "B|4" + ); + assert_eq!(vm.eval("history.pushState('E', ''); [history.state, history.length, navigation.entries().length, navigation.canGoForward].join('|')").unwrap(), "E|3|3|false"); +} + +#[tokio::test] +async fn joint_history_siblings_share_steps_and_forward_pruning() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = + new_storage_page_task_executor_test_vm_with_loader("https://joint-history.test/", &loader); + vm.eval( + r#" + for (const id of ['a', 'b']) { + const frame = document.createElement('iframe'); frame.id = id; + frame.srcdoc = '

child

'; + (document.body || document.documentElement || document).appendChild(frame); + } + 'ready' + "#, + ) + .expect("create siblings"); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .expect("load siblings"); + assert_eq!(vm.eval(r#" + globalThis.a = document.getElementById('a').contentWindow; + globalThis.b = document.getElementById('b').contentWindow; + globalThis.held = [history, a.history, b.history]; + globalThis.snapshot = () => held.map(h => h.length).concat(held.map(h => h.state)).join('|'); + history.replaceState('top', ''); a.history.replaceState('a0', ''); b.history.replaceState('b0', ''); + a.history.pushState('a1', ''); b.history.pushState('b1', ''); a.history.pushState('a2', ''); + snapshot() + "#).unwrap(), "4|4|4|top|a2|b1"); + vm.eval("history.back(); 'queued'").unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .expect("back targets a, not last iframe b"); + assert_eq!(vm.eval("snapshot()").unwrap(), "4|4|4|top|a1|b1"); + vm.eval("a.history.back(); 'queued'").unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .expect("child caller traverses joint history to b0"); + assert_eq!(vm.eval("snapshot()").unwrap(), "4|4|4|top|a1|b0"); + assert_eq!( + vm.eval("b.history.pushState('b2', ''); snapshot()") + .unwrap(), + "3|3|3|top|a1|b2" + ); + assert_eq!(vm.eval("[a.navigation.entries().length, b.navigation.entries().length, a.navigation.canGoForward, b.navigation.canGoForward].join('|')").unwrap(), "2|2|false|false"); +} + +#[tokio::test] +async fn joint_history_navigation_traverse_uses_nearest_shared_step() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = + new_storage_page_task_executor_test_vm_with_loader("https://joint-history.test/", &loader); + vm.eval( + r#" + const frame = document.createElement('iframe'); frame.srcdoc = '

child

'; + (document.body || document.documentElement || document).appendChild(frame); 'ready' + "#, + ) + .unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + vm.eval( + r#" + globalThis.child = document.querySelector('iframe').contentWindow; + history.replaceState('top0', ''); child.history.replaceState('child0', ''); + child.history.pushState('child1', ''); history.pushState('top1', ''); + child.history.pushState('child2', ''); child.navigation.back(); 'queued' + "#, + ) + .unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + assert_eq!( + vm.eval("[history.state, child.history.state, history.length].join('|')") + .unwrap(), + "top1|child1|4" + ); + assert_eq!( + vm.eval("history.pushState('top2', ''); [history.length, child.history.length].join('|')") + .unwrap(), + "4|4" + ); + // A History traversal can change more than one Document's entry; all live + // views must be updated before observers see the committed state. + vm.eval("history.go(-2); 'queued'").unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + assert_eq!( + vm.eval("[history.state, child.history.state, history.length].join('|')") + .unwrap(), + "top0|child1|4" + ); + vm.eval(r#" + history.pushState('top3', ''); + globalThis.jointTraversalResult = 'pending'; + child.navigation.traverseTo(child.navigation.entries()[0].key).finished.then(entry => { + jointTraversalResult = entry === child.navigation.currentEntry ? 'child-entry' : 'wrong-entry'; + }); 'queued' + "#).unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + assert_eq!( + vm.eval("[history.state, child.history.state, jointTraversalResult].join('|')") + .unwrap(), + "top0|child0|child-entry" + ); +} + +#[tokio::test] +async fn joint_history_pending_cursor_is_shared_and_detached_steps_remain_traversable() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let mut vm = + new_storage_page_task_executor_test_vm_with_loader("https://joint-history.test/", &loader); + vm.eval( + r#" + for (const id of ['a', 'b']) { + const f = document.createElement('iframe'); f.id = id; f.srcdoc = '

child

'; + (document.body || document.documentElement || document).append(f); + } + 'ready' + "#, + ) + .unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + vm.eval( + r#" + globalThis.a = document.getElementById('a').contentWindow; + globalThis.b = document.getElementById('b').contentWindow; + a.history.replaceState('a0', ''); b.history.replaceState('b0', ''); + a.history.pushState('a1', ''); b.history.pushState('b1', ''); a.history.pushState('a2', ''); + globalThis.childDocuments = [a.document, b.document]; + history.replaceState('replaced', '', '#replaced'); + history.back(); a.history.back(); 'queued' + "#, + ) + .unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + assert_eq!( + vm.eval("[a.history.state, b.history.state, history.length, a.document === childDocuments[0], b.document === childDocuments[1]].join('|')") + .unwrap(), + "a1|b0|4|true|true" + ); + vm.eval("document.getElementById('a').remove(); document.getElementById('b').remove(); globalThis.pops=0; onpopstate=()=>pops++; history.back(); 'queued'").unwrap(); + vm.drain_ready_page_task_executor_turns_for_setup(&loader, 128) + .await + .unwrap(); + assert_eq!( + vm.eval("history.pushState('branch', ''); [history.length, pops].join('|')") + .unwrap(), + "2|0" + ); +} + #[tokio::test] async fn child_history_push_preserves_existing_top_history_length() { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");