mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
refactor(history): centralize joint session history state
Per-Window length counters cannot shrink after traversal and branching, and leave retained sibling History objects stale. Give each traversable one ordered history and cursor with stable step, browsing context, and Document identities. All History getters read that state; Navigation entries remain Document views. Route push, replace, traversal, and pruning through the shared model. Carry browser positions or renderer snapshots into new Documents and publish committed mutations through the Page owner-action FIFO. Remove cached length adjustments and DOM-order traversal guesses. Preserve srcdoc identity when only its parent's URL changes. Keep reset callback pushes in both renderer and browser history, and prune discarded sibling Navigation entries when forward steps are removed. Validation: cargo fmt --all; workspace all-targets/all-features Clippy with warnings denied; cargo nextest run --no-fail-fast (18,501 passed, 14 skipped); 12 targeted Chromium/Moli state and length comparisons; frontend differential 1,020/1,020 matches, all 5,259 observed frames matching, with zero retries.
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -140,6 +140,7 @@ pub struct NavigationHistoryEntrySeed {
|
||||
pub entries: Vec<NavigationHistorySerializedEntry>,
|
||||
pub current_index: u32,
|
||||
pub activation: Option<NavigationActivationSeed>,
|
||||
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<String>,
|
||||
navigation_state_json: Option<String>,
|
||||
) {
|
||||
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<String>,
|
||||
navigation_state_json: Option<String>,
|
||||
) {
|
||||
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 {
|
||||
|
||||
@@ -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<Self> {
|
||||
(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<Box<JointSessionHistory>>,
|
||||
pub target_step: Option<SessionHistoryStepId>,
|
||||
}
|
||||
|
||||
/// 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<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SessionHistoryStep {
|
||||
id: SessionHistoryStepId,
|
||||
entries: BTreeMap<SessionHistoryContextId, SessionHistoryEntry>,
|
||||
}
|
||||
|
||||
/// 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<SessionHistoryStep>,
|
||||
current: usize,
|
||||
parents: BTreeMap<SessionHistoryContextId, SessionHistoryContextId>,
|
||||
}
|
||||
|
||||
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<SessionHistoryContextId>,
|
||||
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<SessionHistoryStepId> {
|
||||
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<i64> {
|
||||
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<usize> {
|
||||
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<SessionHistoryStepId> {
|
||||
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<SessionHistoryContextId, SessionHistoryEntry>> {
|
||||
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<i64> {
|
||||
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<SessionHistoryContextId> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ pub(crate) struct TargetNavigationLoadInputs {
|
||||
(bool, Option<moli_core::page::SubresourceResourceType>),
|
||||
pub(crate) permission_overrides: Vec<moli_core::page::PermissionOverrideRegistration>,
|
||||
main_document_commit_seed: Option<RendererMainDocumentCommitSeed>,
|
||||
session_history_length: Option<usize>,
|
||||
session_history_position: Option<moli_page_types::SessionHistoryPosition>,
|
||||
}
|
||||
|
||||
impl TargetNavigationLoadInputs {
|
||||
@@ -199,7 +199,7 @@ impl TargetNavigationLoadInputs {
|
||||
) -> Option<RendererMainDocumentCommit> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<String> {
|
||||
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<bool> {
|
||||
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<String> {
|
||||
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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ impl RendererMainDocumentCommitSeed {
|
||||
security_origin,
|
||||
secure_context_type,
|
||||
timestamp: self.timestamp,
|
||||
session_history_length: None,
|
||||
session_history_position: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -531,6 +531,7 @@ enum PageOutputProjectionStep {
|
||||
DocumentLifecycle,
|
||||
ChildFrameActivity,
|
||||
SameDocumentNavigation,
|
||||
SessionHistoryUpdate,
|
||||
TopLevelLocationNavigation,
|
||||
TopLevelHistoryTraversal,
|
||||
}
|
||||
@@ -544,6 +545,11 @@ pub(crate) struct PagePreparedOutputs {
|
||||
document_lifecycle_events: Vec<RendererDocumentLifecycleEvent>,
|
||||
child_frame_activities: Vec<PagePreparedChildFrameActivity>,
|
||||
same_document_navigations: Vec<PagePreparedSameDocumentNavigation>,
|
||||
session_history_updates: Vec<(
|
||||
crate::conn::TargetPageResidenceIdentity,
|
||||
RendererDocumentLifecycleIdentity,
|
||||
moli_page_types::SessionHistoryUpdate,
|
||||
)>,
|
||||
top_level_location_navigation: Option<PagePreparedTopLevelLocationNavigation>,
|
||||
top_level_history_traversal: Option<RendererPendingTopLevelHistoryTraversal>,
|
||||
}
|
||||
@@ -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<BackgroundProtocolEvent>,
|
||||
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"),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")],
|
||||
"<!doctype html><body><script>globalThis.initialHistoryLength=history.length</script>",
|
||||
)
|
||||
}
|
||||
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 = '<p>child</p>';
|
||||
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"),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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::<v8::Object>::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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<v8::Local<'s, v8::Object>>,
|
||||
additional: Vec<AppliedHistoryEntry<'s>>,
|
||||
}
|
||||
|
||||
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<moli_page_types::SessionHistoryStepId>,
|
||||
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<moli_page_types::SessionHistoryStepId>,
|
||||
) -> Option<AppliedHistoryEntry<'s>> {
|
||||
let owner = runtime_window_owner(scope, history);
|
||||
let entry = history_entries(scope, history)?
|
||||
.get_index(scope, index)
|
||||
.and_then(|value| v8::Local::<v8::Object>::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::<v8::Object>::try_from(value).ok())?;
|
||||
let next = entries
|
||||
.get_index(scope, target.target_index)
|
||||
.and_then(|value| v8::Local::<v8::Object>::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::<v8::Object>::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<AppliedHistoryEntry<'s>> {
|
||||
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::<v8::Value>::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<v8::Local<'s, v8::Object>>,
|
||||
) {
|
||||
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::<v8::Object>::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<u32> {
|
||||
(0..entries.length()).find(|index| {
|
||||
entries
|
||||
.get_index(scope, *index)
|
||||
.and_then(|value| v8::Local::<v8::Object>::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);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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<v8::Local<'scope, v8::Object>>,
|
||||
|
||||
#[webapi(slot = TRAVERSAL_PRECOMMIT_JOINT_STEP_SLOT)]
|
||||
joint_step: Option<v8::Local<'scope, v8::Value>>,
|
||||
|
||||
#[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::<v8::Object>::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::<v8::Value>::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<moli_page_types::SessionHistoryStepId>,
|
||||
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::<v8::Object>::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::<v8::Object>::try_from(args.data())
|
||||
.ok()
|
||||
.and_then(|data| get_private_value(scope, data, TRAVERSAL_PRECOMMIT_JOINT_STEP_SLOT))
|
||||
.and_then(|value| v8::Local::<v8::BigInt>::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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<u32> {
|
||||
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<v8::Local<'s, v8::Value>> {
|
||||
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<f64> {
|
||||
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>,
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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<v8::Local<'s, v8::Object>>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<moli_page_types::SessionHistoryStepId>,
|
||||
}
|
||||
|
||||
pub(super) fn queue_navigation_traversal_with_result<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
target: TraversalTarget<'s>,
|
||||
mut target: TraversalTarget<'s>,
|
||||
info: Option<v8::Local<'s, v8::Value>>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
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<v8::Local<'s, v8::Object>> {
|
||||
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>,
|
||||
|
||||
@@ -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<TraversalTarget<'s>> {
|
||||
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),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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<crate::native_bridge::OwnerDispatchScope> {
|
||||
|
||||
@@ -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<u64>,
|
||||
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<SessionHistoryEntry> {
|
||||
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::<v8::Object>::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::<v8::Object>::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<u64>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
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<SessionHistoryStepId> {
|
||||
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<v8::Local<'s, v8::Object>> {
|
||||
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::<v8::Object>::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<Vec<super::navigation_traversal_execution::TraversalTarget<'s>>> {
|
||||
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::<v8::Object>::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<i64> {
|
||||
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::<v8::Object>::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,
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -62,7 +62,6 @@ pub(super) struct ChildBrowsingContextEntry {
|
||||
service_worker_client_id: Option<ServiceWorkerClientId>,
|
||||
pending_service_worker_client_id: Option<ServiceWorkerClientId>,
|
||||
pending_service_worker_client_navigation: Option<ServiceWorkerClientNavigateContinuation>,
|
||||
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 {
|
||||
|
||||
@@ -8,6 +8,8 @@ impl JsContextHost {
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
) -> Option<ChildBrowsingContextEntry> {
|
||||
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 {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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<layout_state::DocumentLayoutState>,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<u64, JointSessionHistory>,
|
||||
contexts: HashMap<OwnerDispatchScope, SessionHistoryContextId>,
|
||||
}
|
||||
|
||||
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<u64>) -> &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<u64>,
|
||||
) -> Option<OwnerDispatchScope> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ pub(crate) struct QueuedNavigationApiTask {
|
||||
}
|
||||
|
||||
pub(crate) struct PendingHistoryTraversal {
|
||||
pub(crate) joint_step: Option<moli_page_types::SessionHistoryStepId>,
|
||||
pub(crate) target: WindowTaskTarget,
|
||||
pub(crate) target_index: u32,
|
||||
pub(crate) target_key: Option<String>,
|
||||
@@ -134,6 +135,7 @@ impl HistoryQueueState {
|
||||
relevant_context: WindowExecutionContextBinding,
|
||||
target: WindowTaskTarget,
|
||||
target_index: u32,
|
||||
joint_step: Option<moli_page_types::SessionHistoryStepId>,
|
||||
target_key: Option<String>,
|
||||
info: Option<v8::Global<v8::Value>>,
|
||||
result: Option<PendingNavigationResult>,
|
||||
@@ -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<moli_page_types::SessionHistoryStepId> {
|
||||
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<moli_page_types::SessionHistoryStepId>,
|
||||
target_key: Option<String>,
|
||||
) -> Option<RendererPageHistoryTraversalProducer> {
|
||||
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<moli_page_types::SessionHistoryStepId>,
|
||||
target_key: Option<String>,
|
||||
info: Option<v8::Local<'s, v8::Value>>,
|
||||
) -> 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 {
|
||||
|
||||
@@ -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<usize>,
|
||||
/// Browser-owned session-history cursor and length at this document's commit.
|
||||
pub session_history_position: Option<moli_page_types::SessionHistoryPosition>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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::<usize>()),
|
||||
),
|
||||
RendererOwnerAction::TopLevelLocationNavigation(event) => string_charge(event.url())
|
||||
.saturating_add(string_charge(event.request_method()))
|
||||
.saturating_add(
|
||||
|
||||
@@ -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(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '<p>child</p>';
|
||||
(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 = '<p>child</p>';
|
||||
(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 = '<p>child</p>';
|
||||
(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");
|
||||
|
||||
Reference in New Issue
Block a user