mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 00:01:34 +00:00
fix(history): coordinate complete joint traversal plans
This commit is contained in:
@@ -310,7 +310,10 @@ 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;
|
||||
seed.session_history = crate::SessionHistorySeed {
|
||||
commit: crate::SessionHistoryCommit::Push,
|
||||
..Default::default()
|
||||
};
|
||||
let next_index = seed.current_index + 1;
|
||||
let current_navigation_index = seed
|
||||
.entries
|
||||
@@ -368,7 +371,10 @@ 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;
|
||||
seed.session_history = crate::SessionHistorySeed {
|
||||
commit: crate::SessionHistoryCommit::Replace,
|
||||
..Default::default()
|
||||
};
|
||||
let current_index = seed.current_index;
|
||||
let current_navigation_index = seed
|
||||
.entries
|
||||
@@ -412,7 +418,10 @@ 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;
|
||||
seed.session_history = crate::SessionHistorySeed {
|
||||
commit: crate::SessionHistoryCommit::Replace,
|
||||
..Default::default()
|
||||
};
|
||||
let current_index = seed.current_index;
|
||||
let Some(previous_entry) = seed
|
||||
.entries
|
||||
|
||||
@@ -94,6 +94,9 @@ pub struct SessionHistorySeed {
|
||||
pub commit: SessionHistoryCommit,
|
||||
pub traversable: Option<Box<JointSessionHistory>>,
|
||||
pub target_step: Option<SessionHistoryStepId>,
|
||||
/// A cross-Document participant of an already accepted joint traversal.
|
||||
/// Its load may materialize this entry, but may not move the shared cursor.
|
||||
pub admitted_entry: Option<SessionHistoryEntry>,
|
||||
}
|
||||
|
||||
/// One already-committed traversable mutation, published in renderer FIFO
|
||||
|
||||
@@ -2390,6 +2390,15 @@ impl CdpConnection {
|
||||
.owner_state
|
||||
.navigation_history_state
|
||||
.snapshot();
|
||||
if (index, entries.len()) != (update.position.index(), update.position.length()) {
|
||||
tracing::error!(
|
||||
browser_index = index,
|
||||
browser_length = entries.len(),
|
||||
renderer_index = update.position.index(),
|
||||
renderer_length = update.position.length(),
|
||||
"browser and renderer joint history positions diverged"
|
||||
);
|
||||
}
|
||||
debug_assert_eq!(
|
||||
(index, entries.len()),
|
||||
(update.position.index(), update.position.length()),
|
||||
|
||||
@@ -484,6 +484,430 @@ async fn joint_session_history_syncs_child_steps_cursor_and_reload_bootstrap() {
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_cross_document_before_same_document() {
|
||||
assert_joint_history_multi_frame_traversal("a", false, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_same_document_before_cross_document() {
|
||||
assert_joint_history_multi_frame_traversal("b", false, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_schedules_both_cross_document_frames() {
|
||||
assert_joint_history_multi_frame_traversal("a", true, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_reentrant_admission_cancels_every_participant() {
|
||||
for cross in ["a", "b"] {
|
||||
assert_joint_history_multi_frame_traversal(cross, false, true).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_precommit_holds_every_participant_and_browser_cursor() {
|
||||
assert_joint_history_precommit("resolve").await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_precommit_rejection_aborts_the_whole_step() {
|
||||
assert_joint_history_precommit("reject").await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_precommit_cannot_restore_a_pruned_step() {
|
||||
assert_joint_history_precommit("prune").await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_late_response_cannot_overwrite_a_successor_entry() {
|
||||
assert_joint_history_late_response(false).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn joint_history_traversal_outgoing_state_update_keeps_the_accepted_destination() {
|
||||
assert_joint_history_late_response(true).await;
|
||||
}
|
||||
|
||||
async fn assert_joint_history_late_response(update_state: bool) {
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
let requests = Arc::new(AtomicUsize::new(0));
|
||||
let release = Arc::new(tokio::sync::Notify::new());
|
||||
let handler_requests = requests.clone();
|
||||
let handler_release = release.clone();
|
||||
let app = axum::Router::new().route(
|
||||
"/{page}",
|
||||
axum::routing::get(
|
||||
move |axum::extract::Path(name): axum::extract::Path<String>| {
|
||||
let requests = handler_requests.clone();
|
||||
let release = handler_release.clone();
|
||||
async move {
|
||||
if name == "a0" && requests.fetch_add(1, Ordering::SeqCst) > 0 {
|
||||
release.notified().await;
|
||||
}
|
||||
(
|
||||
[("content-type", "text/html"), ("cache-control", "no-store")],
|
||||
format!("<!doctype html><body>{name}"),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
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, app).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":9343,"method":"Page.navigate","sessionId":"SID-1","params":{"url":format!("http://{addr}/top")}})).await;
|
||||
assert!(take_response_by_id(&mut ctx, 9343)["error"].is_null());
|
||||
wait_until_message(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"late traversal fixture load",
|
||||
|message| message["method"] == "Page.domContentEventFired",
|
||||
)
|
||||
.await;
|
||||
joint_history_test_evaluate(&mut ctx, r#"(async()=>{
|
||||
history.replaceState('top0','');
|
||||
globalThis.frame=document.createElement('iframe');frame.src='/a0';
|
||||
await new Promise(resolve=>{frame.onload=resolve;document.body.append(frame)});
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
frame.contentWindow.history.replaceState('a0','');
|
||||
await new Promise(resolve=>{frame.onload=resolve;frame.contentWindow.location.assign('/a1')});
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
frame.contentWindow.history.replaceState('a1','');history.pushState('top1','');
|
||||
globalThis.loads=0;
|
||||
globalThis.frameLoaded=new Promise(resolve=>frame.onload=()=>{loads++;resolve(true)});
|
||||
globalThis.snapshot=()=>[history.state,frame.contentWindow.history.state,
|
||||
frame.contentWindow.location.pathname,frame.contentDocument.body.textContent,
|
||||
history.length,frame.contentWindow.history.length,loads];
|
||||
})()"#).await;
|
||||
ctx.process_async(json!({"id":9344,"method":"Network.enable","sessionId":"SID-1"}))
|
||||
.await;
|
||||
assert!(take_response_by_id(&mut ctx, 9344)["error"].is_null());
|
||||
ctx.sent.clear();
|
||||
joint_history_test_evaluate(
|
||||
&mut ctx,
|
||||
"new Promise(resolve=>{onpopstate=()=>resolve(true);history.go(-2)})",
|
||||
)
|
||||
.await;
|
||||
wait_until_messages(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"accepted child traversal reaches held response",
|
||||
|_| requests.load(Ordering::SeqCst) == 2,
|
||||
)
|
||||
.await;
|
||||
let mutation = if update_state {
|
||||
"frame.contentWindow.navigation.updateCurrentEntry({state:'updated'});snapshot()"
|
||||
} else {
|
||||
"frame.contentWindow.history.pushState('successor','');snapshot()"
|
||||
};
|
||||
let successor = joint_history_test_evaluate(&mut ctx, mutation).await;
|
||||
let expected = if update_state {
|
||||
json!(["top0", "a1", "/a1", "a1", 4, 4, 0])
|
||||
} else {
|
||||
json!(["top0", "successor", "/a1", "a1", 3, 3, 0])
|
||||
};
|
||||
assert_eq!(successor, expected);
|
||||
release.notify_one();
|
||||
wait_until_messages(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"held child response completes",
|
||||
|messages| {
|
||||
messages.iter().any(|request| {
|
||||
request["method"] == "Network.requestWillBeSent"
|
||||
&& request["params"]["request"]["url"] == format!("http://{addr}/a0")
|
||||
&& messages.iter().any(|done| {
|
||||
done["method"] == "Network.loadingFinished"
|
||||
&& done["params"]["requestId"] == request["params"]["requestId"]
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.await;
|
||||
if update_state {
|
||||
let after = joint_history_test_evaluate(&mut ctx, "frameLoaded.then(()=>snapshot())").await;
|
||||
assert_eq!(after, json!(["top0", "a0", "/a0", "a0", 4, 4, 1]));
|
||||
assert_eq!(
|
||||
joint_history_test_evaluate(
|
||||
&mut ctx,
|
||||
"frame.contentWindow.navigation.entries()[1].getState()"
|
||||
)
|
||||
.await,
|
||||
json!("updated")
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
joint_history_test_evaluate(&mut ctx, "snapshot()").await,
|
||||
successor
|
||||
);
|
||||
}
|
||||
ctx.process_async(json!({"id":9345,"method":"Page.getNavigationHistory","sessionId":"SID-1"}))
|
||||
.await;
|
||||
let browser = take_response_by_id(&mut ctx, 9345);
|
||||
assert_eq!(
|
||||
browser["result"]["currentIndex"],
|
||||
json!(if update_state { 1 } else { 2 }),
|
||||
"{browser}"
|
||||
);
|
||||
assert_eq!(
|
||||
browser["result"]["entries"].as_array().unwrap().len(),
|
||||
if update_state { 4 } else { 3 }
|
||||
);
|
||||
server.abort();
|
||||
}
|
||||
|
||||
async fn joint_history_test_evaluate(ctx: &mut TestContext, expression: &str) -> serde_json::Value {
|
||||
ctx.process_async(json!({"id":9340,"method":"Runtime.evaluate","sessionId":"SID-1","params":{"expression":expression,"awaitPromise":true,"returnByValue":true}})).await;
|
||||
wait_until_message(
|
||||
ctx,
|
||||
Some("SID-1"),
|
||||
"joint traversal script completes",
|
||||
|message| message["id"] == json!(9340),
|
||||
)
|
||||
.await;
|
||||
let response = take_response_by_id(ctx, 9340);
|
||||
assert!(
|
||||
response["error"].is_null() && response["result"]["exceptionDetails"].is_null(),
|
||||
"{response}"
|
||||
);
|
||||
response["result"]["result"]["value"].clone()
|
||||
}
|
||||
|
||||
async fn assert_joint_history_precommit(mode: &str) {
|
||||
async fn page(
|
||||
axum::extract::Path(name): axum::extract::Path<String>,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
(
|
||||
[(axum::http::header::CONTENT_TYPE.as_str(), "text/html")],
|
||||
format!("<!doctype html><body>{name}"),
|
||||
)
|
||||
}
|
||||
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":9339,"method":"Page.navigate","sessionId":"SID-1","params":{"url":format!("http://{addr}/top")}})).await;
|
||||
assert!(take_response_by_id(&mut ctx, 9339)["error"].is_null());
|
||||
wait_until_message(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"precommit test page load",
|
||||
|message| message["method"] == json!("Page.domContentEventFired"),
|
||||
)
|
||||
.await;
|
||||
let pending=joint_history_test_evaluate(&mut ctx,r#"(async()=>{
|
||||
history.replaceState('top0','');
|
||||
globalThis.frame=document.createElement('iframe');frame.src='/a0';
|
||||
await new Promise(resolve=>{frame.onload=resolve;document.body.append(frame)});
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
frame.contentWindow.history.replaceState('a0','');
|
||||
await new Promise(resolve=>{frame.onload=resolve;frame.contentWindow.location.assign('/a1')});
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
frame.contentWindow.history.replaceState('a1','');
|
||||
history.pushState('top1','');
|
||||
globalThis.pagehides=0;frame.contentWindow.onpagehide=()=>pagehides++;
|
||||
globalThis.snapshot=()=>[history.state,frame.contentWindow.history.state,
|
||||
frame.contentWindow.location.pathname,history.length,frame.contentWindow.history.length,pagehides];
|
||||
let admitted;const started=new Promise(resolve=>admitted=resolve);
|
||||
const gate=new Promise((resolve,reject)=>{globalThis.release=resolve;globalThis.block=reject});
|
||||
globalThis.failed=new Promise(resolve=>navigation.onnavigateerror=resolve);
|
||||
navigation.onnavigate=event=>{
|
||||
if(event.navigationType==='traverse')event.intercept({precommitHandler:()=>{admitted();return gate}});
|
||||
};
|
||||
history.go(-2);await started;return snapshot();
|
||||
})()"#).await;
|
||||
assert_eq!(pending, json!(["top1", "a1", "/a1", 4, 4, 0]), "{mode}");
|
||||
ctx.process_async(json!({"id":9341,"method":"Page.getNavigationHistory","sessionId":"SID-1"}))
|
||||
.await;
|
||||
let browser = take_response_by_id(&mut ctx, 9341);
|
||||
assert_eq!(
|
||||
browser["result"]["currentIndex"],
|
||||
json!(3),
|
||||
"{mode}: {browser}"
|
||||
);
|
||||
assert_eq!(browser["result"]["entries"].as_array().unwrap().len(), 4);
|
||||
if mode == "prune" {
|
||||
ctx.process_async(
|
||||
json!({"id":9342,"method":"Page.resetNavigationHistory","sessionId":"SID-1"}),
|
||||
)
|
||||
.await;
|
||||
assert!(take_response_by_id(&mut ctx, 9342)["error"].is_null());
|
||||
}
|
||||
let expression = match mode {
|
||||
"resolve" => {
|
||||
"new Promise(resolve=>{frame.onload=()=>setTimeout(()=>resolve(snapshot()),0);release()})"
|
||||
}
|
||||
"reject" => "block(new Error('blocked'));failed.then(()=>snapshot())",
|
||||
_ => "release();failed.then(()=>snapshot())",
|
||||
};
|
||||
let after = joint_history_test_evaluate(&mut ctx, expression).await;
|
||||
let (expected, index, length) = match mode {
|
||||
"resolve" => (json!(["top0", "a0", "/a0", 4, 4, 1]), 1, 4),
|
||||
"reject" => (json!(["top1", "a1", "/a1", 4, 4, 0]), 3, 4),
|
||||
_ => (json!(["top1", "a1", "/a1", 1, 1, 0]), 0, 1),
|
||||
};
|
||||
assert_eq!(after, expected, "{mode}");
|
||||
ctx.process_async(json!({"id":9341,"method":"Page.getNavigationHistory","sessionId":"SID-1"}))
|
||||
.await;
|
||||
let browser = take_response_by_id(&mut ctx, 9341);
|
||||
assert_eq!(
|
||||
browser["result"]["currentIndex"],
|
||||
json!(index),
|
||||
"{mode}: {browser}"
|
||||
);
|
||||
assert_eq!(
|
||||
browser["result"]["entries"].as_array().unwrap().len(),
|
||||
length
|
||||
);
|
||||
server.abort();
|
||||
}
|
||||
|
||||
async fn assert_joint_history_multi_frame_traversal(cross: &str, both_cross: bool, reenter: bool) {
|
||||
async fn page(
|
||||
axum::extract::Path(name): axum::extract::Path<String>,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
(
|
||||
[(axum::http::header::CONTENT_TYPE.as_str(), "text/html")],
|
||||
format!("<!doctype html><body data-page='{name}'>{name}"),
|
||||
)
|
||||
}
|
||||
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":9290,"method":"Page.navigate","sessionId":"SID-1","params":{"url":format!("http://{addr}/top")}})).await;
|
||||
assert!(take_response_by_id(&mut ctx, 9290)["error"].is_null());
|
||||
wait_until_message(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"mixed traversal page load",
|
||||
|message| message["method"] == json!("Page.domContentEventFired"),
|
||||
)
|
||||
.await;
|
||||
let expression = r#"(async () => {
|
||||
const cross = CROSS, bothCross = BOTH_CROSS, reenter = REENTER;
|
||||
const other = cross === 'a' ? 'b' : 'a';
|
||||
const task = () => new Promise(resolve => setTimeout(resolve, 0));
|
||||
for (const id of ['a', 'b']) {
|
||||
const frame = document.createElement('iframe'); frame.id = id; frame.src = '/'+id+'0';
|
||||
await new Promise(resolve => {frame.onload=resolve;document.body.append(frame)});
|
||||
await task();
|
||||
frame.contentWindow.history.replaceState(id+'0', '');
|
||||
}
|
||||
const frame = id => document.getElementById(id);
|
||||
const navigate = async id => {
|
||||
await new Promise(resolve => {frame(id).onload=resolve;frame(id).contentWindow.location.assign('/'+id+'1')});
|
||||
await task(); frame(id).contentWindow.history.replaceState(id+'1', '');
|
||||
};
|
||||
await navigate(cross);
|
||||
if (bothCross) await navigate(other);
|
||||
else frame(other).contentWindow.history.pushState(other+'1', '');
|
||||
let admitted;
|
||||
const admittedPromise = new Promise(resolve => admitted=resolve);
|
||||
if (reenter) frame(cross).contentWindow.navigation.addEventListener('navigate', event => {
|
||||
if (event.navigationType==='traverse') { history.pushState('from-admission',''); admitted(); }
|
||||
}, {once:true});
|
||||
globalThis.jointMove = delta => new Promise(resolve => {
|
||||
const completed = new Set();
|
||||
const done = id => { completed.add(id); if(completed.size===2) {clearTimeout(timer);resolve()} };
|
||||
const timer = setTimeout(()=>resolve(), 3000);
|
||||
frame(cross).onload=()=>done(cross);
|
||||
if (bothCross) frame(other).onload=()=>done(other);
|
||||
else frame(other).contentWindow.addEventListener('popstate',()=>done(other),{once:true});
|
||||
history.go(delta);
|
||||
});
|
||||
globalThis.jointSnapshot = () => ({
|
||||
pages: ['a','b'].map(id=>frame(id).contentDocument.body.dataset.page),
|
||||
states: ['a','b'].map(id=>frame(id).contentWindow.history.state),
|
||||
lengths: [history.length,...['a','b'].map(id=>frame(id).contentWindow.history.length)],
|
||||
indices: ['a','b'].map(id=>frame(id).contentWindow.navigation.currentEntry.index)
|
||||
});
|
||||
if (reenter) { history.go(-2); await admittedPromise; await task(); }
|
||||
else await jointMove(-2);
|
||||
await task();
|
||||
return jointSnapshot();
|
||||
})()"#.replace("BOTH_CROSS", if both_cross {"true"} else {"false"})
|
||||
.replace("REENTER", if reenter {"true"} else {"false"})
|
||||
.replace("CROSS", &format!("'{cross}'"));
|
||||
ctx.process_async(json!({"id":9291,"method":"Runtime.evaluate","sessionId":"SID-1","params":{"expression":expression,"awaitPromise":true,"returnByValue":true}})).await;
|
||||
wait_until_message(
|
||||
&mut ctx,
|
||||
Some("SID-1"),
|
||||
"mixed traversal settles",
|
||||
|message| message["id"] == json!(9291),
|
||||
)
|
||||
.await;
|
||||
let response = take_response_by_id(&mut ctx, 9291);
|
||||
let expected = if reenter {
|
||||
json!({"pages":if cross=="a" {vec!["a1","b0"]} else {vec!["a0","b1"]},"states":["a1","b1"],"lengths":[5,5,5],"indices":[1,1]})
|
||||
} else {
|
||||
json!({"pages":["a0","b0"],"states":["a0","b0"],"lengths":[4,4,4],"indices":[0,0]})
|
||||
};
|
||||
assert_eq!(
|
||||
response["result"]["result"]["value"], expected,
|
||||
"cross={cross}, both_cross={both_cross}, reenter={reenter}: {response}"
|
||||
);
|
||||
ctx.process_async(json!({"id":9292,"method":"Page.getNavigationHistory","sessionId":"SID-1"}))
|
||||
.await;
|
||||
let browser = take_response_by_id(&mut ctx, 9292);
|
||||
assert_eq!(
|
||||
browser["result"]["currentIndex"],
|
||||
json!(if reenter { 4 } else { 1 }),
|
||||
"{browser}"
|
||||
);
|
||||
assert_eq!(
|
||||
browser["result"]["entries"].as_array().unwrap().len(),
|
||||
if reenter { 5 } else { 4 }
|
||||
);
|
||||
if !reenter {
|
||||
let forward = joint_history_test_evaluate(&mut ctx, "jointMove(2).then(()=>new Promise(resolve=>setTimeout(()=>resolve(jointSnapshot()),0)))").await;
|
||||
let pages = if both_cross {
|
||||
vec!["a1", "b1"]
|
||||
} else if cross == "a" {
|
||||
vec!["a1", "b0"]
|
||||
} else {
|
||||
vec!["a0", "b1"]
|
||||
};
|
||||
assert_eq!(
|
||||
forward,
|
||||
json!({"pages":pages,"states":["a1","b1"],"lengths":[4,4,4],"indices":[1,1]}),
|
||||
"cross={cross}, both_cross={both_cross}"
|
||||
);
|
||||
ctx.process_async(
|
||||
json!({"id":9292,"method":"Page.getNavigationHistory","sessionId":"SID-1"}),
|
||||
)
|
||||
.await;
|
||||
let browser = take_response_by_id(&mut ctx, 9292);
|
||||
assert_eq!(browser["result"]["currentIndex"], json!(3), "{browser}");
|
||||
assert_eq!(browser["result"]["entries"].as_array().unwrap().len(), 4);
|
||||
}
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn navigation_bootstraps_browser_history_length_before_author_scripts() {
|
||||
async fn page() -> impl axum::response::IntoResponse {
|
||||
|
||||
@@ -56,6 +56,7 @@ mod navigation_entry_state;
|
||||
mod navigation_events;
|
||||
mod navigation_handler_callbacks;
|
||||
mod navigation_history_pruning;
|
||||
mod navigation_joint_traversal;
|
||||
mod navigation_lifecycle;
|
||||
mod navigation_mutation;
|
||||
mod navigation_projection;
|
||||
@@ -324,8 +325,8 @@ pub(crate) use self::navigation_events::dispatch_cross_document_navigation_navig
|
||||
pub(crate) use self::navigation_events::dispatch_srcdoc_navigation_navigate_event_for_window;
|
||||
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_navigation_entry_view_for_holder,
|
||||
commit_navigation_history_for_document, install_navigation_bootstrap_entry,
|
||||
install_navigation_bootstrap_entry_for_holder, 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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod admission;
|
||||
mod apply;
|
||||
pub(super) mod apply;
|
||||
mod results;
|
||||
mod traversal;
|
||||
pub(super) mod traversal;
|
||||
|
||||
pub(super) use self::admission::require_fully_active_history_owner;
|
||||
pub(super) use self::apply::apply_history_entry;
|
||||
|
||||
@@ -33,7 +33,12 @@ 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) struct PreparedHistoryEntry<'s> {
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
location: v8::Local<'s, v8::Object>,
|
||||
applied: AppliedHistoryEntry<'s>,
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn apply_history_entry<'s>(
|
||||
@@ -98,34 +103,13 @@ pub(in crate::context_bootstrap) fn apply_history_entry_commit<'s>(
|
||||
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);
|
||||
// The multi-participant coordinator admits and commits complete plans.
|
||||
// This path is only the specialized single same-Document executor.
|
||||
if targets
|
||||
.iter()
|
||||
.any(|target| !target.history.strict_equals(history.into()))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Admission can run script, so validate the stable step again before any
|
||||
// local entry is changed. Forward pruning invalidates it atomically.
|
||||
@@ -146,20 +130,14 @@ pub(in crate::context_bootstrap) fn apply_history_entry_commit<'s>(
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let prepared = prepare_local_history_entry_commit(scope, history, index)?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
let applied = commit_prepared_history_entry(scope, prepared);
|
||||
if let Some(delta) = delta.filter(|delta| *delta != 0) {
|
||||
super::super::session_history::publish(
|
||||
scope,
|
||||
@@ -167,19 +145,15 @@ pub(in crate::context_bootstrap) fn apply_history_entry_commit<'s>(
|
||||
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>(
|
||||
/// Resolve every fallible input before any participant changes its live view.
|
||||
pub(in crate::context_bootstrap) fn prepare_local_history_entry_commit<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
index: u32,
|
||||
) -> Option<AppliedHistoryEntry<'s>> {
|
||||
) -> Option<PreparedHistoryEntry<'s>> {
|
||||
let owner = runtime_window_owner(scope, history);
|
||||
let previous_history_index = history_index(scope, history);
|
||||
let previous_entry = navigation_current_entry(scope, owner);
|
||||
@@ -192,41 +166,51 @@ fn apply_local_history_entry_commit<'s>(
|
||||
let state = super::super::navigation_entry_state::clone_history_entry_state(scope, entry)
|
||||
.unwrap_or_else(|| v8::null(scope).into());
|
||||
let url = navigation_entry_url_value(scope, entry).unwrap_or_else(|| "about:blank".to_owned());
|
||||
set_history_index(scope, history, index);
|
||||
set_history_state(scope, history, state);
|
||||
|
||||
let location = window_location_for_holder(scope, owner)?;
|
||||
sync_location_object(scope, location, &url);
|
||||
let Ok(parsed_url) = url::Url::parse(&url) else {
|
||||
return None;
|
||||
};
|
||||
sync_navigation_current_entry_from_history_entry(scope, owner, entry);
|
||||
let resolved_entry = navigation_current_entry(scope, owner)
|
||||
.map(v8::Local::<v8::Value>::from)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
sync_child_navigation_entry_seed_from_owner(scope, owner);
|
||||
Some(AppliedHistoryEntry {
|
||||
owner,
|
||||
state,
|
||||
old_url,
|
||||
url,
|
||||
parsed_url,
|
||||
resolved_entry,
|
||||
previous_history_index,
|
||||
history_index: index,
|
||||
entry,
|
||||
previous_entry,
|
||||
additional: Vec::new(),
|
||||
let parsed_url = url::Url::parse(&url).ok()?;
|
||||
Some(PreparedHistoryEntry {
|
||||
history,
|
||||
location,
|
||||
applied: AppliedHistoryEntry {
|
||||
owner,
|
||||
state,
|
||||
old_url,
|
||||
url,
|
||||
parsed_url,
|
||||
resolved_entry: v8::undefined(scope).into(),
|
||||
previous_history_index,
|
||||
history_index: index,
|
||||
entry,
|
||||
previous_entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// No author callbacks or fallible lookups are allowed in the commit phase.
|
||||
pub(in crate::context_bootstrap) fn commit_prepared_history_entry<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
prepared: PreparedHistoryEntry<'s>,
|
||||
) -> AppliedHistoryEntry<'s> {
|
||||
let PreparedHistoryEntry {
|
||||
history,
|
||||
location,
|
||||
mut applied,
|
||||
} = prepared;
|
||||
set_history_index(scope, history, applied.history_index);
|
||||
set_history_state(scope, history, applied.state);
|
||||
sync_location_object(scope, location, &applied.url);
|
||||
sync_navigation_current_entry_from_history_entry(scope, applied.owner, applied.entry);
|
||||
applied.resolved_entry = navigation_current_entry(scope, applied.owner)
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
sync_child_navigation_entry_seed_from_owner(scope, applied.owner);
|
||||
applied
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -244,9 +228,6 @@ 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;
|
||||
|
||||
@@ -236,21 +236,43 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal(
|
||||
host: &mut JsContextHost,
|
||||
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));
|
||||
if let Some(step) = traversal.joint_step {
|
||||
let Some(owner) = history_traversal_target_window(scope, host, traversal.target) else {
|
||||
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
reject_pending_navigation_results(scope, &traversal.results, error);
|
||||
return;
|
||||
};
|
||||
let Some(plan) = super::super::navigation_traversal_plan::JointTraversalPlan::resolve(
|
||||
scope, owner, step,
|
||||
) else {
|
||||
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
reject_pending_navigation_results(scope, &traversal.results, error);
|
||||
return;
|
||||
};
|
||||
if super::super::navigation_joint_traversal::requires_joint_execution(scope, &plan) {
|
||||
let info = traversal
|
||||
.info
|
||||
.as_ref()
|
||||
.map(|info| v8::Local::new(scope, info));
|
||||
super::super::navigation_joint_traversal::execute(
|
||||
scope,
|
||||
plan,
|
||||
info,
|
||||
&traversal.results,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if traversal.results.is_empty()
|
||||
&& let [target] = plan.targets.as_slice()
|
||||
&& 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)
|
||||
@@ -466,6 +488,86 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal(
|
||||
resolve_pending_navigation_results(scope, results, resolved_entry);
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn prepare_joint_history_participant<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
applied: &super::apply::AppliedHistoryEntry<'s>,
|
||||
outcome: &NavigationDispatchOutcome<'s>,
|
||||
finished_resolvers: v8::Local<'s, v8::Array>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if !outcome.intercepted {
|
||||
return None;
|
||||
}
|
||||
let navigation = window_navigation_for_holder(scope, applied.owner)?;
|
||||
Some(set_active_traversal_intercept_settlement(
|
||||
scope,
|
||||
navigation,
|
||||
outcome.signal,
|
||||
finished_resolvers,
|
||||
applied.resolved_entry,
|
||||
&applied.url,
|
||||
))
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn finish_joint_history_participant<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
applied: &super::apply::AppliedHistoryEntry<'s>,
|
||||
outcome: NavigationDispatchOutcome<'s>,
|
||||
finished_resolvers: v8::Local<'s, v8::Array>,
|
||||
settlement: Option<v8::Local<'s, v8::Object>>,
|
||||
) {
|
||||
if settlement.is_some_and(|data| !traversal_intercept_is_active(scope, data.into())) {
|
||||
return;
|
||||
}
|
||||
let Some(navigation) = window_navigation_for_holder(scope, applied.owner) else {
|
||||
dispatch_history_entry_post_commit_events(scope, applied, true);
|
||||
resolve_resolver_array(scope, finished_resolvers, applied.resolved_entry);
|
||||
return;
|
||||
};
|
||||
let (error, result) = if outcome.intercepted {
|
||||
if let Some(event) = outcome.precommit_event {
|
||||
run_navigation_precommit_deferred_handlers(scope, event)
|
||||
} else {
|
||||
(outcome.intercept_error, outcome.intercept_result)
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
suppress_intercept_result_unhandled_rejection(scope, result);
|
||||
dispatch_history_entry_post_commit_events(scope, applied, true);
|
||||
if let Some(data) = settlement {
|
||||
if !traversal_intercept_is_active(scope, data.into()) {
|
||||
return;
|
||||
}
|
||||
set_traversal_intercept_inactive(scope, navigation, data.into());
|
||||
}
|
||||
if let Some(error) = error {
|
||||
finish_navigation_error_events(scope, navigation, error, &applied.url);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
if let Some(signal) = outcome.signal
|
||||
&& let Some(host) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host }.abort_signal(scope, signal, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(result) = result
|
||||
&& queue_pending_traversal_intercept_settlement(
|
||||
scope,
|
||||
navigation,
|
||||
outcome.signal,
|
||||
finished_resolvers,
|
||||
applied.resolved_entry,
|
||||
&applied.url,
|
||||
result,
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
perform_navigation_scroll_if_needed(scope, navigation, &applied.url, true);
|
||||
dispatch_navigation_success(scope, navigation);
|
||||
resolve_resolver_array(scope, finished_resolvers, applied.resolved_entry);
|
||||
}
|
||||
|
||||
fn history_traversal_target_window<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
host: &mut JsContextHost,
|
||||
@@ -567,7 +669,7 @@ fn queue_pending_precommit_history_traversal<'s>(
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn pending_result_resolver_arrays<'s>(
|
||||
pub(in crate::context_bootstrap) fn pending_result_resolver_arrays<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
results: &[crate::native_bridge::PendingNavigationResult],
|
||||
) -> (v8::Local<'s, v8::Array>, v8::Local<'s, v8::Array>) {
|
||||
@@ -666,6 +768,9 @@ pub(in crate::context_bootstrap) fn cancel_pending_precommit_history_traversal<'
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
if super::super::navigation_joint_traversal::cancel_pending(scope, navigation) {
|
||||
return true;
|
||||
}
|
||||
let Some(data) = navigation_pending_traversal_precommit(scope, navigation)
|
||||
.filter(|data| traversal_precommit_is_active(scope, (*data).into()))
|
||||
else {
|
||||
@@ -1047,7 +1152,7 @@ fn traversal_intercept_rejected_callback<'s>(
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
}
|
||||
|
||||
fn resolve_resolver_array<'s>(
|
||||
pub(in crate::context_bootstrap) fn resolve_resolver_array<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
resolvers: v8::Local<'s, v8::Array>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
@@ -1064,7 +1169,7 @@ fn resolve_resolver_array<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_resolver_array<'s>(
|
||||
pub(in crate::context_bootstrap) fn reject_resolver_array<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
resolvers: v8::Local<'s, v8::Array>,
|
||||
error: v8::Local<'s, v8::Value>,
|
||||
|
||||
@@ -110,7 +110,6 @@ 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);
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
//! A joint step is admitted as a unit. No participant may independently move
|
||||
//! the shared cursor while its siblings are still running navigation script.
|
||||
|
||||
use super::history_runtime::{apply, traversal};
|
||||
use super::navigation_entry::{history_entries, history_index, navigation_current_entry};
|
||||
use super::navigation_events::{
|
||||
NavigationDispatchOutcome, dispatch_beforeunload_for_runtime_owner,
|
||||
dispatch_navigation_traverse_event_with_outcome, dispatch_pagehide_for_runtime_owner,
|
||||
dispatch_unload_for_runtime_owner,
|
||||
};
|
||||
use super::navigation_result::navigation_dom_exception;
|
||||
use super::navigation_seed::history_entry_seed_for_traversal;
|
||||
use super::navigation_traversal_execution::{
|
||||
TraversalTarget, queue_history_traversal_without_result,
|
||||
};
|
||||
use super::navigation_traversal_plan::JointTraversalPlan;
|
||||
use super::navigation_window::{
|
||||
child_browsing_context_handle_for_runtime_owner, navigation_document_has_opaque_origin,
|
||||
navigation_document_is_active, window_history_for_holder, window_navigation_for_holder,
|
||||
};
|
||||
use crate::native_bridge::PendingNavigationResult;
|
||||
use crate::util::{context_host_ptr_from_global_bridge, get_private_value, set_private_value};
|
||||
use moli_page_types::SessionHistoryStepId;
|
||||
|
||||
const PENDING: &str = "__lmPendingJointTraversal";
|
||||
const ACTIVE: &str = "__lmJointActive";
|
||||
const OWNER: &str = "__lmJointOwner";
|
||||
const STEP: &str = "__lmJointStep";
|
||||
const PARTICIPANTS: &str = "__lmJointParticipants";
|
||||
const REMAINING: &str = "__lmJointRemaining";
|
||||
const COMMITTED: &str = "__lmJointCommitted";
|
||||
const FINISHED: &str = "__lmJointFinished";
|
||||
const HISTORY: &str = "__lmJointHistory";
|
||||
const SOURCE: &str = "__lmJointSource";
|
||||
const DESTINATION: &str = "__lmJointDestination";
|
||||
const NAVIGATION: &str = "__lmJointNavigation";
|
||||
const SIGNAL: &str = "__lmJointSignal";
|
||||
const EVENT: &str = "__lmJointEvent";
|
||||
const INTERCEPTED: &str = "__lmJointIntercepted";
|
||||
const INTERCEPT_RESULT: &str = "__lmJointInterceptResult";
|
||||
const INTERCEPT_ERROR: &str = "__lmJointInterceptError";
|
||||
|
||||
pub(super) fn queue_plan<'s>(scope: &mut v8::PinScope<'s, '_>, mut plan: JointTraversalPlan<'s>) {
|
||||
// A root Document replacement owns the whole descendant tree and must be
|
||||
// performed by its browser/popup loader. Otherwise queue the step on its caller, never
|
||||
// select one changing frame as a substitute for the joint operation.
|
||||
if let Some(index) = plan.cross_document_root_index(scope) {
|
||||
queue_history_traversal_without_result(scope, plan.targets.remove(index));
|
||||
return;
|
||||
}
|
||||
let Some(history) = window_history_for_holder(scope, plan.owner) else {
|
||||
return;
|
||||
};
|
||||
let index = history_index(scope, history);
|
||||
queue_history_traversal_without_result(
|
||||
scope,
|
||||
TraversalTarget {
|
||||
owner: plan.owner,
|
||||
history,
|
||||
current_index: index,
|
||||
target_index: index,
|
||||
joint_step: Some(plan.step),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn requires_joint_execution<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
plan: &JointTraversalPlan<'s>,
|
||||
) -> bool {
|
||||
match plan.targets.as_slice() {
|
||||
[target] => history_entry_seed_for_traversal(
|
||||
scope,
|
||||
target.owner,
|
||||
target.current_index,
|
||||
target.target_index,
|
||||
)
|
||||
.is_some(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Object>,
|
||||
key: &str,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
get_private_value(scope, data, key).and_then(|value| value.try_into().ok())
|
||||
}
|
||||
|
||||
fn array<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Object>,
|
||||
key: &str,
|
||||
) -> Option<v8::Local<'s, v8::Array>> {
|
||||
get_private_value(scope, data, key).and_then(|value| value.try_into().ok())
|
||||
}
|
||||
|
||||
fn active<'s>(scope: &mut v8::PinScope<'s, '_>, data: v8::Local<'s, v8::Object>) -> bool {
|
||||
get_private_value(scope, data, ACTIVE).is_some_and(|value| value.is_true())
|
||||
}
|
||||
|
||||
fn entry_signature<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
index: u32,
|
||||
) -> Option<String> {
|
||||
let entry = history_entries(scope, history)?
|
||||
.get_index(scope, index)?
|
||||
.try_into()
|
||||
.ok()?;
|
||||
let entry = super::session_history::entry_reference(scope, entry)?;
|
||||
serde_json::to_string(&(entry.key.as_str(), entry.document.as_str())).ok()
|
||||
}
|
||||
|
||||
fn signature_matches<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Object>,
|
||||
key: &str,
|
||||
signature: Option<String>,
|
||||
) -> bool {
|
||||
let stored = get_private_value(scope, data, key)
|
||||
.and_then(|value| value.to_string(scope))
|
||||
.map(|value| value.to_rust_string_lossy(scope));
|
||||
stored.is_some() && stored == signature
|
||||
}
|
||||
|
||||
/// All participants and their source/destination identities are captured
|
||||
/// before the first callback. Precommit promises delay the whole operation.
|
||||
pub(super) fn execute<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
plan: JointTraversalPlan<'s>,
|
||||
info: Option<v8::Local<'s, v8::Value>>,
|
||||
results: &[PendingNavigationResult],
|
||||
) {
|
||||
let data = v8::Object::new(scope);
|
||||
set_private_value(scope, data, ACTIVE, v8::Boolean::new(scope, true).into());
|
||||
set_private_value(scope, data, OWNER, plan.owner.into());
|
||||
set_private_value(
|
||||
scope,
|
||||
data,
|
||||
STEP,
|
||||
v8::BigInt::new_from_u64(scope, plan.step.raw()).into(),
|
||||
);
|
||||
let participants = v8::Array::new(scope, plan.targets.len() as i32);
|
||||
set_private_value(scope, data, PARTICIPANTS, participants.into());
|
||||
let (committed, finished) = traversal::pending_result_resolver_arrays(scope, results);
|
||||
set_private_value(scope, data, COMMITTED, committed.into());
|
||||
set_private_value(scope, data, FINISHED, finished.into());
|
||||
for (index, target) in plan.targets.iter().enumerate() {
|
||||
let participant = v8::Object::new(scope);
|
||||
let _ = participants.set_index(scope, index as u32, participant.into());
|
||||
set_private_value(scope, participant, HISTORY, target.history.into());
|
||||
if !navigation_document_has_opaque_origin(scope, target.owner)
|
||||
&& let Some(navigation) = window_navigation_for_holder(scope, target.owner)
|
||||
{
|
||||
set_private_value(scope, participant, NAVIGATION, navigation.into());
|
||||
}
|
||||
for (slot, index) in [
|
||||
(SOURCE, target.current_index),
|
||||
(DESTINATION, target.target_index),
|
||||
] {
|
||||
let Some(signature) = entry_signature(scope, target.history, index) else {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
};
|
||||
set_private_value(
|
||||
scope,
|
||||
participant,
|
||||
slot,
|
||||
v8::String::new(scope, &signature).unwrap().into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut precommit = Vec::new();
|
||||
for (index, target) in plan.targets.iter().enumerate() {
|
||||
let participant = participants
|
||||
.get_index(scope, index as u32)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
if history_entry_seed_for_traversal(
|
||||
scope,
|
||||
target.owner,
|
||||
target.current_index,
|
||||
target.target_index,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
dispatch_beforeunload_for_runtime_owner(scope, target.owner);
|
||||
}
|
||||
if validate(scope, data).is_none() {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
}
|
||||
let outcome = object(scope, participant, NAVIGATION)
|
||||
.map(|navigation| {
|
||||
dispatch_navigation_traverse_event_with_outcome(
|
||||
scope,
|
||||
navigation,
|
||||
target.history,
|
||||
target.target_index,
|
||||
info,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(NavigationDispatchOutcome::proceed);
|
||||
for value in [outcome.precommit_result, outcome.intercept_result]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if let Ok(promise) = v8::Local::<v8::Promise>::try_from(value) {
|
||||
super::navigation_result::suppress_unhandled_rejection(scope, promise);
|
||||
}
|
||||
}
|
||||
for (slot, value) in [
|
||||
(SIGNAL, outcome.signal.map(Into::into)),
|
||||
(EVENT, outcome.precommit_event.map(Into::into)),
|
||||
(INTERCEPT_RESULT, outcome.intercept_result),
|
||||
(INTERCEPT_ERROR, outcome.intercept_error),
|
||||
] {
|
||||
if let Some(value) = value {
|
||||
set_private_value(scope, participant, slot, value);
|
||||
}
|
||||
}
|
||||
set_private_value(
|
||||
scope,
|
||||
participant,
|
||||
INTERCEPTED,
|
||||
v8::Boolean::new(scope, outcome.intercepted).into(),
|
||||
);
|
||||
if !outcome.proceed
|
||||
|| outcome.abort_error.is_some()
|
||||
|| outcome.precommit_error.is_some()
|
||||
|| validate(scope, data).is_none()
|
||||
{
|
||||
super::navigation_events::mark_navigation_outcome_default_prevented(scope, &outcome);
|
||||
abort(scope, data, outcome.abort_error.or(outcome.precommit_error));
|
||||
return;
|
||||
}
|
||||
if let Some(promise) = outcome.precommit_result {
|
||||
precommit.push(promise);
|
||||
}
|
||||
}
|
||||
if precommit.is_empty() {
|
||||
commit(scope, data);
|
||||
return;
|
||||
}
|
||||
set_private_value(
|
||||
scope,
|
||||
data,
|
||||
REMAINING,
|
||||
v8::Integer::new_from_unsigned(scope, precommit.len() as u32).into(),
|
||||
);
|
||||
for index in 0..participants.length() {
|
||||
let participant = participants
|
||||
.get_index(scope, index)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
if let Some(navigation) = object(scope, participant, NAVIGATION) {
|
||||
cancel_pending(scope, navigation);
|
||||
set_private_value(scope, navigation, PENDING, data.into());
|
||||
}
|
||||
}
|
||||
for value in precommit {
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
};
|
||||
let _ = resolver.resolve(scope, value);
|
||||
let promise = resolver.get_promise(scope);
|
||||
let fulfilled = v8::Function::builder(precommit_fulfilled)
|
||||
.data(data.into())
|
||||
.build(scope)
|
||||
.unwrap();
|
||||
let rejected = v8::Function::builder(precommit_rejected)
|
||||
.data(data.into())
|
||||
.build(scope)
|
||||
.unwrap();
|
||||
let _ = promise.then2(scope, fulfilled, rejected);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Object>,
|
||||
) -> Option<JointTraversalPlan<'s>> {
|
||||
if !active(scope, data) {
|
||||
return None;
|
||||
}
|
||||
let owner = object(scope, data, OWNER)?;
|
||||
if !navigation_document_is_active(scope, owner) {
|
||||
return None;
|
||||
}
|
||||
let step = get_private_value(scope, data, STEP)
|
||||
.and_then(|value| v8::Local::<v8::BigInt>::try_from(value).ok())?;
|
||||
let plan = JointTraversalPlan::resolve(
|
||||
scope,
|
||||
owner,
|
||||
SessionHistoryStepId::from_raw(step.u64_value().0),
|
||||
)?;
|
||||
let participants = array(scope, data, PARTICIPANTS)?;
|
||||
if participants.length() as usize != plan.targets.len() {
|
||||
return None;
|
||||
}
|
||||
for (index, target) in plan.targets.iter().enumerate() {
|
||||
let participant = participants
|
||||
.get_index(scope, index as u32)?
|
||||
.try_into()
|
||||
.ok()?;
|
||||
if !navigation_document_is_active(scope, target.owner)
|
||||
|| !object(scope, participant, HISTORY)?.strict_equals(target.history.into())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let source = entry_signature(scope, target.history, target.current_index);
|
||||
let destination = entry_signature(scope, target.history, target.target_index);
|
||||
if !signature_matches(scope, participant, SOURCE, source)
|
||||
|| !signature_matches(scope, participant, DESTINATION, destination)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some(signal) = object(scope, participant, SIGNAL)
|
||||
&& let Some(host) = context_host_ptr_from_global_bridge(scope)
|
||||
&& unsafe { &mut *host }.abort_signal_aborted(scope, signal)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(plan)
|
||||
}
|
||||
|
||||
fn deactivate<'s>(scope: &mut v8::PinScope<'s, '_>, data: v8::Local<'s, v8::Object>) {
|
||||
set_private_value(scope, data, ACTIVE, v8::Boolean::new(scope, false).into());
|
||||
if let Some(participants) = array(scope, data, PARTICIPANTS) {
|
||||
for index in 0..participants.length() {
|
||||
let Some(participant) = participants
|
||||
.get_index(scope, index)
|
||||
.and_then(|value| value.try_into().ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(navigation) = object(scope, participant, NAVIGATION)
|
||||
&& object(scope, navigation, PENDING)
|
||||
.is_some_and(|pending| pending.strict_equals(data.into()))
|
||||
{
|
||||
set_private_value(scope, navigation, PENDING, v8::undefined(scope).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn abort<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Object>,
|
||||
error: Option<v8::Local<'s, v8::Value>>,
|
||||
) {
|
||||
if !active(scope, data) {
|
||||
return;
|
||||
}
|
||||
deactivate(scope, data);
|
||||
let error = error.unwrap_or_else(|| {
|
||||
navigation_dom_exception(scope, "Joint history traversal was canceled", "AbortError")
|
||||
});
|
||||
for slot in [COMMITTED, FINISHED] {
|
||||
if let Some(resolvers) = array(scope, data, slot) {
|
||||
traversal::reject_resolver_array(scope, resolvers, error, slot == FINISHED);
|
||||
}
|
||||
}
|
||||
if let Some(participants) = array(scope, data, PARTICIPANTS) {
|
||||
for index in 0..participants.length() {
|
||||
let Some(participant) = participants
|
||||
.get_index(scope, index)
|
||||
.and_then(|value| value.try_into().ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(signal) = object(scope, participant, SIGNAL)
|
||||
&& let Some(host) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host }.abort_signal(scope, signal, error);
|
||||
}
|
||||
if let Some(navigation) = object(scope, participant, NAVIGATION) {
|
||||
super::navigation_lifecycle::finish_navigation_error_events(
|
||||
scope, navigation, error, "",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn cancel_pending<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
let Some(data) = object(scope, navigation, PENDING) else {
|
||||
return false;
|
||||
};
|
||||
if !active(scope, data) {
|
||||
return false;
|
||||
}
|
||||
abort(scope, data, None);
|
||||
true
|
||||
}
|
||||
|
||||
fn precommit_fulfilled<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Ok(data) = v8::Local::<v8::Object>::try_from(args.data()) else {
|
||||
return;
|
||||
};
|
||||
if !active(scope, data) {
|
||||
return;
|
||||
}
|
||||
let Some(remaining) =
|
||||
get_private_value(scope, data, REMAINING).and_then(|value| value.uint32_value(scope))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if remaining <= 1 {
|
||||
commit(scope, data);
|
||||
} else {
|
||||
set_private_value(
|
||||
scope,
|
||||
data,
|
||||
REMAINING,
|
||||
v8::Integer::new_from_unsigned(scope, remaining - 1).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn precommit_rejected<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if let Ok(data) = v8::Local::<v8::Object>::try_from(args.data()) {
|
||||
abort(scope, data, Some(args.get(0)));
|
||||
}
|
||||
}
|
||||
|
||||
fn commit<'s>(scope: &mut v8::PinScope<'s, '_>, data: v8::Local<'s, v8::Object>) {
|
||||
let Some(plan) = validate(scope, data) else {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
};
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
};
|
||||
let mut cross_document = Vec::new();
|
||||
for target in &plan.targets {
|
||||
if let Some((url, mut seed)) = history_entry_seed_for_traversal(
|
||||
scope,
|
||||
target.owner,
|
||||
target.current_index,
|
||||
target.target_index,
|
||||
) {
|
||||
let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, target.owner)
|
||||
else {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
};
|
||||
seed.session_history.target_step = Some(plan.step);
|
||||
seed.session_history.admitted_entry = history_entries(scope, target.history)
|
||||
.and_then(|entries| entries.get_index(scope, target.target_index))
|
||||
.and_then(|entry| entry.try_into().ok())
|
||||
.and_then(|entry| super::session_history::entry_reference(scope, entry));
|
||||
cross_document.push((target.owner, handle, url, seed));
|
||||
}
|
||||
}
|
||||
// Unload is script too. Finish it for every participant and validate the
|
||||
// entire plan again before scheduling any replacement or changing a view.
|
||||
for (owner, _, _, _) in &cross_document {
|
||||
dispatch_pagehide_for_runtime_owner(scope, *owner);
|
||||
if validate(scope, data).is_none() {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
}
|
||||
dispatch_unload_for_runtime_owner(scope, *owner);
|
||||
if validate(scope, data).is_none() {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let mut prepared = Vec::new();
|
||||
for (index, target) in plan.targets.iter().enumerate() {
|
||||
if cross_document
|
||||
.iter()
|
||||
.any(|(owner, _, _, _)| owner.strict_equals(target.owner.into()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(entry) =
|
||||
apply::prepare_local_history_entry_commit(scope, target.history, target.target_index)
|
||||
else {
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
};
|
||||
prepared.push((index as u32, entry));
|
||||
}
|
||||
for (_, handle, url, seed) in &cross_document {
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
let _ =
|
||||
host.mark_current_child_document_unload_dispatched_after_navigation_traversal(*handle);
|
||||
if !host.queue_deferred_child_browsing_context_navigation_from_entry_seed(
|
||||
*handle,
|
||||
url.as_str(),
|
||||
seed.clone(),
|
||||
) {
|
||||
for (_, queued, _, _) in &cross_document {
|
||||
host.cancel_joint_child_history_navigation(*queued, plan.step);
|
||||
}
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Acceptance point: all admissions passed, all child navigations have
|
||||
// reservations, and same-Document views can commit without script or failure.
|
||||
let Some(delta) = super::session_history::commit_traversal(scope, plan.owner, plan.step) else {
|
||||
for (_, handle, _, _) in &cross_document {
|
||||
unsafe { &mut *host_ptr }.cancel_joint_child_history_navigation(*handle, plan.step);
|
||||
}
|
||||
abort(scope, data, None);
|
||||
return;
|
||||
};
|
||||
let applied = prepared
|
||||
.into_iter()
|
||||
.map(|(index, entry)| (index, apply::commit_prepared_history_entry(scope, entry)))
|
||||
.collect::<Vec<_>>();
|
||||
deactivate(scope, data);
|
||||
if delta != 0 {
|
||||
super::session_history::publish(
|
||||
scope,
|
||||
plan.owner,
|
||||
moli_page_types::SessionHistoryUpdateKind::Traverse { delta },
|
||||
);
|
||||
}
|
||||
let participants = array(scope, data, PARTICIPANTS).unwrap();
|
||||
let committed = array(scope, data, COMMITTED).unwrap();
|
||||
let finished = array(scope, data, FINISHED).unwrap();
|
||||
let resolved_entry = navigation_current_entry(scope, plan.owner)
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
let requester_replaces_document = cross_document
|
||||
.iter()
|
||||
.any(|(owner, _, _, _)| owner.strict_equals(plan.owner.into()));
|
||||
if !requester_replaces_document {
|
||||
traversal::resolve_resolver_array(scope, committed, resolved_entry);
|
||||
}
|
||||
let mut completions = Vec::new();
|
||||
for (index, entry) in &applied {
|
||||
let participant = participants
|
||||
.get_index(scope, *index)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let mut outcome = NavigationDispatchOutcome::proceed();
|
||||
outcome.intercepted =
|
||||
get_private_value(scope, participant, INTERCEPTED).is_some_and(|value| value.is_true());
|
||||
outcome.signal = object(scope, participant, SIGNAL);
|
||||
outcome.precommit_event = object(scope, participant, EVENT);
|
||||
outcome.intercept_result = get_private_value(scope, participant, INTERCEPT_RESULT);
|
||||
outcome.intercept_error = get_private_value(scope, participant, INTERCEPT_ERROR);
|
||||
let resolvers = if entry.owner.strict_equals(plan.owner.into()) {
|
||||
finished
|
||||
} else {
|
||||
v8::Array::new(scope, 0)
|
||||
};
|
||||
let settlement =
|
||||
traversal::prepare_joint_history_participant(scope, entry, &outcome, resolvers);
|
||||
completions.push((outcome, resolvers, settlement));
|
||||
}
|
||||
for (_, entry) in &applied {
|
||||
apply::dispatch_history_entry_currententrychange(scope, entry);
|
||||
}
|
||||
for ((_, entry), (outcome, resolvers, settlement)) in applied.iter().zip(completions) {
|
||||
traversal::finish_joint_history_participant(scope, entry, outcome, resolvers, settlement);
|
||||
}
|
||||
if !requester_replaces_document
|
||||
&& !applied
|
||||
.iter()
|
||||
.any(|(_, entry)| entry.owner.strict_equals(plan.owner.into()))
|
||||
{
|
||||
traversal::resolve_resolver_array(scope, finished, resolved_entry);
|
||||
}
|
||||
}
|
||||
@@ -27,16 +27,29 @@ pub(crate) fn install_navigation_bootstrap_entry_for_holder<'s>(
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
entry_seed: &NavigationHistoryEntrySeed,
|
||||
) {
|
||||
install_navigation_entry_view_for_holder(scope, owner, entry_seed, true);
|
||||
install_navigation_entry_view_for_holder(scope, owner, entry_seed);
|
||||
commit_navigation_history_for_document(scope, owner, entry_seed);
|
||||
}
|
||||
|
||||
/// A pending child navigation may expose its entry view before replacing the
|
||||
/// live Document. Only a committed seed advances the shared traversable.
|
||||
/// Document installation is the authoritative boundary, including commits
|
||||
/// that reuse an initial Window and therefore do not initialize a new realm.
|
||||
pub(crate) fn commit_navigation_history_for_document<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
entry_seed: &NavigationHistoryEntrySeed,
|
||||
) {
|
||||
super::session_history::initialize(scope, owner, entry_seed);
|
||||
super::session_history::restore(scope, owner, entry_seed);
|
||||
}
|
||||
|
||||
/// Refresh a Document's projection without mutating the shared traversable.
|
||||
/// During an accepted cross-Document traversal this can still be the outgoing
|
||||
/// Document's view. Only document bootstrap or an explicit history operation
|
||||
/// may commit a session history transition.
|
||||
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;
|
||||
@@ -89,9 +102,6 @@ pub(crate) fn install_navigation_entry_view_for_holder<'s>(
|
||||
});
|
||||
set_history_entries(scope, history, entries);
|
||||
set_history_index(scope, history, entry_seed.current_index);
|
||||
if committed {
|
||||
super::session_history::restore(scope, owner, entry_seed);
|
||||
}
|
||||
set_history_state(
|
||||
scope,
|
||||
history,
|
||||
|
||||
@@ -12,9 +12,7 @@ use super::navigation_callbacks::{
|
||||
cancel_pending_precommit_same_document_navigation,
|
||||
queue_pending_precommit_same_document_navigation, settle_intercepted_same_document_navigation,
|
||||
};
|
||||
use super::navigation_entry::{
|
||||
history_entries, history_index, navigation_current_entry, navigation_entries_share_document,
|
||||
};
|
||||
use super::navigation_entry::{history_entries, history_index, navigation_current_entry};
|
||||
use super::navigation_entry_state::{
|
||||
clone_navigation_entry_state, clone_navigation_state_arg_for_result,
|
||||
};
|
||||
@@ -31,17 +29,15 @@ use super::navigation_result::{
|
||||
navigation_rejected_dom_exception_result, navigation_rejected_invalid_state_result,
|
||||
navigation_rejected_value_result, navigation_result_with_pending_commit,
|
||||
};
|
||||
use super::navigation_traversal_execution::{
|
||||
queue_history_traversal_without_result, queue_navigation_traversal_with_result,
|
||||
};
|
||||
use super::navigation_traversal_execution::queue_navigation_traversal_with_result;
|
||||
use super::navigation_traversal_plan::{
|
||||
NavigationTraversalPlan, history_delta_traversal_target, navigation_delta_traversal_plan,
|
||||
NavigationTraversalPlan, history_delta_traversal_plan, navigation_delta_traversal_plan,
|
||||
navigation_index_traversal_plan,
|
||||
};
|
||||
use super::navigation_window::{
|
||||
navigation_document_can_update_current_entry, navigation_document_is_active,
|
||||
navigation_unload_event_active, runtime_window_is_global, runtime_window_owner,
|
||||
window_history_for_holder, window_location_for_holder, window_navigation_for_holder,
|
||||
navigation_unload_event_active, runtime_window_owner, window_history_for_holder,
|
||||
window_location_for_holder, window_navigation_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
@@ -530,11 +526,11 @@ fn history_traverse<'s>(
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
delta: i64,
|
||||
) {
|
||||
let Some(target) = history_delta_traversal_target(scope, history, delta) else {
|
||||
let Some(plan) = history_delta_traversal_plan(scope, history, delta) else {
|
||||
queue_browser_owned_top_level_history_traversal(scope, history, delta);
|
||||
return;
|
||||
};
|
||||
queue_history_traversal_without_result(scope, target);
|
||||
super::navigation_joint_traversal::queue_plan(scope, plan);
|
||||
}
|
||||
|
||||
fn queue_browser_owned_top_level_history_traversal<'s>(
|
||||
@@ -563,30 +559,13 @@ pub(crate) fn queue_top_level_history_traversal_by_delta(
|
||||
let Some(history) = window_history_for_holder(scope, global) else {
|
||||
return false;
|
||||
};
|
||||
let Some(target) = history_delta_traversal_target(scope, history, delta) else {
|
||||
let Some(plan) = history_delta_traversal_plan(scope, history, delta) else {
|
||||
return false;
|
||||
};
|
||||
let Some(entries) = history_entries(scope, target.history) else {
|
||||
return false;
|
||||
};
|
||||
let Some(current_entry) = entries
|
||||
.get_index(scope, target.current_index)
|
||||
.and_then(|entry| v8::Local::<v8::Object>::try_from(entry).ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(target_entry) = entries
|
||||
.get_index(scope, target.target_index)
|
||||
.and_then(|entry| v8::Local::<v8::Object>::try_from(entry).ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if runtime_window_is_global(scope, target.owner)
|
||||
&& !navigation_entries_share_document(scope, current_entry, target_entry)
|
||||
{
|
||||
if plan.has_cross_document_root(scope) {
|
||||
return false;
|
||||
}
|
||||
queue_history_traversal_without_result(scope, target);
|
||||
super::navigation_joint_traversal::queue_plan(scope, plan);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -355,6 +355,16 @@ pub(in crate::context_bootstrap) fn apply_pending_child_cross_document_traversal
|
||||
.info
|
||||
.as_ref()
|
||||
.map(|info| v8::Local::new(scope, info));
|
||||
if let Some(step) = traversal.seed.session_history.target_step {
|
||||
let Some(plan) =
|
||||
super::navigation_traversal_plan::JointTraversalPlan::resolve(scope, owner, step)
|
||||
else {
|
||||
reject_child_cross_document_traversal(scope, &traversal);
|
||||
return;
|
||||
};
|
||||
super::navigation_joint_traversal::execute(scope, plan, info, &traversal.results);
|
||||
return;
|
||||
}
|
||||
if !dispatch_child_cross_document_traverse_event(scope, &target, info) {
|
||||
reject_child_cross_document_traversal(scope, &traversal);
|
||||
return;
|
||||
|
||||
@@ -11,6 +11,48 @@ use super::navigation_window::{
|
||||
window_history_for_holder,
|
||||
};
|
||||
|
||||
pub(super) struct JointTraversalPlan<'s> {
|
||||
pub(super) step: moli_page_types::SessionHistoryStepId,
|
||||
pub(super) owner: v8::Local<'s, v8::Object>,
|
||||
pub(super) targets: Vec<TraversalTarget<'s>>,
|
||||
}
|
||||
|
||||
impl<'s> JointTraversalPlan<'s> {
|
||||
pub(super) fn resolve(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
step: moli_page_types::SessionHistoryStepId,
|
||||
) -> Option<Self> {
|
||||
Some(Self {
|
||||
step,
|
||||
owner,
|
||||
targets: super::session_history::targets_at(scope, owner, step)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn has_cross_document_root(&self, scope: &mut v8::PinScope<'s, '_>) -> bool {
|
||||
self.cross_document_root_index(scope).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn cross_document_root_index(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Option<usize> {
|
||||
self.targets.iter().position(|target| {
|
||||
(super::navigation_window::runtime_window_is_global(scope, target.owner)
|
||||
|| crate::native_bridge::lightweight_popup_id_from_window(scope, target.owner)
|
||||
.is_some())
|
||||
&& super::navigation_seed::history_entry_seed_for_traversal(
|
||||
scope,
|
||||
target.owner,
|
||||
target.current_index,
|
||||
target.target_index,
|
||||
)
|
||||
.is_some()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum NavigationTraversalPlan<'s> {
|
||||
RejectInvalidState(&'static str),
|
||||
ResolveCurrentEntry(v8::Local<'s, v8::Object>),
|
||||
@@ -115,11 +157,11 @@ pub(super) fn navigation_index_traversal_plan<'s>(
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn history_delta_traversal_target<'s>(
|
||||
pub(super) fn history_delta_traversal_plan<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
delta: i64,
|
||||
) -> Option<TraversalTarget<'s>> {
|
||||
) -> Option<JointTraversalPlan<'s>> {
|
||||
let owner = runtime_window_owner(scope, history);
|
||||
let host = unsafe { &mut *crate::util::context_host_ptr_from_global_bridge(scope)? };
|
||||
let binding = super::session_history::binding(scope, host, owner);
|
||||
@@ -128,16 +170,5 @@ pub(super) fn history_delta_traversal_target<'s>(
|
||||
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,
|
||||
history,
|
||||
current_index: history_index(scope, history),
|
||||
target_index: history_index(scope, history),
|
||||
joint_step: Some(step),
|
||||
}))
|
||||
JointTraversalPlan::resolve(scope, owner, step)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,9 @@ pub(super) fn restore<'s>(
|
||||
};
|
||||
// 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) {
|
||||
if history.entry(binding.context) == Some(&entry)
|
||||
|| seed.session_history.admitted_entry.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let update = match seed.session_history.commit {
|
||||
|
||||
@@ -42,6 +42,7 @@ fn configure_child_document_navigation_request(
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct PendingChildDocumentNavigation {
|
||||
pub(super) admitted_history_entry: Option<moli_page_types::SessionHistoryEntry>,
|
||||
pub(super) target: ChildDocumentNavigationFetchTarget,
|
||||
pub(super) target_url: Url,
|
||||
pub(super) resource_loader: crate::network::navigation::NavigationResourceLoader,
|
||||
|
||||
@@ -18,6 +18,24 @@ impl JsContextHost {
|
||||
if !self.child_browsing_contexts.contains_key(&handle) {
|
||||
return None;
|
||||
}
|
||||
if self
|
||||
.child_browsing_contexts
|
||||
.get(&handle)
|
||||
.and_then(|entry| entry.navigation_entry_seed().session_history.admitted_entry)
|
||||
.is_some_and(|entry| {
|
||||
!self
|
||||
.session_histories
|
||||
.entry_is_current(super::super::OwnerDispatchScope::Child(handle), &entry)
|
||||
})
|
||||
{
|
||||
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();
|
||||
}
|
||||
let _ =
|
||||
self.finish_child_frame_navigation_without_load_dispatch(handle, navigation_load);
|
||||
return Some(ChildDocumentCommitResult::ready(None));
|
||||
}
|
||||
let creation_kind = child_document_creation_kind_for_bootstrap(&bootstrap);
|
||||
if let Some(request_url) = child_document_frame_csp_request_url(&bootstrap) {
|
||||
if let Some(violation) =
|
||||
|
||||
@@ -381,6 +381,15 @@ impl JsContextHost {
|
||||
),
|
||||
resource_authority,
|
||||
);
|
||||
// A preserved initial LocalWindow may already have a history surface
|
||||
// without running realm bootstrap again. Commit its Document seed here;
|
||||
// subsequent view refreshes must never replay this history transition.
|
||||
if let Some(window) = self.existing_child_browsing_context_window_wrapper(scope, handle)
|
||||
&& let Some(entry) = self.child_browsing_contexts.get(&handle)
|
||||
{
|
||||
let seed = entry.committed_navigation_entry_seed();
|
||||
crate::context_bootstrap::commit_navigation_history_for_document(scope, window, &seed);
|
||||
}
|
||||
Some(owner_transition)
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,10 @@ impl JsContextHost {
|
||||
self.pending_child_document_navigations.insert(
|
||||
load_id,
|
||||
PendingChildDocumentNavigation {
|
||||
admitted_history_entry: self
|
||||
.child_browsing_contexts
|
||||
.get(&handle)
|
||||
.and_then(|entry| entry.navigation_entry_seed().session_history.admitted_entry),
|
||||
target,
|
||||
target_url: target_url.clone(),
|
||||
resource_loader: resource_loader.clone(),
|
||||
@@ -401,6 +405,37 @@ impl JsContextHost {
|
||||
handle,
|
||||
target.load_id(),
|
||||
);
|
||||
if pending
|
||||
.admitted_history_entry
|
||||
.as_ref()
|
||||
.is_some_and(|entry| {
|
||||
!self
|
||||
.session_histories
|
||||
.entry_is_current(super::super::OwnerDispatchScope::Child(handle), entry)
|
||||
})
|
||||
{
|
||||
// Author navigation while the response was in flight superseded
|
||||
// this participant. It cannot restore an old view or joint cursor.
|
||||
self.clear_child_browsing_context_pending_navigation(handle);
|
||||
if let Some(entry) = self.child_browsing_contexts.get_mut(&handle)
|
||||
&& entry.navigation_entry_seed().session_history.admitted_entry
|
||||
== pending.admitted_history_entry
|
||||
{
|
||||
entry.restore_navigation_entry_seed_from_committed();
|
||||
}
|
||||
self.clear_pending_service_worker_child_client_if_matches(
|
||||
handle,
|
||||
pending.reserved_service_worker_client_id,
|
||||
);
|
||||
let _ = self.finish_child_frame_navigation_without_load_dispatch(
|
||||
handle,
|
||||
target.navigation_load(),
|
||||
);
|
||||
return ChildDocumentLoadApplication::SupersededDuringApplication {
|
||||
completion: ChildDocumentLoadCompletion::new(target, network_attribution, result),
|
||||
body_activity: ChildDocumentLoadBodyActivity::NoPageCodeOrEventDispatch,
|
||||
};
|
||||
}
|
||||
let result = match result {
|
||||
Ok(ChildDocumentLoadOutcome::Loaded(mut loaded)) => {
|
||||
if self.bypass_content_security_policy() {
|
||||
|
||||
@@ -119,6 +119,25 @@ impl JsContextHost {
|
||||
self.queue_child_browsing_context_navigation_commit(handle)
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_joint_child_history_navigation(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
step: moli_page_types::SessionHistoryStepId,
|
||||
) {
|
||||
if self
|
||||
.child_browsing_contexts
|
||||
.get(&handle)
|
||||
.is_some_and(|entry| {
|
||||
entry.navigation_entry_seed().session_history.target_step == Some(step)
|
||||
})
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn queue_deferred_child_browsing_context_navigation_to_url(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
|
||||
@@ -94,12 +94,7 @@ impl JsContextHost {
|
||||
} else {
|
||||
sync_window_location_history_navigation_runtime_surface(scope, wrapper);
|
||||
}
|
||||
install_navigation_entry_view_for_holder(
|
||||
scope,
|
||||
wrapper,
|
||||
&visible_state.entry_seed,
|
||||
visible_state.seed_is_committed,
|
||||
);
|
||||
install_navigation_entry_view_for_holder(scope, wrapper, &visible_state.entry_seed);
|
||||
let performance_navigation_type = self.child_performance_navigation_type(handle);
|
||||
let _ = bind_window_performance_seed(
|
||||
scope,
|
||||
@@ -164,7 +159,7 @@ impl JsContextHost {
|
||||
wrapper,
|
||||
¤t_entry.url,
|
||||
);
|
||||
install_navigation_entry_view_for_holder(scope, wrapper, &entry_seed, false);
|
||||
install_navigation_entry_view_for_holder(scope, wrapper, &entry_seed);
|
||||
let performance_navigation_type = self.child_performance_navigation_type(handle);
|
||||
let _ = bind_window_performance_seed(
|
||||
scope,
|
||||
|
||||
@@ -693,7 +693,42 @@ impl ChildBrowsingContextEntry {
|
||||
let same_document_update = committed_current_document_id.is_none()
|
||||
|| committed_current_document_id == next_current_document_id
|
||||
|| self.pending_attribute_bootstrap_commit;
|
||||
self.navigation_entry_seed = entry_seed.clone();
|
||||
let updates_outgoing_document = same_document_update
|
||||
&& entry_seed.session_history.commit == moli_page_types::SessionHistoryCommit::Attach
|
||||
&& self
|
||||
.navigation_entry_seed
|
||||
.session_history
|
||||
.admitted_entry
|
||||
.is_some()
|
||||
&& committed_current_document_id
|
||||
!= child_navigation_current_document_id(&self.navigation_entry_seed);
|
||||
if updates_outgoing_document {
|
||||
// An accepted traversal owns the destination and activation. State
|
||||
// updates in the outgoing Document only refresh its retained entry;
|
||||
// they cannot replace the pending destination with the live cursor.
|
||||
if let Some(updated) = entry_seed
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.history_index == entry_seed.current_index)
|
||||
{
|
||||
let refresh = |entry: &mut NavigationHistorySerializedEntry| {
|
||||
if entry.key == updated.key && entry.document_id == updated.document_id {
|
||||
*entry = updated.clone();
|
||||
}
|
||||
};
|
||||
for entry in &mut self.navigation_entry_seed.entries {
|
||||
refresh(entry);
|
||||
}
|
||||
if let Some(activation) = &mut self.navigation_entry_seed.activation {
|
||||
refresh(&mut activation.entry);
|
||||
if let Some(from) = &mut activation.from {
|
||||
refresh(from);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.navigation_entry_seed = entry_seed.clone();
|
||||
}
|
||||
if same_document_update {
|
||||
self.committed_navigation_entry_seed = entry_seed;
|
||||
self.pending_attribute_bootstrap_commit = false;
|
||||
|
||||
@@ -12,6 +12,21 @@ pub(crate) struct RendererSessionHistories {
|
||||
}
|
||||
|
||||
impl RendererSessionHistories {
|
||||
pub(crate) fn entry_is_current(
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
entry: &moli_page_types::SessionHistoryEntry,
|
||||
) -> bool {
|
||||
let Some(context) = self.contexts.get(&owner).copied() else {
|
||||
return false;
|
||||
};
|
||||
self.main.entry(context) == Some(entry)
|
||||
|| self
|
||||
.popups
|
||||
.values()
|
||||
.any(|history| history.entry(context) == Some(entry))
|
||||
}
|
||||
|
||||
pub(crate) fn detach(&mut self, owner: OwnerDispatchScope) {
|
||||
if let Some(context) = self.contexts.remove(&owner) {
|
||||
self.main.detach(context);
|
||||
|
||||
Reference in New Issue
Block a user