mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 16:00:45 +00:00
fix(parser): defer document close while blocked
This commit is contained in:
@@ -354,15 +354,10 @@ impl DocumentRuntime {
|
||||
closed,
|
||||
"a live root parser must own an open document stream"
|
||||
);
|
||||
let close_transitioned = self
|
||||
.root_document_parser
|
||||
self.root_document_parser
|
||||
.as_mut()
|
||||
.expect("root document parser existence was checked")
|
||||
.request_close();
|
||||
debug_assert!(
|
||||
close_transitioned,
|
||||
"an open root document parser must accept its first close request"
|
||||
);
|
||||
}
|
||||
let _ = self.finish_root_document_parser_stream_if_ready(scope, host_ptr);
|
||||
true
|
||||
|
||||
@@ -177,6 +177,15 @@ pub(crate) enum DocumentParserLifetime {
|
||||
Closing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum DocumentParserCloseDisposition {
|
||||
/// The parser is ready to consume any queued input and reach EOF now.
|
||||
DrainNow,
|
||||
/// An active parser operation or blocker owns progress; closing resumes
|
||||
/// through that operation's existing completion path.
|
||||
DeferredUntilReady,
|
||||
}
|
||||
|
||||
static NEXT_DOCUMENT_PARSER_SESSION_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -664,12 +673,13 @@ impl DocumentParserSession {
|
||||
self.lifetime
|
||||
}
|
||||
|
||||
pub(crate) fn request_close(&mut self) -> bool {
|
||||
if self.lifetime == DocumentParserLifetime::Closing {
|
||||
return false;
|
||||
}
|
||||
pub(crate) fn request_close(&mut self) -> DocumentParserCloseDisposition {
|
||||
self.lifetime = DocumentParserLifetime::Closing;
|
||||
true
|
||||
if self.run_state() == DocumentParserRunState::Ready {
|
||||
DocumentParserCloseDisposition::DrainNow
|
||||
} else {
|
||||
DocumentParserCloseDisposition::DeferredUntilReady
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finishes_when_drained(&self) -> bool {
|
||||
@@ -1098,6 +1108,39 @@ mod session_state_tests {
|
||||
assert_eq!(parser.resume(second), ParserResumeApplication::Resumed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_defers_without_consuming_the_active_parser_suspension() {
|
||||
let mut parser = DocumentParserSession::start_open_live_document(
|
||||
Url::parse("https://parser-session.test/").expect("test URL"),
|
||||
NativeNodeId::new(1),
|
||||
);
|
||||
let parser_owner = owner(1, 2, 3);
|
||||
parser.bind_owner(parser_owner, 11);
|
||||
let permit = parser.suspend(ParserSuspensionCause::ParserClassicSource {
|
||||
script: NativeNodeId::new(8),
|
||||
});
|
||||
let suspended_state = parser.run_state();
|
||||
|
||||
assert_eq!(
|
||||
parser.request_close(),
|
||||
DocumentParserCloseDisposition::DeferredUntilReady
|
||||
);
|
||||
assert_eq!(parser.lifetime(), DocumentParserLifetime::Closing);
|
||||
assert_eq!(
|
||||
parser.run_state(),
|
||||
suspended_state,
|
||||
"document.close() must not bypass the active parser blocker"
|
||||
);
|
||||
assert_eq!(parser.current_resume_permit(), Some(permit));
|
||||
|
||||
assert_eq!(parser.resume(permit), ParserResumeApplication::Resumed);
|
||||
assert_eq!(
|
||||
parser.request_close(),
|
||||
DocumentParserCloseDisposition::DrainNow,
|
||||
"the delayed close can drain after the exact blocker releases"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_pump_and_drop_transitions_are_observable_by_derived_capabilities() {
|
||||
let parser = session();
|
||||
|
||||
@@ -13,9 +13,9 @@ use crate::{
|
||||
LocalWindowId,
|
||||
},
|
||||
live_document_parser::{
|
||||
DocumentParserLifetime, DocumentParserRunState, DocumentParserSession,
|
||||
LiveDocumentParserOwner, LiveDocumentParserStepOutcome, ParserResumeApplication,
|
||||
ParserSuspensionCause,
|
||||
DocumentParserCloseDisposition, DocumentParserLifetime, DocumentParserRunState,
|
||||
DocumentParserSession, LiveDocumentParserOwner, LiveDocumentParserStepOutcome,
|
||||
ParserResumeApplication, ParserSuspensionCause,
|
||||
},
|
||||
modulepreload::{
|
||||
invalid_modulepreload_as_value, invalid_modulepreload_as_warning,
|
||||
@@ -1044,8 +1044,14 @@ impl JsContextHost {
|
||||
.script_input_session()
|
||||
.enqueue_script_input_html(chunk);
|
||||
}
|
||||
if close_requested {
|
||||
entry.request_close();
|
||||
let parser_ready_to_advance = if close_requested {
|
||||
entry.request_close() == DocumentParserCloseDisposition::DrainNow
|
||||
} else {
|
||||
entry.run_state() == DocumentParserRunState::Ready
|
||||
};
|
||||
if !parser_ready_to_advance {
|
||||
self.child_document_parsers.replace(owner, entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
loop {
|
||||
|
||||
@@ -11067,6 +11067,137 @@ async fn child_document_write_nested_external_classic_blocks_domcontentloaded()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_document_close_defers_while_written_external_classic_blocks_parser() {
|
||||
run_page_vm_async_test(async move {
|
||||
let (base_url, server) = spawn_path_response_http_server(vec![(
|
||||
"/child-close-blocker.js",
|
||||
"HTTP/1.1 200 OK",
|
||||
"parent.__childCloseEvents.push('external:' + Boolean(document.getElementById('after-blocker')));"
|
||||
.to_owned(),
|
||||
Duration::ZERO,
|
||||
)])
|
||||
.await;
|
||||
let loader =
|
||||
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
||||
let document_url = Url::parse(&format!("{base_url}/page")).expect("page url");
|
||||
let page_vm = test_page_vm_with_loader_and_document_url(&loader, Vec::new(), document_url);
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
let (
|
||||
events_after_close,
|
||||
source_load_source,
|
||||
events_after_external,
|
||||
final_events,
|
||||
tail_exists,
|
||||
) = local_executor
|
||||
.run(async move {
|
||||
let mut page_vm = page_vm;
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__childCloseEvents = [];
|
||||
const frame = document.createElement("iframe");
|
||||
frame.id = "child-close-frame";
|
||||
document.body.appendChild(frame);
|
||||
})()
|
||||
"#,
|
||||
)?;
|
||||
materialize_child_realm_through_page_turn_for_test(
|
||||
&mut page_vm,
|
||||
"child-close-frame",
|
||||
)?;
|
||||
|
||||
let script_url = format!("{base_url}/child-close-blocker.js");
|
||||
page_vm.vm_mut().eval(&format!(
|
||||
r#"
|
||||
(() => {{
|
||||
const frame = document.getElementById("child-close-frame");
|
||||
frame.onload = () => __childCloseEvents.push("load");
|
||||
const childDocument = frame.contentDocument;
|
||||
childDocument.open();
|
||||
childDocument.addEventListener("DOMContentLoaded", () => __childCloseEvents.push("dcl"));
|
||||
childDocument.write(`<script src="{script_url}"><\/script><main id="after-blocker">tail</main>`);
|
||||
childDocument.close();
|
||||
__childCloseEvents.push("after-close");
|
||||
}})()
|
||||
"#,
|
||||
))?;
|
||||
let events_after_close = page_vm.vm_mut().eval("__childCloseEvents.join('|')")?;
|
||||
let source_load_source =
|
||||
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
|
||||
&mut page_vm,
|
||||
ChildFrameSemanticTurnKind::ClassicScriptSourceLoad,
|
||||
"document.close delayed external classic source load",
|
||||
)
|
||||
.await;
|
||||
if !page_vm
|
||||
.page_resource_completion_queue()
|
||||
.has_ready_completion()
|
||||
{
|
||||
let arrived = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
wait_for_typed_page_resource_completion(&mut page_vm),
|
||||
)
|
||||
.await
|
||||
.expect("child close blocker completion should arrive before timeout");
|
||||
assert!(
|
||||
arrived,
|
||||
"child close blocker completion sender should remain open"
|
||||
);
|
||||
}
|
||||
let _ = run_next_resource_completion_as_typed_page_turn(&mut page_vm)?;
|
||||
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
|
||||
&mut page_vm,
|
||||
ChildFrameSemanticTurnKind::DocumentScriptReady,
|
||||
"document.close delayed external classic execution",
|
||||
)
|
||||
.await;
|
||||
let events_after_external =
|
||||
page_vm.vm_mut().eval("__childCloseEvents.join('|')")?;
|
||||
run_child_interactive_domcontentloaded_then_host_load_for_wait(
|
||||
&mut page_vm,
|
||||
"document.close delayed child load",
|
||||
)
|
||||
.await;
|
||||
let final_events = page_vm.vm_mut().eval("__childCloseEvents.join('|')")?;
|
||||
let tail_exists = page_vm.vm_mut().eval(
|
||||
"String(Boolean(document.getElementById('child-close-frame').contentDocument.getElementById('after-blocker')))",
|
||||
)?;
|
||||
|
||||
Ok::<_, anyhow::Error>((
|
||||
events_after_close,
|
||||
source_load_source,
|
||||
events_after_external,
|
||||
final_events,
|
||||
tail_exists,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.expect("child document.close delayed EOF test should run");
|
||||
|
||||
assert_eq!(
|
||||
events_after_close, "after-close",
|
||||
"document.close() should return without executing or bypassing the blocker"
|
||||
);
|
||||
assert_eq!(
|
||||
source_load_source,
|
||||
ChildFrameSemanticTurnKind::ClassicScriptSourceLoad
|
||||
);
|
||||
assert_eq!(
|
||||
events_after_external, "after-close|external:false",
|
||||
"the external parser-blocking script must run before future markup is parsed"
|
||||
);
|
||||
assert_eq!(final_events, "after-close|external:false|dcl|load");
|
||||
assert_eq!(tail_exists, "true");
|
||||
|
||||
server
|
||||
.await
|
||||
.expect("child close blocker server should finish");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn page_vm_nested_frame_finish_releases_parent_document_lifecycle() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
Reference in New Issue
Block a user