mirror of
https://github.com/lexmount/moli.git
synced 2026-11-13 08:08:10 +00:00
fix(service-worker): retain Window client messages until the queue is enabled
This commit is contained in:
@@ -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::<v8::BigInt>::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>(
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -996,6 +996,8 @@ pub(crate) struct JsContextHost {
|
||||
pending_service_worker_unregisters:
|
||||
HashMap<u64, service_workers::PendingServiceWorkerUnregister>,
|
||||
pending_service_worker_ready: HashMap<u64, service_workers::PendingServiceWorkerReady>,
|
||||
service_worker_client_message_queues:
|
||||
HashMap<WindowDocumentOwner, service_workers::ServiceWorkerClientMessageQueue>,
|
||||
service_worker_registration_watchers: Vec<service_workers::ServiceWorkerRegistrationWatcher>,
|
||||
service_worker_lifecycle_watched_scopes: HashSet<(Url, String, WindowDocumentOwner)>,
|
||||
service_worker_popup_clients: HashMap<u64, ServiceWorkerClientId>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ServiceWorkerClientMessageCompletion>,
|
||||
scheduled: usize,
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
pub(crate) fn service_worker_window_id_for_dispatch_scope(
|
||||
&self,
|
||||
scope: OwnerDispatchScope,
|
||||
) -> Option<u64> {
|
||||
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<ServiceWorkerClientMessageCompletion> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -24,6 +24,9 @@ use super::{IntoPageTaskCompletion, PageTaskCompletion, PageVm};
|
||||
impl From<ServiceWorkerClientMessageBodyEffect> 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
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
@@ -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(); }));
|
||||
|
||||
Reference in New Issue
Block a user