diff --git a/moli-renderer-v8/src/document_script_scheduler/module_ready.rs b/moli-renderer-v8/src/document_script_scheduler/module_ready.rs index 76526fc5e..fd1850618 100644 --- a/moli-renderer-v8/src/document_script_scheduler/module_ready.rs +++ b/moli-renderer-v8/src/document_script_scheduler/module_ready.rs @@ -550,6 +550,7 @@ pub(super) struct ParserModuleScriptRunner< struct DocumentParserModulePendingScript { node_id: NodeId, state: ParserPendingModuleScriptState, + is_async: bool, retained_by_parser_order: bool, blocking_stylesheet_signatures: HashSet, @@ -560,6 +561,7 @@ impl DocumentParserModulePendingScript { 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 ParserModuleScriptRunner ParserModuleScriptRunner ParserModuleScriptRunner ParserModuleScriptRunner Vec> { + // 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 ParserModuleScriptRunner Option { - 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( diff --git a/moli-renderer-v8/src/document_script_scheduler/store.rs b/moli-renderer-v8/src/document_script_scheduler/store.rs index 6bb8ee972..d4cc00e81 100644 --- a/moli-renderer-v8/src/document_script_scheduler/store.rs +++ b/moli-renderer-v8/src/document_script_scheduler/store.rs @@ -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 = + 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 = + 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<