From 71ff1009f7616a734dcfbd5c04b5ef2bce06d343 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 12 Nov 2026 07:18:25 +0800 Subject: [PATCH] fix(service-worker): retain Window client messages until the queue is enabled --- .../window_runtime/service_worker.rs | 52 ++++++- .../context_host/child_documents/lifecycle.rs | 4 + .../src/native_bridge/context_host/core.rs | 1 + .../src/native_bridge/context_host/mod.rs | 2 + .../src/native_bridge/context_host/popups.rs | 4 + .../context_host/service_workers.rs | 7 + .../service_workers/client_messages.rs | 138 ++++++++++++++++++ .../service_worker_client_message.rs | 31 +++- .../page_task_queue/service_worker_tasks.rs | 9 ++ .../page_service_worker_client_message.rs | 6 +- .../tests/service_worker_client_message.rs | 103 +++++++++++++ .../src/script_vm/page_task_enqueue.rs | 11 ++ .../service_worker_client_message_body.rs | 14 +- .../misc/extracted/service_worker_clients.rs | 4 + .../browser_api/simple_handler_object.rs | 2 + 15 files changed, 379 insertions(+), 9 deletions(-) create mode 100644 moli-renderer-v8/src/native_bridge/context_host/service_workers/client_messages.rs diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs index 783b849530..2971ad6992 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs @@ -22,6 +22,7 @@ use crate::worker::WorkerScriptKind; use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject, WebApiObjectDeclaration}; const SERVICE_WORKER_REGISTRATION_SCOPE_SLOT: &str = "__moliServiceWorkerRegistrationScope"; +const SERVICE_WORKER_WINDOW_ID_SLOT: &str = "__moliServiceWorkerWindowIdentity"; const SERVICE_WORKER_REGISTRATION_ID_SLOT: &str = "__moliServiceWorkerRegistrationId"; const SERVICE_WORKER_REGISTRATION_WORKER_SLOT: &str = "__moliServiceWorkerRegistrationWorker"; const SERVICE_WORKER_REGISTRATION_WORKERS_SLOT: &str = "__moliServiceWorkerRegistrationWorkers"; @@ -993,6 +994,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_message_handler_set SERVICE_WORKER_CONTAINER_ONMESSAGE_SLOT, args.get(0), ); + enable_service_worker_client_message_queue(scope, args.this()); } pub(in crate::context_bootstrap) fn navigator_service_worker_messageerror_handler_getter_callback< @@ -2765,13 +2767,35 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_ready_getter_callba } } -pub(in crate::context_bootstrap) fn navigator_service_worker_start_messages_callback( - _scope: &mut v8::PinScope<'_, '_>, - _args: v8::FunctionCallbackArguments<'_>, - _rv: v8::ReturnValue<'_, v8::Value>, +pub(in crate::context_bootstrap) fn navigator_service_worker_start_messages_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'s, v8::Value>, ) { - // The current native client-message queue is enabled when the container is - // created. Calling startMessages again leaves that enabled state unchanged. + enable_service_worker_client_message_queue(scope, args.this()); +} + +fn enable_service_worker_client_message_queue<'s>( + scope: &mut v8::PinScope<'s, '_>, + container: v8::Local<'s, v8::Object>, +) { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + // Worker client messages become runnable after worker bootstrap. + return; + }; + let owner = service_worker_container_owner_scope(scope, container); + let Some(identity) = get_private_value(scope, container, SERVICE_WORKER_WINDOW_ID_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let (identity, lossless) = identity.u64_value(); + let host = unsafe { &mut *host_ptr }; + // A container retained from a replaced Window must not enable the new + // Window's queue. document.open() can retain the same Window and container. + if lossless && host.service_worker_window_id_for_dispatch_scope(owner) == Some(identity) { + host.enable_service_worker_client_message_queue(owner); + } } fn mark_service_worker_registration_event_target<'s>( @@ -2850,6 +2874,22 @@ pub(in crate::context_bootstrap) fn service_worker_object_set_owner_scope<'s>( ) { let owner_token = service_worker_owner_token_value(scope, owner); set_private_value(scope, object, SERVICE_WORKER_OWNER_TOKEN_SLOT, owner_token); + if let Some(identity) = context_host_ptr_from_global_bridge(scope).and_then(|host| { + let host = unsafe { &*host }; + // A retained Navigator can materialize this object after navigation. + // Its creation realm must still belong to this current Window; a + // dispatch address alone may already refer to a replacement Window. + host.current_runtime_window_execution_context_identity_for_dispatch_scope(scope, owner)?; + host.service_worker_window_id_for_dispatch_scope(owner) + }) { + let identity = v8::BigInt::new_from_u64(scope, identity); + set_private_value( + scope, + object, + SERVICE_WORKER_WINDOW_ID_SLOT, + identity.into(), + ); + } } fn service_worker_worker_set_owner_scope<'s>( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs index ee12a44bca..e0a5d2d3d9 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs @@ -795,6 +795,10 @@ impl JsContextHost { ) }); record_performance_dom_content_loaded_event_end(scope); + self.enable_service_worker_client_message_queue_for_document( + super::super::OwnerDispatchScope::Child(child_handle), + crate::native_bridge::WindowDocumentOwner::Frame(action.owner()), + ); if self .frame_owner_store .child_document_task_owner_is_current(child_handle, action.owner()) diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 2beb57e797..70bc512af4 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -458,6 +458,7 @@ impl JsContextHost { pending_service_worker_registers: HashMap::new(), pending_service_worker_unregisters: HashMap::new(), pending_service_worker_ready: HashMap::new(), + service_worker_client_message_queues: HashMap::new(), service_worker_registration_watchers: Vec::new(), service_worker_lifecycle_watched_scopes: HashSet::new(), service_worker_popup_clients: HashMap::new(), diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 815aac6fa8..f33eccdbd1 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -996,6 +996,8 @@ pub(crate) struct JsContextHost { pending_service_worker_unregisters: HashMap, pending_service_worker_ready: HashMap, + service_worker_client_message_queues: + HashMap, service_worker_registration_watchers: Vec, service_worker_lifecycle_watched_scopes: HashSet<(Url, String, WindowDocumentOwner)>, service_worker_popup_clients: HashMap, diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index b1da00d9c1..cd19680e5c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -5400,6 +5400,10 @@ impl JsContextHost { return; } document.dom_content_loaded = PopupDomContentLoadedState::Dispatched; + self.enable_service_worker_client_message_queue_for_document( + OwnerDispatchScope::LightweightPopup(popup_id), + crate::native_bridge::WindowDocumentOwner::LightweightPopup(window.document_owner), + ); self.queue_lightweight_popup_post_parse_autofocus(popup_id); self.publish_lightweight_popup_load_event_if_ready(popup_id); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs b/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs index f7f217c3a2..a8e9c4f195 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/service_workers.rs @@ -16,6 +16,9 @@ use crate::worker::{WorkerNetworkPolicy, WorkerScriptKind, worker_secure_context use moli_storage_key::MoliStorageKey; use url::Url; +mod client_messages; +pub(super) use client_messages::ServiceWorkerClientMessageQueue; + pub(super) fn service_worker_first_party_storage_key(document_url: &Url) -> String { moli_storage_key::MoliStorageKey::first_party_from_url(document_url, None) .serialized_storage_key() @@ -378,6 +381,8 @@ impl JsContextHost { retired_owner: WindowDocumentOwner, ) { self.retire_web_mcp_document(retired_owner); + self.service_worker_client_message_queues + .remove(&retired_owner); let register_count = self.pending_service_worker_registers.len(); self.pending_service_worker_registers .retain(|_, pending| pending.owner.window_document_owner() != retired_owner); @@ -409,6 +414,8 @@ impl JsContextHost { transition: crate::frame_owner_model::MainDocumentOwnerTransition, ) { let retired_owner = transition.retired_owner(); + self.service_worker_client_message_queues + .remove(&WindowDocumentOwner::Frame(retired_owner)); self.retire_web_mcp_document(WindowDocumentOwner::Frame(retired_owner)); let register_count = self.pending_service_worker_registers.len(); self.pending_service_worker_registers diff --git a/moli-renderer-v8/src/native_bridge/context_host/service_workers/client_messages.rs b/moli-renderer-v8/src/native_bridge/context_host/service_workers/client_messages.rs new file mode 100644 index 0000000000..62da27ecfa --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/service_workers/client_messages.rs @@ -0,0 +1,138 @@ +use std::collections::VecDeque; + +use super::{JsContextHost, OwnerDispatchScope, ServiceWorkerWindowOwner, WindowDocumentOwner}; +use crate::types::ServiceWorkerClientMessageCompletion; + +#[derive(Default)] +pub(in crate::native_bridge::context_host) struct ServiceWorkerClientMessageQueue { + enabled: bool, + pending: VecDeque, + scheduled: usize, +} + +impl JsContextHost { + pub(crate) fn service_worker_window_id_for_dispatch_scope( + &self, + scope: OwnerDispatchScope, + ) -> Option { + use super::super::WindowExecutionContextOwner; + match self.current_window_execution_context_owner(scope)? { + WindowExecutionContextOwner::Frame(id) => Some(id.0), + WindowExecutionContextOwner::LightweightPopup { + local_window_id, .. + } => Some(local_window_id.as_u64()), + } + } + + fn service_worker_client_queue_initially_enabled(&self, scope: OwnerDispatchScope) -> bool { + let document = match scope { + OwnerDispatchScope::Top => Some(self.document_handle()), + OwnerDispatchScope::Child(child) => self.child_browsing_context_document_handle(child), + OwnerDispatchScope::LightweightPopup(popup) => { + self.lightweight_popup_document_handle(popup) + } + }; + // Initial about:blank documents can already be complete without a + // parser lifecycle task. Interactive Documents are enabled explicitly + // by their real DOMContentLoaded transition. + document + .is_some_and(|document| self.document_ready_state_for_handle(document) == "complete") + } + + pub(crate) fn enable_service_worker_client_message_queue(&mut self, scope: OwnerDispatchScope) { + let Some(owner) = self.service_worker_window_owner_for_dispatch_scope(scope) else { + return; + }; + self.enable_service_worker_client_message_queue_for_document( + scope, + owner.window_document_owner(), + ); + } + + pub(crate) fn enable_service_worker_client_message_queue_for_document( + &mut self, + scope: OwnerDispatchScope, + document: WindowDocumentOwner, + ) { + if self + .service_worker_window_owner_for_dispatch_scope(scope) + .is_none_or(|owner| owner.window_document_owner() != document) + { + return; + } + let queue = self + .service_worker_client_message_queues + .entry(document) + .or_default(); + if queue.enabled { + return; + } + queue.enabled = true; + let pending = std::mem::take(&mut queue.pending); + for completion in pending { + self.schedule_service_worker_client_queue_delivery(document, completion); + } + } + + fn schedule_service_worker_client_queue_delivery( + &mut self, + document: WindowDocumentOwner, + completion: ServiceWorkerClientMessageCompletion, + ) { + let queue = self + .service_worker_client_message_queues + .get_mut(&document) + .expect("a queued client message retains its current Document queue"); + queue.scheduled = queue + .scheduled + .checked_add(1) + .expect("ServiceWorker client message queue size exhausted"); + if self + .service_worker_task_tx + .send_service_worker_client_queue_delivery(completion) + .is_err() + { + self.service_worker_client_message_queues + .get_mut(&document) + .expect("sending a Page task cannot retire its Document queue") + .scheduled -= 1; + } + } + + pub(crate) fn admit_service_worker_client_message( + &mut self, + owner: ServiceWorkerWindowOwner, + completion: ServiceWorkerClientMessageCompletion, + client_queue_delivery: bool, + ) -> Option { + assert!(self.service_worker_window_owner_is_current(owner)); + let document = owner.window_document_owner(); + let enabled = self.service_worker_client_queue_initially_enabled(owner.dispatch_scope()); + let queue = self + .service_worker_client_message_queues + .entry(document) + .or_insert_with(|| ServiceWorkerClientMessageQueue { + enabled, + ..Default::default() + }); + if client_queue_delivery { + queue.scheduled = queue + .scheduled + .checked_sub(1) + .expect("a resumed client message must own a scheduled queue delivery"); + return Some(completion); + } + if !queue.enabled { + queue.pending.push_back(completion); + return None; + } + if queue.scheduled != 0 { + // New incoming Page tasks can precede messages resumed by + // startMessages(). Put them behind that older backlog, rather + // than dispatching them ahead of it or dispatching synchronously. + self.schedule_service_worker_client_queue_delivery(document, completion); + return None; + } + Some(completion) + } +} diff --git a/moli-renderer-v8/src/page_task_queue/service_worker_client_message.rs b/moli-renderer-v8/src/page_task_queue/service_worker_client_message.rs index 03dffb07fe..bc863d5d29 100644 --- a/moli-renderer-v8/src/page_task_queue/service_worker_client_message.rs +++ b/moli-renderer-v8/src/page_task_queue/service_worker_client_message.rs @@ -38,12 +38,14 @@ impl RendererPageServiceWorkerClientMessageOwner { pub(crate) struct RendererPageServiceWorkerClientMessageTask { owner: RendererPageServiceWorkerClientMessageOwner, completion: ServiceWorkerClientMessageCompletion, + client_queue_delivery: bool, } impl RendererPageServiceWorkerClientMessageTask { fn new( root_document: RendererDocumentToken, completion: ServiceWorkerClientMessageCompletion, + client_queue_delivery: bool, ) -> Self { Self { owner: RendererPageServiceWorkerClientMessageOwner::new( @@ -51,6 +53,7 @@ impl RendererPageServiceWorkerClientMessageTask { completion.target, ), completion, + client_queue_delivery, } } @@ -61,6 +64,10 @@ impl RendererPageServiceWorkerClientMessageTask { pub(crate) fn into_completion(self) -> ServiceWorkerClientMessageCompletion { self.completion } + + pub(crate) const fn is_client_queue_delivery(&self) -> bool { + self.client_queue_delivery + } } #[derive(Clone, Debug)] @@ -103,10 +110,29 @@ impl RendererPageServiceWorkerClientMessageSender { pub(crate) fn send( &self, completion: ServiceWorkerClientMessageCompletion, + ) -> Result<(), RendererPageServiceWorkerClientMessageRouteClosed> { + self.send_with_client_queue_state(completion, false) + } + + pub(crate) fn send_client_queue_delivery( + &self, + completion: ServiceWorkerClientMessageCompletion, + ) -> Result<(), RendererPageServiceWorkerClientMessageRouteClosed> { + self.send_with_client_queue_state(completion, true) + } + + fn send_with_client_queue_state( + &self, + completion: ServiceWorkerClientMessageCompletion, + client_queue_delivery: bool, ) -> Result<(), RendererPageServiceWorkerClientMessageRouteClosed> { self.task_route .send_and_signal_if_newly_ready(ReadyPageTask::new( - RendererPageServiceWorkerClientMessageTask::new(self.root_document, completion), + RendererPageServiceWorkerClientMessageTask::new( + self.root_document, + completion, + client_queue_delivery, + ), )) .map_err(|_| RendererPageServiceWorkerClientMessageRouteClosed) } @@ -194,6 +220,9 @@ pub(crate) enum ServiceWorkerClientMessageCallbackEffect { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum PageServiceWorkerClientMessageTargetEffect { + /// The exact current Document retained the message for asynchronous FIFO + /// delivery on its client message queue. + QueuedForClientMessageQueue, /// A `message` or `messageerror` event was dispatched in the exact /// current Window client. `callback_effect` determines whether selected /// completion performs callback reconciliation or checkpoint only. diff --git a/moli-renderer-v8/src/page_task_queue/service_worker_tasks.rs b/moli-renderer-v8/src/page_task_queue/service_worker_tasks.rs index c95adc99dc..6424f10987 100644 --- a/moli-renderer-v8/src/page_task_queue/service_worker_tasks.rs +++ b/moli-renderer-v8/src/page_task_queue/service_worker_tasks.rs @@ -80,6 +80,15 @@ impl RendererPageServiceWorkerTaskSender { .map_err(|_| RendererPageServiceWorkerTaskRouteClosed) } + pub(crate) fn send_service_worker_client_queue_delivery( + &self, + completion: ServiceWorkerClientMessageCompletion, + ) -> Result<(), RendererPageServiceWorkerTaskRouteClosed> { + self.client_message + .send_client_queue_delivery(completion) + .map_err(|_| RendererPageServiceWorkerTaskRouteClosed) + } + pub(crate) fn send_service_worker_client_navigate_request( &self, completion: ServiceWorkerClientNavigateRequestCompletion, diff --git a/moli-renderer-v8/src/runtime/page_vm/page_service_worker_client_message.rs b/moli-renderer-v8/src/runtime/page_vm/page_service_worker_client_message.rs index bc415bc7ea..0e1bab0949 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_service_worker_client_message.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_service_worker_client_message.rs @@ -24,6 +24,9 @@ use super::{IntoPageTaskCompletion, PageTaskCompletion, PageVm}; impl From for PageServiceWorkerClientMessageTargetEffect { fn from(effect: ServiceWorkerClientMessageBodyEffect) -> Self { match effect { + ServiceWorkerClientMessageBodyEffect::QueuedForClientMessageQueue => { + Self::QueuedForClientMessageQueue + } ServiceWorkerClientMessageBodyEffect::EventDispatched { event_kind, callback_effect, @@ -63,7 +66,8 @@ impl IntoPageTaskCompletion for PageServiceWorkerClientMessageTurnAction { callback_effect: ServiceWorkerClientMessageCallbackEffect::CurrentTargetHadNoCallback, .. } - | PageServiceWorkerClientMessageTargetEffect::CurrentTargetProducedNoDispatchableEvent => { + | PageServiceWorkerClientMessageTargetEffect::CurrentTargetProducedNoDispatchableEvent + | PageServiceWorkerClientMessageTargetEffect::QueuedForClientMessageQueue => { PageTaskCompletion::CheckpointOnly } PageServiceWorkerClientMessageTargetEffect::DiscardedStaleTarget diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/service_worker_client_message.rs b/moli-renderer-v8/src/runtime/page_vm/tests/service_worker_client_message.rs index 7f48e18de6..b1053c5024 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/service_worker_client_message.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/service_worker_client_message.rs @@ -10,6 +10,104 @@ use crate::{ types::ServiceWorkerClientMessageCompletion, }; +#[tokio::test(flavor = "current_thread")] +async fn service_worker_client_queue_enables_asynchronously_and_keeps_older_messages_first() { + run_page_vm_async_test(async move { + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?; + for enable in [ + "navigator.serviceWorker.startMessages()", + "navigator.serviceWorker.onmessage = () => {}", + "navigator.serviceWorker.onmessage = null", + "navigator.serviceWorker.onmessage = {}", + ] { + let (mut page, _resources, _wakes) = page_vm_with_bound_task_sources_and_owner_wake( + &loader, + Url::parse("https://service-worker-client-message.test/queue")?, + ); + page.vm_mut().set_document_ready_state(crate::dom::native::DocumentReadyState::Loading)?; + page.vm_mut().eval(r#" +globalThis.received = []; +navigator.serviceWorker.addEventListener('message', event => { + received.push(event.data); + Promise.resolve().then(() => received.push('after:' + event.data)); +}); +navigator.serviceWorker.onmessageerror = () => {}; +document.dispatchEvent(new Event('DOMContentLoaded')); +void 0; + "#)?; + let target = page.vm().service_worker_client_message_target_for_test( + crate::native_bridge::OwnerDispatchScope::Top, + )?; + let root = page.document_lifecycle.identity().document; + let sender = page.service_worker_task_sender_for_root_for_test(root); + let first = page.vm_mut().service_worker_client_message_payload_for_test("first")?; + sender.send_service_worker_client_message(service_worker_client_message(target, first)).unwrap(); + let task = page.take_service_worker_client_message_body_task_for_test().unwrap(); + let result = page.apply_selected_page_service_worker_client_message_turn(task)?; + assert_eq!(result.action.target_effect, PageServiceWorkerClientMessageTargetEffect::QueuedForClientMessageQueue); + page.finish_selected_page_task_completion(result.action.into_page_task_completion(), &loader).await?; + assert_eq!(page.vm_mut().eval("received.join('|')")?, ""); + assert!(page.take_service_worker_client_message_body_task_for_test().is_none()); + + // This incoming message is already resident before the older + // disabled message is rescheduled by the enabling call. + let second = page.vm_mut().service_worker_client_message_payload_for_test("second")?; + sender.send_service_worker_client_message(service_worker_client_message(target, second)).unwrap(); + page.vm_mut().eval(&format!("{enable}; Promise.resolve().then(() => received.push('enabled-microtask')); void 0;"))?; + assert_eq!(page.vm_mut().eval("received.join('|')")?, "enabled-microtask", "{enable} must not dispatch synchronously"); + let newer = page.take_service_worker_client_message_body_task_for_test().unwrap(); + let result = page.apply_selected_page_service_worker_client_message_turn(newer)?; + assert_eq!(result.action.target_effect, PageServiceWorkerClientMessageTargetEffect::QueuedForClientMessageQueue); + page.finish_selected_page_task_completion(result.action.into_page_task_completion(), &loader).await?; + for expected in ["enabled-microtask|first|after:first", "enabled-microtask|first|after:first|second|after:second"] { + let task = page.take_service_worker_client_message_body_task_for_test().unwrap(); + assert!(task.is_client_queue_delivery()); + let result = page.apply_selected_page_service_worker_client_message_turn(task)?; + assert!(matches!(result.action.target_effect, PageServiceWorkerClientMessageTargetEffect::EventDispatchedToCurrentTarget { .. })); + page.finish_selected_page_task_completion(result.action.into_page_task_completion(), &loader).await?; + assert_eq!(page.vm_mut().eval("received.join('|')")?, expected, "{enable}"); + } + page.vm_mut().eval("navigator.serviceWorker.startMessages(); navigator.serviceWorker.startMessages();")?; + assert!(page.take_service_worker_client_message_body_task_for_test().is_none()); + } + anyhow::Ok(()) + }).await.unwrap(); +} + +#[tokio::test(flavor = "current_thread")] +async fn service_worker_client_queue_discards_pending_messages_when_document_open_replaces_document() + { + run_page_vm_async_test(async move { + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?; + let (mut page, _resources, _wakes) = page_vm_with_bound_task_sources_and_owner_wake( + &loader, + Url::parse("https://service-worker-client-message.test/open-queue")?, + ); + page.vm_mut().set_document_ready_state(crate::dom::native::DocumentReadyState::Loading)?; + page.vm_mut().eval("globalThis.received=[]; globalThis.savedContainer=navigator.serviceWorker; savedContainer.addEventListener('message', event => received.push(event.data));")?; + let root = page.document_lifecycle.identity().document; + let old = page.vm().service_worker_client_message_target_for_test(crate::native_bridge::OwnerDispatchScope::Top)?; + let payload = page.vm_mut().service_worker_client_message_payload_for_test("retired")?; + page.service_worker_task_sender_for_root_for_test(root).send_service_worker_client_message(service_worker_client_message(old, payload)).unwrap(); + let task = page.take_service_worker_client_message_body_task_for_test().unwrap(); + let result = page.apply_selected_page_service_worker_client_message_turn(task)?; + assert_eq!(result.action.target_effect, PageServiceWorkerClientMessageTargetEffect::QueuedForClientMessageQueue); + page.finish_selected_page_task_completion(result.action.into_page_task_completion(), &loader).await?; + page.vm_mut().eval("document.open(); savedContainer.startMessages();")?; + let current = page.vm().service_worker_client_message_target_for_test(crate::native_bridge::OwnerDispatchScope::Top)?; + assert_ne!(old.document_owner, current.document_owner); + assert!(page.take_service_worker_client_message_body_task_for_test().is_none()); + let payload = page.vm_mut().service_worker_client_message_payload_for_test("current")?; + page.service_worker_task_sender_for_root_for_test(root).send_service_worker_client_message(service_worker_client_message(current, payload)).unwrap(); + let task = page.take_service_worker_client_message_body_task_for_test().unwrap(); + let result = page.apply_selected_page_service_worker_client_message_turn(task)?; + assert!(matches!(result.action.target_effect, PageServiceWorkerClientMessageTargetEffect::EventDispatchedToCurrentTarget { .. })); + page.finish_selected_page_task_completion(result.action.into_page_task_completion(), &loader).await?; + assert_eq!(page.vm_mut().eval("received.join('|')")?, "current"); + anyhow::Ok(()) + }).await.unwrap(); +} + fn service_worker_client_message( target: crate::types::ServiceWorkerWindowClientTarget, payload: crate::structured_clone::V8StructuredClonePayload, @@ -128,6 +226,7 @@ navigator.serviceWorker.onmessageerror = event => { __serviceWorkerClientMessageErrorEvents.push("microtask"); }); }; +navigator.serviceWorker.startMessages(); "listener-ready" "#, )?; @@ -193,6 +292,7 @@ async fn current_service_worker_client_without_a_dispatchable_container_is_check )?; page_vm.vm_mut().eval( r#" +navigator.serviceWorker.startMessages(); Object.defineProperty(navigator, "serviceWorker", { configurable: true, value: null, @@ -248,6 +348,9 @@ async fn service_worker_client_message_without_listener_is_checkpoint_only() { Url::parse("https://service-worker-client-message.test/no-listener").unwrap(); let (mut page_vm, _resource_source, _owner_wake_rx) = page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url); + page_vm + .vm_mut() + .eval("navigator.serviceWorker.startMessages();")?; let target = page_vm.vm().service_worker_client_message_target_for_test( crate::native_bridge::OwnerDispatchScope::Top, )?; diff --git a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs index d7fb0a5b17..98a81e7c1d 100644 --- a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs +++ b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs @@ -143,6 +143,7 @@ impl ScriptVm { &mut self, event_type: &str, ) -> Result<()> { + let document_owner = self.current_main_document_task_owner(); let watchdog = self .renderer_document_isolate .with_renderer_document_isolate_mut(|isolate| { @@ -178,6 +179,16 @@ impl ScriptVm { watchdog_timeout ); } + if event_type == "DOMContentLoaded" + && let Some(owner) = document_owner + { + self._context_host + .borrow_mut() + .enable_service_worker_client_message_queue_for_document( + crate::native_bridge::OwnerDispatchScope::Top, + crate::native_bridge::WindowDocumentOwner::Frame(owner), + ); + } Ok(()) } diff --git a/moli-renderer-v8/src/script_vm/service_worker_client_message_body.rs b/moli-renderer-v8/src/script_vm/service_worker_client_message_body.rs index 27712e18f0..6dc9e633ff 100644 --- a/moli-renderer-v8/src/script_vm/service_worker_client_message_body.rs +++ b/moli-renderer-v8/src/script_vm/service_worker_client_message_body.rs @@ -18,6 +18,7 @@ use crate::{ #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ServiceWorkerClientMessageBodyEffect { + QueuedForClientMessageQueue, EventDispatched { event_kind: ServiceWorkerClientMessageBodyEventKind, callback_effect: ServiceWorkerClientMessageBodyCallbackEffect, @@ -100,7 +101,18 @@ impl ScriptVm { .service_worker_window_owner_is_current(window_owner), "authorized ServiceWorker client-message owner changed inside one Page turn" ); - let completion = task.into_completion(); + let client_queue_delivery = task.is_client_queue_delivery(); + let Some(completion) = self + ._context_host + .borrow_mut() + .admit_service_worker_client_message( + window_owner, + task.into_completion(), + client_queue_delivery, + ) + else { + return Ok(ServiceWorkerClientMessageBodyEffect::QueuedForClientMessageQueue); + }; self.with_default_context_scope(move |scope, _host_ptr| { Ok( crate::context_bootstrap::dispatch_service_worker_client_message_body( diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/service_worker_clients.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/service_worker_clients.rs index 8a521695e6..3781e93166 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/service_worker_clients.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/service_worker_clients.rs @@ -323,6 +323,8 @@ async fn navigator_service_worker_post_message_routes_to_waiting_and_active_vers globalThis.__serviceWorkerWaitingPostMessageProbe = "pending"; (async () => { const sw = navigator.serviceWorker; + // This API fixture does not run a parser-driven DOMContentLoaded. + sw.startMessages(); const registration = await sw.register("worker.js", { scope: "./" }); await __moliTestActivatedServiceWorker(sw); const updated = await sw.register("worker.js", { scope: "./" }); @@ -435,6 +437,7 @@ async fn navigator_service_worker_post_message_routes_to_installing_version() { globalThis.__serviceWorkerInstallingPostMessageProbe = "pending"; (async () => { const sw = navigator.serviceWorker; + sw.startMessages(); const registration = await sw.register("worker.js", { scope: "./" }); await __moliTestActivatedServiceWorker(sw); let installingWorker = null; @@ -1330,6 +1333,7 @@ async fn navigator_service_worker_post_message_to_redundant_worker_is_dropped() globalThis.__serviceWorkerRedundantPostMessageProbe = "pending"; (async () => { const sw = navigator.serviceWorker; + sw.startMessages(); const registration = await sw.register("worker-v1.js", { scope: "./" }); await __moliTestActivatedServiceWorker(sw); const firstWorker = registration.active; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/simple_handler_object.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/simple_handler_object.rs index c091675c9f..903295215c 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/simple_handler_object.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/simple_handler_object.rs @@ -149,6 +149,8 @@ const WORKER_PARENT_PROBE: &str = r#" if (config.mode === 'service') { const listener = event => { navigator.serviceWorker.removeEventListener('message', listener); resolve(event.data); }; navigator.serviceWorker.addEventListener('message', listener); + // This API fixture does not run a parser-driven DOMContentLoaded. + navigator.serviceWorker.startMessages(); registration = await navigator.serviceWorker.register('/handler-sw.js?case=' + encodeURIComponent(config.name), {scope:'./'}); const active = registration.active || registration.installing || registration.waiting; if (active.state !== 'activated') await new Promise(done => active.addEventListener('statechange', () => { if (active.state === 'activated') done(); }));