mirror of
https://github.com/lexmount/moli.git
synced 2026-09-27 00:01:32 +00:00
fix(modules): release async graphs independently
Execute ready async module scripts without waiting for earlier pending modules. Keep ordered modules in sequence and leave parser-retained terminals under parser ownership. Cover successful and failed graphs, readiness before watching, and mixed async/in-order delivery in the shared document scheduler.
This commit is contained in:
@@ -550,6 +550,7 @@ pub(super) struct ParserModuleScriptRunner<
|
||||
struct DocumentParserModulePendingScript<T> {
|
||||
node_id: NodeId,
|
||||
state: ParserPendingModuleScriptState<T>,
|
||||
is_async: bool,
|
||||
retained_by_parser_order: bool,
|
||||
blocking_stylesheet_signatures:
|
||||
HashSet<crate::stylesheet_blocking::DocumentBlockingStylesheetSignature>,
|
||||
@@ -560,6 +561,7 @@ impl<T> DocumentParserModulePendingScript<T> {
|
||||
Self {
|
||||
node_id: script.node_id,
|
||||
state: ParserPendingModuleScriptState::new(),
|
||||
is_async: script.mode == crate::types::ScriptMode::Async,
|
||||
retained_by_parser_order: false,
|
||||
blocking_stylesheet_signatures: HashSet::new(),
|
||||
}
|
||||
@@ -641,7 +643,7 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
|
||||
return ParserModulePendingScriptWatchResult::WaitingForTree;
|
||||
}
|
||||
|
||||
let ready = self.take_ready_terminals_in_document_order();
|
||||
let ready = self.take_ready_terminals(key);
|
||||
if ready.is_empty() {
|
||||
ParserModulePendingScriptWatchResult::WaitingForTree
|
||||
} else {
|
||||
@@ -659,7 +661,7 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
|
||||
if self.is_retained_by_parser_order(key) {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
Some(self.take_ready_terminals_in_document_order())
|
||||
Some(self.take_ready_terminals(key))
|
||||
}
|
||||
|
||||
pub(super) fn notify_module_tree_load_failed(
|
||||
@@ -672,7 +674,7 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
|
||||
if self.is_retained_by_parser_order(key) {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
Some(self.take_ready_terminals_in_document_order())
|
||||
Some(self.take_ready_terminals(key))
|
||||
}
|
||||
|
||||
pub(super) fn blocking_stylesheet_signatures(
|
||||
@@ -726,9 +728,21 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn take_ready_terminals_in_document_order(
|
||||
fn take_ready_terminals(
|
||||
&mut self,
|
||||
key: ParserPendingScriptKey,
|
||||
) -> Vec<ParserModuleGraphTerminalWork<Target, ParserModuleGraphFailure>> {
|
||||
// Async modules belong to the execute-as-soon-as-possible set, so only
|
||||
// their own readiness controls delivery, regardless of document order.
|
||||
if let Some(pending) = self.pending.get_mut(&key)
|
||||
&& pending.is_async
|
||||
{
|
||||
let Some(terminal) = pending.state.take_ready_terminal() else {
|
||||
return Vec::new();
|
||||
};
|
||||
self.remove_pending(key);
|
||||
return vec![terminal];
|
||||
}
|
||||
let mut ready = Vec::new();
|
||||
while let Some(key) = self.next_pending_key_in_document_order() {
|
||||
let Some(pending) = self.pending.get(&key) else {
|
||||
@@ -747,9 +761,9 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
|
||||
}
|
||||
|
||||
fn next_pending_key_in_document_order(&self) -> Option<ParserPendingScriptKey> {
|
||||
self.pending
|
||||
.iter()
|
||||
.find_map(|(key, pending)| (!pending.retained_by_parser_order).then_some(*key))
|
||||
self.pending.iter().find_map(|(key, pending)| {
|
||||
(!pending.retained_by_parser_order && !pending.is_async).then_some(*key)
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_pending(
|
||||
|
||||
@@ -1187,6 +1187,196 @@ mod tests {
|
||||
assert_eq!(parser_ready.script().node_id, first.node_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_module_readiness_bypasses_earlier_pending_modules() {
|
||||
for earlier_mode in [ScriptMode::Async, ScriptMode::ModuleInOrder] {
|
||||
for watch_before_ready in [false, true] {
|
||||
let mut store: DocumentScriptSchedulerStore<u64, MainLikeModuleReadyTarget> =
|
||||
DocumentScriptSchedulerStore::default();
|
||||
let target = MainLikeModuleReadyTarget {
|
||||
owner: 42,
|
||||
route: 1,
|
||||
};
|
||||
let mut slow =
|
||||
parser_module_script("https://document-scripts.test/slow.js", 10, 10);
|
||||
slow.mode = earlier_mode;
|
||||
let mut fast =
|
||||
parser_module_script("https://document-scripts.test/fast.js", 20, 20);
|
||||
fast.mode = ScriptMode::Async;
|
||||
assert!(
|
||||
store
|
||||
.register_and_watch_module_script(target.owner, &slow)
|
||||
.watched()
|
||||
);
|
||||
let fast_id = store.register_module_script(target.owner, &fast);
|
||||
if watch_before_ready {
|
||||
assert!(store.watch_module_script(fast_id).watched());
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
|
||||
target.clone(),
|
||||
fast.clone(),
|
||||
vec![ModuleEntryId::from_raw(20)],
|
||||
)),
|
||||
watch_before_ready,
|
||||
);
|
||||
if !watch_before_ready {
|
||||
assert!(!store.has_ready_work());
|
||||
assert!(store.watch_module_script(fast_id).watched());
|
||||
}
|
||||
|
||||
let ready = store
|
||||
.take_next_ready_work()
|
||||
.expect("ready async module must not wait for an earlier module")
|
||||
.into_module_script_graph_ready();
|
||||
assert_eq!(ready.script().node_id, fast.node_id);
|
||||
assert_eq!(
|
||||
store.pending_parser_module_script_count_for_test(target.owner),
|
||||
1
|
||||
);
|
||||
assert!(!store.has_ready_work());
|
||||
|
||||
assert!(
|
||||
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
|
||||
target.clone(),
|
||||
slow.clone(),
|
||||
vec![ModuleEntryId::from_raw(10)],
|
||||
))
|
||||
);
|
||||
let ready = store
|
||||
.take_next_ready_work()
|
||||
.expect("slow module completes later")
|
||||
.into_module_script_graph_ready();
|
||||
assert_eq!(ready.script().node_id, slow.node_id);
|
||||
assert_eq!(
|
||||
store.pending_parser_module_script_count_for_test(target.owner),
|
||||
0
|
||||
);
|
||||
assert!(!store.has_ready_work());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_module_failure_bypasses_earlier_pending_modules() {
|
||||
for watch_before_failure in [false, true] {
|
||||
let mut store: DocumentScriptSchedulerStore<
|
||||
u64,
|
||||
std::convert::Infallible,
|
||||
std::convert::Infallible,
|
||||
TestModuleGraphFailure,
|
||||
> = DocumentScriptSchedulerStore::default();
|
||||
let owner = 42;
|
||||
let mut slow = parser_module_script("https://document-scripts.test/slow.js", 10, 10);
|
||||
slow.mode = ScriptMode::Async;
|
||||
let mut failed =
|
||||
parser_module_script("https://document-scripts.test/failed.js", 20, 20);
|
||||
failed.mode = ScriptMode::Async;
|
||||
assert!(
|
||||
store
|
||||
.register_and_watch_module_script(owner, &slow)
|
||||
.watched()
|
||||
);
|
||||
let failed_id = store.register_module_script(owner, &failed);
|
||||
if watch_before_failure {
|
||||
assert!(store.watch_module_script(failed_id).watched());
|
||||
}
|
||||
assert_eq!(
|
||||
store.notify_module_script_graph_failed_action(TestModuleGraphFailure::new(
|
||||
owner,
|
||||
&failed,
|
||||
"fetch failed",
|
||||
)),
|
||||
watch_before_failure,
|
||||
);
|
||||
if !watch_before_failure {
|
||||
assert!(!store.has_ready_work());
|
||||
assert!(store.watch_module_script(failed_id).watched());
|
||||
}
|
||||
|
||||
let failure = store
|
||||
.take_next_ready_work()
|
||||
.expect("async failure must not wait for an earlier module")
|
||||
.into_module_script_graph_failed();
|
||||
assert_eq!(failure.script_node_id(), failed.node_id);
|
||||
assert_eq!(failure.message, "fetch failed");
|
||||
assert_eq!(store.pending_parser_module_script_count_for_test(owner), 1);
|
||||
assert!(!store.has_ready_work());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordered_modules_preserve_order_without_waiting_for_async_modules() {
|
||||
let mut store: DocumentScriptSchedulerStore<u64, MainLikeModuleReadyTarget> =
|
||||
DocumentScriptSchedulerStore::default();
|
||||
let target = MainLikeModuleReadyTarget {
|
||||
owner: 42,
|
||||
route: 1,
|
||||
};
|
||||
let mut first = parser_module_script("https://document-scripts.test/first.js", 10, 10);
|
||||
first.mode = ScriptMode::ModuleInOrder;
|
||||
let mut middle = parser_module_script("https://document-scripts.test/async.js", 20, 20);
|
||||
middle.mode = ScriptMode::Async;
|
||||
let mut last = parser_module_script("https://document-scripts.test/last.js", 30, 30);
|
||||
last.mode = ScriptMode::ModuleInOrder;
|
||||
for script in [&first, &middle, &last] {
|
||||
assert!(
|
||||
store
|
||||
.register_and_watch_module_script(target.owner, script)
|
||||
.watched()
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
!store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
|
||||
target.clone(),
|
||||
last.clone(),
|
||||
vec![ModuleEntryId::from_raw(30)],
|
||||
))
|
||||
);
|
||||
assert!(
|
||||
!store.has_ready_work(),
|
||||
"ordered modules must wait for earlier ordered modules"
|
||||
);
|
||||
assert!(
|
||||
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
|
||||
target.clone(),
|
||||
first.clone(),
|
||||
vec![ModuleEntryId::from_raw(10)],
|
||||
))
|
||||
);
|
||||
for expected in [&first, &last] {
|
||||
let ready = store
|
||||
.take_next_ready_work()
|
||||
.expect("pending async module must not block ready ordered modules")
|
||||
.into_module_script_graph_ready();
|
||||
assert_eq!(ready.script().node_id, expected.node_id);
|
||||
}
|
||||
assert_eq!(
|
||||
store.pending_parser_module_script_count_for_test(target.owner),
|
||||
1
|
||||
);
|
||||
assert!(!store.has_ready_work());
|
||||
assert!(
|
||||
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
|
||||
target.clone(),
|
||||
middle.clone(),
|
||||
vec![ModuleEntryId::from_raw(20)],
|
||||
))
|
||||
);
|
||||
let ready = store
|
||||
.take_next_ready_work()
|
||||
.expect("async module completes independently")
|
||||
.into_module_script_graph_ready();
|
||||
assert_eq!(ready.script().node_id, middle.node_id);
|
||||
assert_eq!(
|
||||
store.pending_parser_module_script_count_for_test(target.owner),
|
||||
0
|
||||
);
|
||||
assert!(!store.has_ready_work());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_script_scheduler_store_routes_graph_failure_by_payload_owner() {
|
||||
let mut store: DocumentScriptSchedulerStore<
|
||||
|
||||
Reference in New Issue
Block a user