mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 08:01:00 +00:00
fix(service-worker): implement registration update jobs
This commit is contained in:
@@ -15,6 +15,7 @@ use crate::worker::WorkerScriptKind;
|
||||
use moli_webapi_declare::{ObjectLiteralDeclaration, WebApiObject, WebApiObjectDeclaration};
|
||||
|
||||
const SERVICE_WORKER_REGISTRATION_SCOPE_SLOT: &str = "__moliServiceWorkerRegistrationScope";
|
||||
const SERVICE_WORKER_REGISTRATION_ID_SLOT: &str = "__moliServiceWorkerRegistrationId";
|
||||
const SERVICE_WORKER_REGISTRATION_EVENTS_SLOT: &str = "__moliServiceWorkerRegistrationEvents";
|
||||
const SERVICE_WORKER_REGISTRATION_WORKER_SLOT: &str = "__moliServiceWorkerRegistrationWorker";
|
||||
const SERVICE_WORKER_REGISTRATION_WORKERS_SLOT: &str = "__moliServiceWorkerRegistrationWorkers";
|
||||
@@ -64,6 +65,9 @@ struct ServiceWorkerRegistrationObjectDeclaration<'scope> {
|
||||
#[webapi(method, callback = navigator_service_worker_unregister_callback, length = 0)]
|
||||
unregister: (),
|
||||
|
||||
#[webapi(method, callback = service_worker_registration_update_callback, length = 0)]
|
||||
update: (),
|
||||
|
||||
#[webapi(
|
||||
method = "showNotification",
|
||||
callback = service_worker_registration_show_notification_callback,
|
||||
@@ -513,6 +517,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
scope,
|
||||
scope_url.clone(),
|
||||
resolver,
|
||||
None,
|
||||
request_context.owner(),
|
||||
);
|
||||
host.start_service_worker_runtime(
|
||||
@@ -532,6 +537,7 @@ pub(in crate::context_bootstrap) fn navigator_service_worker_register_callback<'
|
||||
pub(crate) fn settle_service_worker_register_completion<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
resolver: v8::Local<'s, v8::PromiseResolver>,
|
||||
update_registration: Option<v8::Local<'s, v8::Object>>,
|
||||
owner: OwnerDispatchScope,
|
||||
scope_url: &url::Url,
|
||||
result: std::result::Result<
|
||||
@@ -547,6 +553,16 @@ pub(crate) fn settle_service_worker_register_completion<'s>(
|
||||
.or_else(|| state.installing())
|
||||
.map(|version| version.script_url().as_str())
|
||||
.unwrap_or_else(|| state.scope_url().as_str());
|
||||
if let Some(registration) = update_registration {
|
||||
update_service_worker_registration_object(
|
||||
scope,
|
||||
registration,
|
||||
script_url,
|
||||
ServiceWorkerRegistrationPhase::Snapshot(&state),
|
||||
);
|
||||
let _ = resolver.resolve(scope, registration.into());
|
||||
return;
|
||||
}
|
||||
let registration =
|
||||
if let Some(container) = service_worker_container_for_owner(scope, owner) {
|
||||
build_service_worker_registration_object_for_container(
|
||||
@@ -673,6 +689,11 @@ pub(crate) fn dispatch_service_worker_lifecycle_notification(
|
||||
notification.document_owner,
|
||||
);
|
||||
for (owner, registration) in registrations {
|
||||
if service_worker_registration_id_from_object(scope, registration)
|
||||
!= Some(notification.registration.registration_id())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let previous_owner_context = owner.enter(scope);
|
||||
update_service_worker_registration_object(
|
||||
scope,
|
||||
@@ -1010,12 +1031,20 @@ fn remember_service_worker_container_registration<'s>(
|
||||
return;
|
||||
};
|
||||
let scope_url = scope_url.to_rust_string_lossy(scope);
|
||||
if service_worker_container_cached_registration_for_scope(scope, container, &scope_url)
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let cache = service_worker_container_registration_cache(scope, container);
|
||||
if let Some(previous) =
|
||||
service_worker_container_cached_registration_for_scope(scope, container, &scope_url)
|
||||
{
|
||||
for index in 0..cache.length() {
|
||||
if cache
|
||||
.get_index(scope, index)
|
||||
.is_some_and(|value| value.strict_equals(previous.into()))
|
||||
{
|
||||
let _ = cache.set_index(scope, index, registration.into());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
array_push_value(scope, cache, registration.into());
|
||||
}
|
||||
|
||||
@@ -1310,6 +1339,95 @@ fn service_worker_container_owner_scope<'s>(
|
||||
service_worker_owner_scope_from_object(scope, container)
|
||||
}
|
||||
|
||||
fn service_worker_registration_id_from_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
registration: v8::Local<'s, v8::Object>,
|
||||
) -> Option<crate::service_worker_runtime::ServiceWorkerRegistrationId> {
|
||||
let value = get_private_value(scope, registration, SERVICE_WORKER_REGISTRATION_ID_SLOT)?;
|
||||
let value = v8::Local::<v8::BigInt>::try_from(value).ok()?;
|
||||
let (value, lossless) = value.u64_value();
|
||||
lossless.then(|| {
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationId::from_u64_for_binding(value)
|
||||
})
|
||||
}
|
||||
|
||||
fn service_worker_registration_update_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
return;
|
||||
};
|
||||
rv.set(resolver.get_promise(scope).into());
|
||||
let Some(registration_id) = service_worker_registration_id_from_object(scope, args.this())
|
||||
else {
|
||||
reject_service_worker_promise_with_type_error(
|
||||
scope,
|
||||
resolver,
|
||||
"Illegal invocation of ServiceWorkerRegistration.update",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(scope_url) = service_worker_registration_scope_from_this(scope, args.this()) else {
|
||||
reject_service_worker_promise_with_dom_exception(
|
||||
scope,
|
||||
resolver,
|
||||
"The registration is unavailable.",
|
||||
"InvalidStateError",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
reject_service_worker_promise_with_dom_exception(
|
||||
scope,
|
||||
resolver,
|
||||
"The registration owner is unavailable.",
|
||||
"InvalidStateError",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
let owner = service_worker_owner_scope_from_object(scope, args.this());
|
||||
let Some(request_context) = host.service_worker_window_request_context(owner) else {
|
||||
reject_service_worker_promise_with_dom_exception(
|
||||
scope,
|
||||
resolver,
|
||||
"The registration Document is no longer active.",
|
||||
"InvalidStateError",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(request_client) = host
|
||||
.document_resource_loader_for_window_owner(request_context.owner().window_document_owner())
|
||||
.map(|loader| loader.request_client().clone())
|
||||
else {
|
||||
reject_service_worker_promise_with_dom_exception(
|
||||
scope,
|
||||
resolver,
|
||||
"The registration Document is no longer active.",
|
||||
"InvalidStateError",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let (request_id, document_owner, completion_tx) = host
|
||||
.register_pending_service_worker_register(
|
||||
scope,
|
||||
scope_url,
|
||||
resolver,
|
||||
Some(args.this()),
|
||||
request_context.owner(),
|
||||
);
|
||||
host.start_service_worker_registration_update(
|
||||
registration_id,
|
||||
&request_context,
|
||||
request_client,
|
||||
request_id,
|
||||
document_owner,
|
||||
completion_tx,
|
||||
);
|
||||
}
|
||||
|
||||
fn navigator_service_worker_unregister_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -2364,6 +2482,13 @@ fn build_service_worker_registration_object<'s>(
|
||||
navigation_preload,
|
||||
);
|
||||
let registration = bind_declared_service_worker_object(scope, &declaration);
|
||||
let id = v8::BigInt::new_from_u64(scope, resolved.registration_id.as_u64());
|
||||
set_private_value(
|
||||
scope,
|
||||
registration,
|
||||
SERVICE_WORKER_REGISTRATION_ID_SLOT,
|
||||
id.into(),
|
||||
);
|
||||
mark_service_worker_registration_event_target(scope, registration);
|
||||
if let Some(scope_value) = v8_string(scope, scope_url) {
|
||||
define_non_enumerable_value_property(
|
||||
@@ -2408,6 +2533,12 @@ fn build_service_worker_registration_object_for_container<'s>(
|
||||
let owner = service_worker_container_owner_scope(scope, container);
|
||||
if let Some(registration) =
|
||||
service_worker_container_cached_registration_for_scope(scope, container, scope_url)
|
||||
&& match &phase {
|
||||
ServiceWorkerRegistrationPhase::Snapshot(snapshot) => {
|
||||
service_worker_registration_id_from_object(scope, registration)
|
||||
== Some(snapshot.registration_id())
|
||||
}
|
||||
}
|
||||
{
|
||||
service_worker_worker_set_owner_scope(scope, registration, owner);
|
||||
update_service_worker_registration_object(scope, registration, script_url, phase);
|
||||
@@ -2441,6 +2572,7 @@ fn watch_service_worker_registration_object_lifecycle<'s>(
|
||||
}
|
||||
|
||||
struct ResolvedServiceWorkerRegistrationPhase<'s> {
|
||||
registration_id: crate::service_worker_runtime::ServiceWorkerRegistrationId,
|
||||
installing: v8::Local<'s, v8::Value>,
|
||||
waiting: v8::Local<'s, v8::Value>,
|
||||
active: v8::Local<'s, v8::Value>,
|
||||
@@ -2496,6 +2628,7 @@ fn resolve_service_worker_registration_phase<'s>(
|
||||
})
|
||||
.unwrap_or_else(|| build_service_worker_object(scope, script_url, "redundant"));
|
||||
ResolvedServiceWorkerRegistrationPhase {
|
||||
registration_id: snapshot.registration_id(),
|
||||
installing,
|
||||
waiting,
|
||||
active,
|
||||
@@ -2533,6 +2666,11 @@ fn update_service_worker_registration_object<'s>(
|
||||
) {
|
||||
let resolved =
|
||||
resolve_service_worker_registration_phase(scope, Some(registration), script_url, phase);
|
||||
if service_worker_registration_id_from_object(scope, registration)
|
||||
!= Some(resolved.registration_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
set_service_worker_registration_worker_values(
|
||||
scope,
|
||||
registration,
|
||||
|
||||
@@ -70,6 +70,7 @@ pub(crate) struct PendingServiceWorkerRegister {
|
||||
pub(crate) scope_url: Url,
|
||||
pub(crate) context: v8::Global<v8::Context>,
|
||||
pub(crate) resolver: v8::Global<v8::PromiseResolver>,
|
||||
pub(crate) update_registration: Option<v8::Global<v8::Object>>,
|
||||
}
|
||||
|
||||
pub(crate) struct PendingServiceWorkerReady {
|
||||
@@ -444,6 +445,7 @@ impl JsContextHost {
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
scope_url: Url,
|
||||
resolver: v8::Local<'_, v8::PromiseResolver>,
|
||||
update_registration: Option<v8::Local<'_, v8::Object>>,
|
||||
owner: ServiceWorkerWindowOwner,
|
||||
) -> (
|
||||
u64,
|
||||
@@ -458,6 +460,8 @@ impl JsContextHost {
|
||||
scope_url,
|
||||
context: v8::Global::new(scope, scope.get_current_context()),
|
||||
resolver: v8::Global::new(scope, resolver),
|
||||
update_registration: update_registration
|
||||
.map(|registration| v8::Global::new(scope, registration)),
|
||||
},
|
||||
);
|
||||
tracing::debug!(
|
||||
@@ -673,6 +677,54 @@ impl JsContextHost {
|
||||
self.watch_pending_service_worker_ready();
|
||||
}
|
||||
|
||||
pub(crate) fn start_service_worker_registration_update(
|
||||
&mut self,
|
||||
registration_id: crate::service_worker_runtime::ServiceWorkerRegistrationId,
|
||||
request_context: &ServiceWorkerWindowRequestContext,
|
||||
request_client: crate::network::ResourceRequestClient,
|
||||
request_id: u64,
|
||||
document_owner: WindowDocumentOwner,
|
||||
completion_tx: crate::page_task_queue::RendererPageServiceWorkerTaskSender,
|
||||
) {
|
||||
self.ensure_service_worker_client_for_request_context(request_context);
|
||||
self.browser_context_runtime
|
||||
.service_worker_runtime()
|
||||
.start_registration_update(
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationUpdate {
|
||||
registration_id,
|
||||
caller_version_id: None,
|
||||
storage_key: request_context.serialized_storage_key(),
|
||||
document_url: request_context.document_url().clone(),
|
||||
request_client,
|
||||
network_policy: WorkerNetworkPolicy {
|
||||
secure_context: moli_url::is_potentially_trustworthy_url(
|
||||
request_context.document_url(),
|
||||
),
|
||||
permission_overrides: self.permission_overrides().to_vec(),
|
||||
extra_http_headers: self.extra_http_headers().to_vec(),
|
||||
network_offline: self.network_offline(),
|
||||
blocked_url_patterns: self.blocked_url_patterns().to_vec(),
|
||||
network_partition_key: None,
|
||||
fetch_subresource_interception_enabled: self
|
||||
.fetch_subresource_interception_enabled(),
|
||||
fetch_subresource_interception_resource_type: self
|
||||
.fetch_subresource_interception_resource_type(),
|
||||
},
|
||||
worker_context_runtime: self.browser_context_runtime.worker_context_runtime(),
|
||||
broadcast_channel_top_level_site: Some(
|
||||
request_context.storage_key_top_level_site(),
|
||||
),
|
||||
indexed_db_manager: self.indexed_db_manager(),
|
||||
storage_bucket_store: Some(self.storage_bucket_store()),
|
||||
completion: crate::service_worker_runtime::ServiceWorkerRegisterJob::Page {
|
||||
request_id,
|
||||
document_owner,
|
||||
completion_tx,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn watch_pending_service_worker_ready(&mut self) -> bool {
|
||||
let requests = self.pending_service_worker_ready_requests();
|
||||
let mut attached_any = false;
|
||||
@@ -829,16 +881,17 @@ impl JsContextHost {
|
||||
let owner = request_context.owner();
|
||||
let storage_key = request_context.serialized_storage_key();
|
||||
self.compact_service_worker_registration_watchers();
|
||||
if self
|
||||
.service_worker_registration_watchers
|
||||
.iter()
|
||||
.any(|watcher| {
|
||||
watcher.owner == owner
|
||||
&& watcher.scope_url == scope_url
|
||||
&& watcher.storage_key == storage_key
|
||||
&& !watcher.registration.is_empty()
|
||||
})
|
||||
if let Some(watcher) =
|
||||
self.service_worker_registration_watchers
|
||||
.iter_mut()
|
||||
.find(|watcher| {
|
||||
watcher.owner == owner
|
||||
&& watcher.scope_url == scope_url
|
||||
&& watcher.storage_key == storage_key
|
||||
&& !watcher.registration.is_empty()
|
||||
})
|
||||
{
|
||||
watcher.registration = v8::Weak::new(scope, registration);
|
||||
return;
|
||||
}
|
||||
self.service_worker_registration_watchers
|
||||
|
||||
@@ -291,7 +291,10 @@ impl ResourceRequestClient {
|
||||
cancel_handle: FetchCancelHandle,
|
||||
) -> Result<NetworkFetchResult<Response>> {
|
||||
let request = self.apply_network_policy(request)?;
|
||||
if request.auth_requires_buffered_transport() || !request.follow_redirects {
|
||||
if request.auth_requires_buffered_transport()
|
||||
|| (!request.follow_redirects
|
||||
&& request.redirect_mode != moli_fetch::RequestRedirectMode::Error)
|
||||
{
|
||||
return self
|
||||
.resource_runtime
|
||||
.client()
|
||||
@@ -714,12 +717,16 @@ impl ResourceRequestClient {
|
||||
request: Request,
|
||||
cancel_handle: FetchCancelHandle,
|
||||
) -> Result<Response> {
|
||||
if request.auth_requires_buffered_transport() || !request.follow_redirects {
|
||||
if request.auth_requires_buffered_transport()
|
||||
|| (!request.follow_redirects
|
||||
&& request.redirect_mode != moli_fetch::RequestRedirectMode::Error)
|
||||
{
|
||||
// Challenge-response schemes still need libcurl's buffered auth
|
||||
// retry behavior until the streaming collector models
|
||||
// intermediate authentication challenges explicitly. Manual
|
||||
// redirect callers need the intermediate 3xx response before raw
|
||||
// streaming starts.
|
||||
// streaming starts. Error-mode script fetches can use the raw
|
||||
// stream and its HTTP cache; their caller rejects any 3xx status.
|
||||
return self
|
||||
.resource_runtime
|
||||
.client()
|
||||
@@ -999,7 +1006,10 @@ impl ResourceRequestClient {
|
||||
cancel_handle: Option<FetchCancelHandle>,
|
||||
) -> Result<Response> {
|
||||
let request = self.apply_network_policy(request)?;
|
||||
if request.auth_requires_buffered_transport() || !request.follow_redirects {
|
||||
if request.auth_requires_buffered_transport()
|
||||
|| (!request.follow_redirects
|
||||
&& request.redirect_mode != moli_fetch::RequestRedirectMode::Error)
|
||||
{
|
||||
// Digest auth retries are still completed inside libcurl on the
|
||||
// buffered path. Keep auth requests there until the streaming
|
||||
// collector can distinguish intermediate auth challenges from
|
||||
|
||||
@@ -45,12 +45,17 @@ impl ScriptVm {
|
||||
let context = pending.context;
|
||||
let resolver = pending.resolver;
|
||||
let scope_url = pending.scope_url;
|
||||
let update_registration = pending.update_registration;
|
||||
let context_ptr: *const v8::Global<v8::Context> = &context;
|
||||
self.with_context_scope_by_ptr(context_ptr, move |scope, _host_ptr| {
|
||||
let resolver = v8::Local::new(scope, &resolver);
|
||||
let update_registration = update_registration
|
||||
.as_ref()
|
||||
.map(|value| v8::Local::new(scope, value));
|
||||
crate::context_bootstrap::settle_service_worker_register_completion(
|
||||
scope,
|
||||
resolver,
|
||||
update_registration,
|
||||
owner.dispatch_scope(),
|
||||
&scope_url,
|
||||
completion.result,
|
||||
|
||||
@@ -15655,7 +15655,7 @@ async fn navigator_service_worker_intercepts_connected_stylesheet_link() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn navigator_service_worker_update_check_failure_rejects_register_with_type_error() {
|
||||
async fn navigator_service_worker_update_check_preserves_mime_security_error() {
|
||||
let (base_url, server) = spawn_service_worker_response_server_with_headers(vec![
|
||||
(
|
||||
"/app/worker.js",
|
||||
@@ -15717,7 +15717,7 @@ async fn navigator_service_worker_update_check_failure_rejects_register_with_typ
|
||||
&browser_context_runtime,
|
||||
&loader,
|
||||
"String(globalThis.__serviceWorkerUpdateFailureProbe)",
|
||||
r#"{"name":"TypeError","isTypeError":true,"isDomException":false,"messageIncludesNosniff":true}"#,
|
||||
r#"{"name":"SecurityError","isTypeError":false,"isDomException":true,"messageIncludesNosniff":true}"#,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -18888,6 +18888,263 @@ async fn navigator_service_worker_update_via_cache_option_reflects_registration(
|
||||
.expect("service worker updateViaCache script server should finish");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn navigator_service_worker_update_preserves_identity_and_rejects_stale_receivers() {
|
||||
let (base_url, server) = spawn_service_worker_response_server(vec![
|
||||
("/app/workers/sw.js", "text/javascript", "// first"),
|
||||
("/app/workers/sw.js", "text/javascript", "// second"),
|
||||
("/app/workers/sw.js", "text/javascript", "// second"),
|
||||
(
|
||||
"/app/workers/sw.js",
|
||||
"text/javascript",
|
||||
"// new registration",
|
||||
),
|
||||
("/app/workers/sw.js", "text/javascript", "// new update"),
|
||||
])
|
||||
.await;
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
|
||||
&format!("{base_url}/app/page.html"),
|
||||
&loader,
|
||||
);
|
||||
vm.eval(r#"
|
||||
globalThis.__updateIdentity = 'pending';
|
||||
(async () => {
|
||||
const activated = async r => {
|
||||
const w = r.installing || r.waiting || r.active;
|
||||
if (w.state !== 'activated') await new Promise(resolve => w.addEventListener('statechange', () => {
|
||||
if (w.state === 'activated') resolve();
|
||||
}));
|
||||
};
|
||||
const errorName = async p => { try { await p; return 'success'; } catch(e) { return e.name; } };
|
||||
const register = () => navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'});
|
||||
const r = await register();
|
||||
let found = 0;
|
||||
r.addEventListener('updatefound', () => found++);
|
||||
await activated(r);
|
||||
const initial = found;
|
||||
const receivers = [];
|
||||
for (const fake of [{}, Object.create(r), new Proxy(r, {}), null]) {
|
||||
const p = Reflect.apply(r.update, fake, []);
|
||||
receivers.push(p instanceof Promise && await errorName(p) === 'TypeError');
|
||||
}
|
||||
const before = r.active;
|
||||
const order = [];
|
||||
r.addEventListener('updatefound', () => order.push('event'), {once:true});
|
||||
const updated = await r.update(); order.push('promise');
|
||||
await activated(r);
|
||||
const changed = before !== r.active;
|
||||
const current = r.active;
|
||||
const unchanged = await r.update({get unused(){throw new Error('argument read');}});
|
||||
const stable = unchanged === r && current === r.active && found === 2;
|
||||
await r.unregister();
|
||||
const removed = await errorName(r.update());
|
||||
const replacement = await register();
|
||||
await activated(replacement);
|
||||
const stale = await errorName(r.update());
|
||||
const again = await replacement.update();
|
||||
await activated(replacement);
|
||||
const lookup = await navigator.serviceWorker.getRegistration('workers/client');
|
||||
globalThis.__updateIdentity = [r.update.name, r.update.length, initial,
|
||||
receivers.every(Boolean), updated === r, changed, order.join(','), stable,
|
||||
removed, stale, replacement !== r, again === replacement, lookup === replacement].join('|');
|
||||
await replacement.unregister();
|
||||
})().catch(e => globalThis.__updateIdentity = 'error:' + e);
|
||||
"#).expect("update identity probe");
|
||||
drain_service_worker_test_until_eval_equals(&mut vm, &runtime, &loader,
|
||||
"String(globalThis.__updateIdentity)",
|
||||
"update|0|1|true|true|true|promise,event|true|InvalidStateError|InvalidStateError|true|true|true",
|
||||
).await;
|
||||
server.await.expect("update identity server");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn navigator_service_worker_update_reuses_failed_import_responses() {
|
||||
const MAIN: &str = "importScripts('a.js', 'z.js');";
|
||||
let (base_url, server) = spawn_service_worker_response_server(vec![
|
||||
("/app/workers/sw.js", "text/javascript", MAIN),
|
||||
("/app/workers/a.js", "text/javascript", "// a"),
|
||||
("/app/workers/z.js", "text/javascript", "// z1"),
|
||||
("/app/workers/sw.js", "text/javascript", MAIN),
|
||||
("/app/workers/a.js", "text/html", "missing import"),
|
||||
("/app/workers/z.js", "text/javascript", "// z2"),
|
||||
(
|
||||
"/app/workers/sw.js",
|
||||
"text/javascript",
|
||||
"importScripts('z.js');",
|
||||
),
|
||||
("/app/workers/z.js", "text/javascript", "// z3"),
|
||||
])
|
||||
.await;
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
|
||||
&format!("{base_url}/app/page.html"),
|
||||
&loader,
|
||||
);
|
||||
vm.eval(r#"
|
||||
globalThis.__updateImports = 'pending';
|
||||
(async () => {
|
||||
const activated = async r => {
|
||||
const w = r.installing || r.waiting || r.active;
|
||||
if (w.state !== 'activated') await new Promise(resolve => w.addEventListener('statechange', () => {
|
||||
if (w.state === 'activated') resolve();
|
||||
}));
|
||||
};
|
||||
const r = await navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'});
|
||||
await activated(r);
|
||||
const before = r.active;
|
||||
let found = 0;
|
||||
r.addEventListener('updatefound', () => found++);
|
||||
let error = 'success';
|
||||
try { await r.update(); } catch(e) { error = e.name; }
|
||||
const failed = error === 'TypeError' && found === 0 && r.active === before && r.installing === null;
|
||||
const updated = await r.update();
|
||||
await activated(r);
|
||||
globalThis.__updateImports = [failed, updated === r, r.active !== before, found].join('|');
|
||||
await r.unregister();
|
||||
})().catch(e => globalThis.__updateImports = 'error:' + e);
|
||||
"#).expect("update import response probe");
|
||||
drain_service_worker_test_until_eval_equals(
|
||||
&mut vm,
|
||||
&runtime,
|
||||
&loader,
|
||||
"String(globalThis.__updateImports)",
|
||||
"true|true|true|1",
|
||||
)
|
||||
.await;
|
||||
server
|
||||
.await
|
||||
.expect("update import server should not refetch failed responses");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn navigator_service_worker_update_discards_prefetched_but_unused_imports_after_install() {
|
||||
const MAIN: &str = r#"
|
||||
importScripts('flag.js');
|
||||
if (!self.skipOld) importScripts('unused.js');
|
||||
onmessage = e => {
|
||||
try { importScripts('unused.js'); e.ports[0].postMessage('success'); }
|
||||
catch(error) { e.ports[0].postMessage(error.name); }
|
||||
};
|
||||
"#;
|
||||
let (base_url, server) = spawn_service_worker_response_server(vec![
|
||||
("/app/workers/sw.js", "text/javascript", MAIN),
|
||||
(
|
||||
"/app/workers/flag.js",
|
||||
"text/javascript",
|
||||
"self.skipOld = false;",
|
||||
),
|
||||
("/app/workers/unused.js", "text/javascript", "// unused"),
|
||||
("/app/workers/sw.js", "text/javascript", MAIN),
|
||||
(
|
||||
"/app/workers/flag.js",
|
||||
"text/javascript",
|
||||
"self.skipOld = true;",
|
||||
),
|
||||
("/app/workers/unused.js", "text/javascript", "// unused"),
|
||||
])
|
||||
.await;
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
|
||||
&format!("{base_url}/app/page.html"),
|
||||
&loader,
|
||||
);
|
||||
vm.eval(r#"
|
||||
globalThis.__unusedUpdateImport = 'pending';
|
||||
(async () => {
|
||||
const activated = async r => {
|
||||
const w = r.installing || r.waiting || r.active;
|
||||
if (w.state !== 'activated') await new Promise(resolve => w.addEventListener('statechange', () => {
|
||||
if (w.state === 'activated') resolve();
|
||||
}));
|
||||
};
|
||||
const r = await navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'});
|
||||
await activated(r);
|
||||
await r.update(); await activated(r);
|
||||
const value = await new Promise(resolve => {
|
||||
const c = new MessageChannel();
|
||||
c.port1.onmessage = e => { c.port1.close(); resolve(e.data); };
|
||||
r.active.postMessage('probe', [c.port2]);
|
||||
});
|
||||
globalThis.__unusedUpdateImport = value;
|
||||
await r.unregister();
|
||||
})().catch(e => globalThis.__unusedUpdateImport = 'error:' + e);
|
||||
"#).expect("unused update import probe");
|
||||
drain_service_worker_test_until_eval_equals(
|
||||
&mut vm,
|
||||
&runtime,
|
||||
&loader,
|
||||
"String(globalThis.__unusedUpdateImport)",
|
||||
"NetworkError",
|
||||
)
|
||||
.await;
|
||||
server
|
||||
.await
|
||||
.expect("prefetched imports should not be fetched again");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn navigator_service_worker_update_in_worker_preserves_events_and_install_rejection() {
|
||||
let (base_url, server) = spawn_service_worker_response_server(vec![
|
||||
("/app/workers/sw.js", "text/javascript", r#"
|
||||
const original = registration;
|
||||
const seen = [];
|
||||
let installError;
|
||||
original.addEventListener('updatefound', () => seen.push('before'));
|
||||
original.onupdatefound = function(e) {
|
||||
seen.push([this === original, e.target === original, e.currentTarget === original,
|
||||
e instanceof Event, e.isTrusted, e.bubbles, e.cancelable].join(':'));
|
||||
};
|
||||
original.addEventListener('updatefound', () => seen.push('after'));
|
||||
oninstall = e => e.waitUntil(original.update().then(
|
||||
() => installError = 'success', e => installError = e.name));
|
||||
onactivate = () => seen.push('activate');
|
||||
onmessage = e => {
|
||||
if (e.data === 'sample') e.ports[0].postMessage(seen.join(',') + '|' + installError);
|
||||
else e.waitUntil(original.update().then(r => e.ports[0].postMessage(r === original)));
|
||||
};
|
||||
Object.defineProperty(self, 'registration', {get(){throw new Error('public registration read');}});
|
||||
"#),
|
||||
("/app/workers/sw.js", "text/javascript", "// updated"),
|
||||
]).await;
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let (mut vm, runtime) = new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
|
||||
&format!("{base_url}/app/page.html"),
|
||||
&loader,
|
||||
);
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.__workerUpdate = 'pending';
|
||||
(async () => {
|
||||
const r = await navigator.serviceWorker.register('workers/sw.js', {scope:'workers/'});
|
||||
const w = r.installing;
|
||||
await new Promise(resolve => w.addEventListener('statechange', () => {
|
||||
if (w.state === 'activated') resolve();
|
||||
}));
|
||||
const message = data => new Promise(resolve => {
|
||||
const c = new MessageChannel();
|
||||
c.port1.onmessage = e => { c.port1.close(); resolve(e.data); };
|
||||
w.postMessage(data, [c.port2]);
|
||||
});
|
||||
const first = await message('sample');
|
||||
const same = await message('update');
|
||||
globalThis.__workerUpdate = first + '|' + same;
|
||||
await r.unregister();
|
||||
})().catch(e => globalThis.__workerUpdate = 'error:' + e);
|
||||
"#,
|
||||
)
|
||||
.expect("worker update probe");
|
||||
drain_service_worker_test_until_eval_equals(
|
||||
&mut vm,
|
||||
&runtime,
|
||||
&loader,
|
||||
"String(globalThis.__workerUpdate)",
|
||||
"before,true:true:true:true:true:false:false,after,activate|InvalidStateError|true",
|
||||
)
|
||||
.await;
|
||||
server.await.expect("worker update server");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn navigator_service_worker_update_via_cache_all_uses_fresh_main_script_cache() {
|
||||
let cache_dir = service_worker_http_cache_test_root("update-via-cache-all");
|
||||
@@ -18907,12 +19164,33 @@ async fn navigator_service_worker_update_via_cache_all_uses_fresh_main_script_ca
|
||||
.await;
|
||||
let mut fetch_config = moli_fetch::FetchConfig::default();
|
||||
fetch_config.set_http_cache_dir(Some(cache_dir.display().to_string()));
|
||||
let loader = ResourceRequestClient::new(&fetch_config).expect("loader");
|
||||
let (mut vm, browser_context_runtime) =
|
||||
new_service_worker_page_test_vm_with_loader_and_browser_context_runtime(
|
||||
&format!("{base_url}/app/page.html"),
|
||||
&loader,
|
||||
);
|
||||
let browser_context_runtime = crate::runtime::RendererBrowserContextRuntime::new();
|
||||
let resource_runtime = browser_context_runtime
|
||||
.replace_browser_resource_runtime(crate::network::BrowserResourceRuntimeOwner::new(
|
||||
&fetch_config,
|
||||
moli_cookie_jar::new_shared_browser_cookie_store(),
|
||||
))
|
||||
.expect("cache-enabled browser resource runtime");
|
||||
let loader = ResourceRequestClient::from_browser_resource_runtime(resource_runtime);
|
||||
let mut vm = crate::runtime::PageVmTaskExecutorTestHarness::new_with_browser_context_runtime(
|
||||
url::Url::parse(&format!("{base_url}/app/page.html")).unwrap(),
|
||||
&loader,
|
||||
browser_context_runtime.handle(),
|
||||
);
|
||||
// Execute register and update with the same Document transport, including
|
||||
// its browser-site context used to partition the HTTP cache.
|
||||
let loader = vm
|
||||
._context_host
|
||||
.borrow()
|
||||
.current_main_document_resource_loader()
|
||||
.expect("current Document loader")
|
||||
.request_client()
|
||||
.clone();
|
||||
assert!(
|
||||
loader
|
||||
.browser_resource_runtime()
|
||||
.matches_fetch_config(&fetch_config)
|
||||
);
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
@@ -18933,6 +19211,8 @@ async fn navigator_service_worker_update_via_cache_all_uses_fresh_main_script_ca
|
||||
scope: "./",
|
||||
updateViaCache: "all"
|
||||
});
|
||||
const updated = await registration.update();
|
||||
if (updated !== registration) throw new Error('update identity changed');
|
||||
const all = await sw.getRegistrations();
|
||||
globalThis.__serviceWorkerUpdateViaCacheAllProbe = [
|
||||
updatefoundCount,
|
||||
|
||||
@@ -66,7 +66,9 @@ pub(crate) use ids::{
|
||||
ServiceWorkerClientId, ServiceWorkerClientIdAllocator, ServiceWorkerEventId,
|
||||
ServiceWorkerRegistrationId, ServiceWorkerVersionId,
|
||||
};
|
||||
pub(crate) use jobs::ServiceWorkerUnregisterStart;
|
||||
pub(crate) use jobs::{
|
||||
ServiceWorkerRegisterJob, ServiceWorkerRegistrationUpdate, ServiceWorkerUnregisterStart,
|
||||
};
|
||||
pub(crate) use owner_wake::{
|
||||
ServiceWorkerRuntimeOwnerWake, ServiceWorkerRuntimeOwnerWakeSender,
|
||||
service_worker_owner_wake_channel,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
pub(crate) enum ServiceWorkerRegistrationErrorKind {
|
||||
Abort,
|
||||
Install,
|
||||
InvalidState,
|
||||
Network,
|
||||
NotFound,
|
||||
ScriptEvaluateFailed,
|
||||
@@ -15,6 +16,7 @@ impl ServiceWorkerRegistrationErrorKind {
|
||||
match self {
|
||||
Self::Abort | Self::Install | Self::ScriptEvaluateFailed => "AbortError",
|
||||
Self::Network => "NetworkError",
|
||||
Self::InvalidState => "InvalidStateError",
|
||||
Self::NotFound => "NotFoundError",
|
||||
Self::Security => "SecurityError",
|
||||
Self::Type => unreachable!("type errors are not DOMExceptions"),
|
||||
|
||||
@@ -227,6 +227,16 @@ impl RendererServiceWorkerHost {
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_registration_update_found(&self) {
|
||||
let state = self.state.lock();
|
||||
if let RendererServiceWorkerHostState::Running {
|
||||
handle: Some(handle),
|
||||
} = &*state
|
||||
{
|
||||
handle.dispatch_service_worker_registration_update_found();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_fetch_event(&self, event: ServiceWorkerFetchEvent) -> bool {
|
||||
let state = self.state.lock();
|
||||
let RendererServiceWorkerHostState::Running {
|
||||
@@ -991,6 +1001,7 @@ fn spawn_service_worker(
|
||||
script_map.imported_scripts,
|
||||
script_map.can_import_new_scripts,
|
||||
)
|
||||
.with_service_worker_updated_script_resources(script.updated_imports)
|
||||
.with_global_kind(crate::worker::WorkerGlobalKind::Service {
|
||||
registration_id: params.registration_id,
|
||||
version_id: params.run_owner.version_id(),
|
||||
|
||||
@@ -6,7 +6,7 @@ use url::Url;
|
||||
use crate::{
|
||||
network::{BrowserResourceRuntimeBinding, ResourceRequestClient},
|
||||
page_task_queue::RendererPageServiceWorkerTaskSender,
|
||||
runtime::{RendererBrowserContextRuntime, RendererWorkerContextRuntime},
|
||||
runtime::RendererWorkerContextRuntime,
|
||||
types::{ServiceWorkerRegisterCompletion, ServiceWorkerUnregisterCompletion},
|
||||
worker::{WorkerNetworkPolicy, WorkerScriptKind},
|
||||
};
|
||||
@@ -39,10 +39,16 @@ pub(crate) struct ServiceWorkerLaunchParams {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct ServiceWorkerRegisterJob {
|
||||
pub(super) request_id: u64,
|
||||
pub(super) document_owner: crate::window_document_identity::WindowDocumentOwner,
|
||||
pub(super) completion_tx: RendererPageServiceWorkerTaskSender,
|
||||
pub(crate) enum ServiceWorkerRegisterJob {
|
||||
Page {
|
||||
request_id: u64,
|
||||
document_owner: crate::window_document_identity::WindowDocumentOwner,
|
||||
completion_tx: RendererPageServiceWorkerTaskSender,
|
||||
},
|
||||
Worker {
|
||||
request_id: u64,
|
||||
completion_tx: tokio::sync::mpsc::UnboundedSender<crate::worker::WorkerMessage>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ServiceWorkerRegisterJob {
|
||||
@@ -53,13 +59,30 @@ impl ServiceWorkerRegisterJob {
|
||||
ServiceWorkerRegistrationError,
|
||||
>,
|
||||
) {
|
||||
let _ = self
|
||||
.completion_tx
|
||||
.send_service_worker_register(ServiceWorkerRegisterCompletion {
|
||||
request_id: self.request_id,
|
||||
document_owner: self.document_owner,
|
||||
result,
|
||||
});
|
||||
match self {
|
||||
Self::Page {
|
||||
request_id,
|
||||
document_owner,
|
||||
completion_tx,
|
||||
} => {
|
||||
let _ =
|
||||
completion_tx.send_service_worker_register(ServiceWorkerRegisterCompletion {
|
||||
request_id,
|
||||
document_owner,
|
||||
result,
|
||||
});
|
||||
}
|
||||
Self::Worker {
|
||||
request_id,
|
||||
completion_tx,
|
||||
} => {
|
||||
let _ =
|
||||
completion_tx.send(crate::worker::WorkerMessage::ServiceWorkerUpdateResult {
|
||||
request_id,
|
||||
result: Box::new(result),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn send_all(
|
||||
@@ -75,6 +98,20 @@ impl ServiceWorkerRegisterJob {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ServiceWorkerRegistrationUpdate {
|
||||
pub(crate) registration_id: ServiceWorkerRegistrationId,
|
||||
pub(crate) caller_version_id: Option<ServiceWorkerVersionId>,
|
||||
pub(crate) storage_key: String,
|
||||
pub(crate) document_url: Url,
|
||||
pub(crate) request_client: ResourceRequestClient,
|
||||
pub(crate) network_policy: WorkerNetworkPolicy,
|
||||
pub(crate) worker_context_runtime: RendererWorkerContextRuntime,
|
||||
pub(crate) broadcast_channel_top_level_site: Option<String>,
|
||||
pub(crate) indexed_db_manager: Option<crate::context_bootstrap::WeakIndexedDbManager>,
|
||||
pub(crate) storage_bucket_store: Option<crate::context_bootstrap::SharedStorageBucketStore>,
|
||||
pub(crate) completion: ServiceWorkerRegisterJob,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum ServiceWorkerRegisterJobPhase {
|
||||
Initial,
|
||||
@@ -89,6 +126,7 @@ pub(super) enum ServiceWorkerRegisterJobPhase {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct ServiceWorkerPendingRegisterJob {
|
||||
pub(super) is_update: bool,
|
||||
phase: ServiceWorkerRegisterJobPhase,
|
||||
skip_waiting_after_install: bool,
|
||||
callbacks: Vec<ServiceWorkerRegisterJob>,
|
||||
@@ -109,6 +147,7 @@ impl ServiceWorkerPendingRegisterJob {
|
||||
) -> Self {
|
||||
Self {
|
||||
phase: ServiceWorkerRegisterJobPhase::Initial,
|
||||
is_update: false,
|
||||
skip_waiting_after_install,
|
||||
callbacks,
|
||||
resolved_result: None,
|
||||
@@ -250,7 +289,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pending_register_job_resolves_when_install_starts() {
|
||||
let queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new();
|
||||
let mut job = ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob {
|
||||
let mut job = ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id: 1,
|
||||
document_owner: crate::window_document_identity::WindowDocumentOwner::for_test(1),
|
||||
completion_tx: queue.sender(),
|
||||
@@ -353,6 +392,9 @@ impl ServiceWorkerQueuedUnregisterJob {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ServiceWorkerQueuedRegisterJob {
|
||||
/// Web API update jobs retain their registration identity and cannot
|
||||
/// create a new registration or clear a pending unregistration.
|
||||
pub(super) update_registration_id: Option<ServiceWorkerRegistrationId>,
|
||||
pub(super) script_url: Url,
|
||||
pub(super) scope_url: Url,
|
||||
pub(super) document_url: Url,
|
||||
@@ -365,7 +407,7 @@ pub(super) struct ServiceWorkerQueuedRegisterJob {
|
||||
pub(super) force_update_page_load_waiter_ids: Vec<u64>,
|
||||
pub(super) request_client: ResourceRequestClient,
|
||||
pub(super) network_policy: WorkerNetworkPolicy,
|
||||
pub(super) browser_context_runtime: RendererBrowserContextRuntime,
|
||||
pub(super) worker_context_runtime: RendererWorkerContextRuntime,
|
||||
pub(super) broadcast_channel_top_level_site: Option<String>,
|
||||
pub(super) indexed_db_manager: Option<crate::context_bootstrap::WeakIndexedDbManager>,
|
||||
pub(super) storage_bucket_store: Option<crate::context_bootstrap::SharedStorageBucketStore>,
|
||||
@@ -381,7 +423,8 @@ impl ServiceWorkerQueuedRegisterJob {
|
||||
}
|
||||
|
||||
pub(super) fn matches_registration_job(&self, other: &Self) -> bool {
|
||||
self.scope_url == other.scope_url
|
||||
self.update_registration_id == other.update_registration_id
|
||||
&& self.scope_url == other.scope_url
|
||||
&& self.storage_key == other.storage_key
|
||||
&& self.script_url == other.script_url
|
||||
&& self.script_kind == other.script_kind
|
||||
@@ -461,6 +504,7 @@ pub(super) enum ServiceWorkerMainScriptUpdateCheckStart {
|
||||
impl std::fmt::Debug for ServiceWorkerQueuedRegisterJob {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ServiceWorkerQueuedRegisterJob")
|
||||
.field("update_registration_id", &self.update_registration_id)
|
||||
.field("script_url", &self.script_url)
|
||||
.field("scope_url", &self.scope_url)
|
||||
.field("document_url", &self.document_url)
|
||||
@@ -718,7 +762,7 @@ impl ServiceWorkerVersionLaunchConfig {
|
||||
document_url: job.document_url.clone(),
|
||||
request_client: ServiceWorkerRequestClientSource::Captured(job.request_client.clone()),
|
||||
network_policy: job.network_policy.clone(),
|
||||
worker_context_runtime: job.browser_context_runtime.worker_context_runtime(),
|
||||
worker_context_runtime: job.worker_context_runtime.clone(),
|
||||
broadcast_channel_top_level_site: job.broadcast_channel_top_level_site.clone(),
|
||||
indexed_db_manager: job.indexed_db_manager.clone(),
|
||||
storage_bucket_store: job.storage_bucket_store.clone(),
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::time::Instant;
|
||||
|
||||
use moli_crypto::sha256_hex;
|
||||
use moli_fetch::{
|
||||
FetchCancelHandle, Request, RequestCacheMode, RequestCredentialsMode, ResponseHead,
|
||||
ScriptFetchRequestMetadata,
|
||||
FetchCancelHandle, Request, RequestCacheMode, RequestCredentialsMode, RequestMode,
|
||||
RequestRedirectMode, ResponseHead, ScriptFetchRequestMetadata,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::network::ResourceRequestClient;
|
||||
use crate::worker::WorkerScriptResourceKind;
|
||||
|
||||
use super::{
|
||||
errors::{ServiceWorkerRegistrationError, ServiceWorkerRegistrationErrorKind},
|
||||
jobs::ServiceWorkerLaunchParams,
|
||||
path_restriction::{
|
||||
service_worker_allowed_header_value, verify_service_worker_script_path_restriction,
|
||||
@@ -41,6 +42,7 @@ impl ServiceWorkerScriptLoadParams {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ServiceWorkerScriptUpdateCheckParams {
|
||||
pub(super) script_kind: crate::worker::WorkerScriptKind,
|
||||
pub(super) main_script: ServiceWorkerScriptLoadParams,
|
||||
pub(super) newest_main_body_sha256: String,
|
||||
pub(super) imported_scripts: Vec<ServiceWorkerScriptResource>,
|
||||
@@ -125,6 +127,7 @@ pub(super) struct ServiceWorkerScriptMapSnapshot {
|
||||
}
|
||||
|
||||
pub(super) struct LoadedServiceWorkerScript {
|
||||
pub(super) updated_imports: crate::worker::WorkerScriptUpdateResources,
|
||||
pub(super) resource: ServiceWorkerScriptResource,
|
||||
pub(super) source: String,
|
||||
pub(super) response_referrer_policy: Option<String>,
|
||||
@@ -147,6 +150,7 @@ impl LoadedServiceWorkerScript {
|
||||
);
|
||||
let response_content_security_reporting_endpoints = crate::content_security_policy::content_security_policy_reporting_endpoints_from_headers(&resource.headers, &resource.final_url);
|
||||
Some(Self {
|
||||
updated_imports: Default::default(),
|
||||
resource,
|
||||
source,
|
||||
response_referrer_policy,
|
||||
@@ -173,6 +177,7 @@ pub(super) enum ServiceWorkerScriptUpdateCheckChange {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum ServiceWorkerScriptUpdateCheckFailureStatus {
|
||||
ScriptLoadFailed,
|
||||
Security,
|
||||
Internal,
|
||||
Stale,
|
||||
}
|
||||
@@ -181,6 +186,7 @@ impl ServiceWorkerScriptUpdateCheckFailureStatus {
|
||||
pub(super) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::ScriptLoadFailed => "script-load-failed",
|
||||
Self::Security => "security",
|
||||
Self::Internal => "internal",
|
||||
Self::Stale => "stale",
|
||||
}
|
||||
@@ -229,18 +235,21 @@ pub(super) fn load_service_worker_script_source(
|
||||
load_service_worker_script_source_for_params(
|
||||
&ServiceWorkerScriptLoadParams::from_launch_params(params),
|
||||
)
|
||||
.map_err(|error| error.message)
|
||||
}
|
||||
|
||||
pub(super) fn load_service_worker_script_source_for_params(
|
||||
params: &ServiceWorkerScriptLoadParams,
|
||||
) -> Result<LoadedServiceWorkerScript, String> {
|
||||
) -> Result<LoadedServiceWorkerScript, ServiceWorkerRegistrationError> {
|
||||
let request_client = ¶ms.request_client;
|
||||
let mut request_url = params.script_url.clone();
|
||||
request_url.set_fragment(None);
|
||||
let response_started_at = Instant::now();
|
||||
let request = Request::new("GET", request_url.as_str(), None, vec![])
|
||||
.map_err(|error| error.to_string())?
|
||||
.map_err(|error| ServiceWorkerRegistrationError::network(error.to_string()))?
|
||||
.with_credentials_mode(RequestCredentialsMode::SameOrigin)
|
||||
.with_request_mode(RequestMode::SameOrigin)
|
||||
.with_redirect_mode(RequestRedirectMode::Error)
|
||||
.with_cache_mode(params.cache_mode)
|
||||
.with_page_network_policy()
|
||||
.with_initiator_url(¶ms.document_url)
|
||||
@@ -251,7 +260,9 @@ pub(super) fn load_service_worker_script_source_for_params(
|
||||
let response = request_client
|
||||
.fetch_text_for_worker_blocking_boundary_with_cancel(request, FetchCancelHandle::new())
|
||||
.map_err(|error| {
|
||||
format!("Failed to load service worker script `{request_url}`: {error}")
|
||||
ServiceWorkerRegistrationError::network(format!(
|
||||
"Failed to load service worker script `{request_url}`: {error}"
|
||||
))
|
||||
})?;
|
||||
let response_time_ms = response_started_at
|
||||
.elapsed()
|
||||
@@ -263,21 +274,30 @@ pub(super) fn load_service_worker_script_source_for_params(
|
||||
&response.final_url,
|
||||
)
|
||||
.map_err(|message| {
|
||||
format!("Failed to load service worker script `{request_url}`: {message}")
|
||||
ServiceWorkerRegistrationError::new(
|
||||
ServiceWorkerRegistrationErrorKind::Security,
|
||||
format!("Failed to load service worker script `{request_url}`: {message}"),
|
||||
)
|
||||
})?;
|
||||
moli_fetch::ensure_http_status_success(response.final_url.as_str(), response.status, false)
|
||||
.map_err(|error| error.to_string())?;
|
||||
.map_err(|error| ServiceWorkerRegistrationError::network(error.to_string()))?;
|
||||
crate::worker::ensure_worker_script_mime_acceptable(
|
||||
&response.final_url,
|
||||
&response.headers,
|
||||
response.body_bytes(),
|
||||
)?;
|
||||
)
|
||||
.map_err(|message| {
|
||||
ServiceWorkerRegistrationError::new(ServiceWorkerRegistrationErrorKind::Security, message)
|
||||
})?;
|
||||
let service_worker_allowed_header = service_worker_allowed_header_value(&response.headers);
|
||||
verify_service_worker_script_path_restriction(
|
||||
¶ms.scope_url,
|
||||
&response.final_url,
|
||||
service_worker_allowed_header.as_deref(),
|
||||
)?;
|
||||
)
|
||||
.map_err(|message| {
|
||||
ServiceWorkerRegistrationError::new(ServiceWorkerRegistrationErrorKind::Security, message)
|
||||
})?;
|
||||
let response_referrer_policy =
|
||||
crate::referrer_policy::response_referrer_policy_from_headers(&response.headers);
|
||||
let response_content_security_policies =
|
||||
@@ -311,6 +331,7 @@ pub(super) fn load_service_worker_script_source_for_params(
|
||||
final_url.set_fragment(params.script_url.fragment());
|
||||
resource.final_url = final_url;
|
||||
Ok(LoadedServiceWorkerScript {
|
||||
updated_imports: Default::default(),
|
||||
resource,
|
||||
source: body,
|
||||
response_referrer_policy,
|
||||
@@ -323,8 +344,14 @@ pub(super) fn load_service_worker_script_source_for_params(
|
||||
pub(super) fn load_service_worker_script_update_check(
|
||||
params: &ServiceWorkerScriptUpdateCheckParams,
|
||||
) -> ServiceWorkerScriptUpdateCheckCompletion {
|
||||
let main_script = load_service_worker_script_source_for_params(¶ms.main_script)
|
||||
.map_err(ServiceWorkerScriptUpdateCheckFailure::script_load)?;
|
||||
let mut main_script = load_service_worker_script_source_for_params(¶ms.main_script)
|
||||
.map_err(|error| {
|
||||
let mut failure = ServiceWorkerScriptUpdateCheckFailure::script_load(error.message);
|
||||
if error.kind == ServiceWorkerRegistrationErrorKind::Security {
|
||||
failure.status = ServiceWorkerScriptUpdateCheckFailureStatus::Security;
|
||||
}
|
||||
failure
|
||||
})?;
|
||||
if params.skip_script_comparison {
|
||||
return Ok(ServiceWorkerScriptUpdateCheckResult {
|
||||
main_script,
|
||||
@@ -338,22 +365,26 @@ pub(super) fn load_service_worker_script_update_check(
|
||||
});
|
||||
}
|
||||
let request_client = ¶ms.main_script.request_client;
|
||||
let mut change = ServiceWorkerScriptUpdateCheckChange::Identical;
|
||||
for imported_script in ¶ms.imported_scripts {
|
||||
if !matches!(imported_script.request_url.scheme(), "http" | "https") {
|
||||
continue;
|
||||
}
|
||||
let result = load_imported_script_resource_for_update_check(
|
||||
request_client,
|
||||
&imported_script.request_url,
|
||||
&main_script.resource.final_url,
|
||||
params.imported_script_cache_mode,
|
||||
imported_script.kind,
|
||||
params.script_kind == crate::worker::WorkerScriptKind::Classic,
|
||||
);
|
||||
match result {
|
||||
match &result {
|
||||
Ok(updated_resource) if updated_resource.body_sha256 != imported_script.body_sha256 => {
|
||||
return Ok(ServiceWorkerScriptUpdateCheckResult {
|
||||
main_script,
|
||||
change: ServiceWorkerScriptUpdateCheckChange::ImportedScriptDifferent {
|
||||
if change == ServiceWorkerScriptUpdateCheckChange::Identical {
|
||||
change = ServiceWorkerScriptUpdateCheckChange::ImportedScriptDifferent {
|
||||
script_url: imported_script.request_url.clone(),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(message) => {
|
||||
@@ -364,10 +395,16 @@ pub(super) fn load_service_worker_script_update_check(
|
||||
);
|
||||
}
|
||||
}
|
||||
// The candidate worker must consume these exact responses, including
|
||||
// failures, rather than fetching a third version during evaluation.
|
||||
main_script.updated_imports.insert(
|
||||
imported_script.request_url.clone(),
|
||||
result.map(|resource| resource.to_worker_script_resource()),
|
||||
);
|
||||
}
|
||||
Ok(ServiceWorkerScriptUpdateCheckResult {
|
||||
main_script,
|
||||
change: ServiceWorkerScriptUpdateCheckChange::Identical,
|
||||
change,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -377,12 +414,18 @@ fn load_imported_script_resource_for_update_check(
|
||||
initiator_url: &Url,
|
||||
cache_mode: RequestCacheMode,
|
||||
kind: WorkerScriptResourceKind,
|
||||
classic: bool,
|
||||
) -> Result<ServiceWorkerScriptResource, String> {
|
||||
let mut request_url_without_fragment = request_url.clone();
|
||||
request_url_without_fragment.set_fragment(None);
|
||||
let request = Request::new("GET", request_url_without_fragment.as_str(), None, vec![])
|
||||
.map_err(|error| error.to_string())?
|
||||
.with_credentials_mode(RequestCredentialsMode::SameOrigin)
|
||||
.with_request_mode(if classic {
|
||||
RequestMode::NoCors
|
||||
} else {
|
||||
RequestMode::Cors
|
||||
})
|
||||
.with_cache_mode(cache_mode)
|
||||
.with_page_network_policy()
|
||||
.with_initiator_url(initiator_url)
|
||||
@@ -402,7 +445,8 @@ fn load_imported_script_resource_for_update_check(
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
.min(u64::MAX as u128) as u64;
|
||||
crate::worker::ensure_worker_script_redirect_chain_same_origin(
|
||||
if !classic {
|
||||
crate::worker::ensure_worker_script_redirect_chain_same_origin(
|
||||
initiator_url,
|
||||
&response.redirect_chain,
|
||||
&response.final_url,
|
||||
@@ -412,19 +456,38 @@ fn load_imported_script_resource_for_update_check(
|
||||
"Failed to load service worker imported script `{request_url_without_fragment}`: {message}"
|
||||
)
|
||||
})?;
|
||||
}
|
||||
moli_fetch::ensure_http_status_success(response.final_url.as_str(), response.status, false)
|
||||
.map_err(|error| error.to_string())?;
|
||||
ensure_imported_script_resource_mime(kind, &response)?;
|
||||
let (head, body_bytes) = response.into_byte_parts();
|
||||
let (head, body, body_bytes) = response.into_parts();
|
||||
let mut resource = ServiceWorkerScriptResource::from_response_parts(
|
||||
request_url_without_fragment,
|
||||
request_url.clone(),
|
||||
&head,
|
||||
&body_bytes,
|
||||
response_time_ms,
|
||||
);
|
||||
resource.kind = kind;
|
||||
if classic {
|
||||
resource.classic_script = Some(crate::worker::WorkerStoredClassicScript {
|
||||
source: body.into(),
|
||||
muted_errors: moli_url::WebOrigin::from_url(initiator_url)
|
||||
!= moli_url::WebOrigin::from_url(&head.final_url)
|
||||
|| head.redirect_chain.iter().any(|redirect| {
|
||||
moli_url::WebOrigin::from_url(initiator_url)
|
||||
!= moli_url::WebOrigin::from_url(&redirect.to_url)
|
||||
}),
|
||||
redirect_urls: head
|
||||
.redirect_chain
|
||||
.iter()
|
||||
.map(|redirect| redirect.to_url.clone())
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
let mut final_url = head.final_url;
|
||||
final_url.set_fragment(request_url.fragment());
|
||||
if !classic {
|
||||
final_url.set_fragment(request_url.fragment());
|
||||
}
|
||||
resource.final_url = final_url;
|
||||
Ok(resource)
|
||||
}
|
||||
@@ -535,6 +598,7 @@ mod tests {
|
||||
|
||||
let result =
|
||||
load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: crate::worker::WorkerScriptKind::Classic,
|
||||
main_script: ServiceWorkerScriptLoadParams {
|
||||
script_url: script_url.clone(),
|
||||
scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(),
|
||||
@@ -576,6 +640,7 @@ mod tests {
|
||||
|
||||
let result =
|
||||
load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: crate::worker::WorkerScriptKind::Classic,
|
||||
main_script: ServiceWorkerScriptLoadParams {
|
||||
script_url: script_url.clone(),
|
||||
scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(),
|
||||
@@ -623,6 +688,7 @@ mod tests {
|
||||
|
||||
let result =
|
||||
load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: crate::worker::WorkerScriptKind::Module,
|
||||
main_script: ServiceWorkerScriptLoadParams {
|
||||
script_url,
|
||||
scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(),
|
||||
@@ -676,6 +742,7 @@ mod tests {
|
||||
|
||||
let result =
|
||||
load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: crate::worker::WorkerScriptKind::Module,
|
||||
main_script: ServiceWorkerScriptLoadParams {
|
||||
script_url,
|
||||
scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(),
|
||||
@@ -729,6 +796,7 @@ mod tests {
|
||||
|
||||
let result =
|
||||
load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: crate::worker::WorkerScriptKind::Module,
|
||||
main_script: ServiceWorkerScriptLoadParams {
|
||||
script_url,
|
||||
scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(),
|
||||
@@ -782,6 +850,7 @@ mod tests {
|
||||
|
||||
let result =
|
||||
load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: crate::worker::WorkerScriptKind::Module,
|
||||
main_script: ServiceWorkerScriptLoadParams {
|
||||
script_url,
|
||||
scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(),
|
||||
@@ -834,6 +903,7 @@ mod tests {
|
||||
|
||||
let result =
|
||||
load_service_worker_script_update_check(&ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: crate::worker::WorkerScriptKind::Classic,
|
||||
main_script: ServiceWorkerScriptLoadParams {
|
||||
script_url: Url::parse(&format!("{base_url}/app/sw.js")).unwrap(),
|
||||
scope_url: Url::parse(&format!("{base_url}/app/")).unwrap(),
|
||||
|
||||
@@ -770,7 +770,6 @@ mod tests {
|
||||
ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host: new_loading_test_host(version_id, &run),
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
}
|
||||
}
|
||||
@@ -1349,6 +1348,7 @@ mod tests {
|
||||
|
||||
fn test_loaded_script(script_url: &Url, source: &str) -> LoadedServiceWorkerScript {
|
||||
LoadedServiceWorkerScript {
|
||||
updated_imports: Default::default(),
|
||||
resource: test_script_resource(script_url),
|
||||
source: source.to_owned(),
|
||||
response_referrer_policy: None,
|
||||
@@ -1381,6 +1381,7 @@ mod tests {
|
||||
scope_url: Url,
|
||||
) -> ServiceWorkerQueuedRegisterJob {
|
||||
ServiceWorkerQueuedRegisterJob {
|
||||
update_registration_id: None,
|
||||
script_url,
|
||||
document_url: scope_url.join("page.html").expect("document url"),
|
||||
storage_key: ServiceWorkerRegistrationKey::storage_key_for_scope_url(&scope_url),
|
||||
@@ -1393,11 +1394,11 @@ mod tests {
|
||||
force_update_page_load_waiter_ids: Vec::new(),
|
||||
request_client: test_request_client(service),
|
||||
network_policy: WorkerNetworkPolicy::default(),
|
||||
browser_context_runtime: service.browser_context_runtime(),
|
||||
worker_context_runtime: service.browser_context_runtime().worker_context_runtime(),
|
||||
broadcast_channel_top_level_site: None,
|
||||
indexed_db_manager: None,
|
||||
storage_bucket_store: None,
|
||||
callbacks: vec![ServiceWorkerRegisterJob {
|
||||
callbacks: vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id: 1,
|
||||
document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1),
|
||||
completion_tx: test_completion_sender(),
|
||||
@@ -1417,6 +1418,7 @@ mod tests {
|
||||
let document_url = scope_url.join("page.html").expect("document url");
|
||||
let browser_context_runtime = service.browser_context_runtime();
|
||||
let queued_job = ServiceWorkerQueuedRegisterJob {
|
||||
update_registration_id: None,
|
||||
script_url: script_url.clone(),
|
||||
scope_url: scope_url.clone(),
|
||||
document_url,
|
||||
@@ -1429,11 +1431,11 @@ mod tests {
|
||||
force_update_page_load_waiter_ids: Vec::new(),
|
||||
request_client: test_request_client(service),
|
||||
network_policy: WorkerNetworkPolicy::default(),
|
||||
browser_context_runtime,
|
||||
worker_context_runtime: browser_context_runtime.worker_context_runtime(),
|
||||
broadcast_channel_top_level_site: None,
|
||||
indexed_db_manager: None,
|
||||
storage_bucket_store: None,
|
||||
callbacks: vec![ServiceWorkerRegisterJob {
|
||||
callbacks: vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id,
|
||||
document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1),
|
||||
completion_tx,
|
||||
@@ -1610,7 +1612,7 @@ mod tests {
|
||||
registration.scope_url = scope_url.clone();
|
||||
registration.installing_version_id = Some(version_id);
|
||||
let mut pending_register_job =
|
||||
ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob {
|
||||
ServiceWorkerPendingRegisterJob::new(vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id,
|
||||
document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1),
|
||||
completion_tx,
|
||||
@@ -1665,6 +1667,7 @@ mod tests {
|
||||
state.job_coordinator.enqueue_register(
|
||||
registration_key.clone(),
|
||||
ServiceWorkerQueuedRegisterJob {
|
||||
update_registration_id: None,
|
||||
script_url,
|
||||
scope_url: scope_url.clone(),
|
||||
document_url: scope_url.join("page.html").expect("document url"),
|
||||
@@ -1677,11 +1680,11 @@ mod tests {
|
||||
force_update_page_load_waiter_ids: Vec::new(),
|
||||
request_client: test_request_client(service),
|
||||
network_policy: WorkerNetworkPolicy::default(),
|
||||
browser_context_runtime: service.browser_context_runtime(),
|
||||
worker_context_runtime: service.browser_context_runtime().worker_context_runtime(),
|
||||
broadcast_channel_top_level_site: None,
|
||||
indexed_db_manager: None,
|
||||
storage_bucket_store: None,
|
||||
callbacks: vec![ServiceWorkerRegisterJob {
|
||||
callbacks: vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id,
|
||||
document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1),
|
||||
completion_tx,
|
||||
@@ -5297,6 +5300,191 @@ self.addEventListener("message", event => {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registration_update_rejects_wrong_identity_partition_and_installing_worker() {
|
||||
use crate::service_worker_runtime::{
|
||||
ServiceWorkerRegistrationErrorKind, ServiceWorkerRegistrationUpdate,
|
||||
};
|
||||
for invalid in ["identity", "partition", "installing"] {
|
||||
let service = new_service_worker_runtime_service();
|
||||
let (registration_id, version_id) = insert_running_installing_version(&service);
|
||||
let mut queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new();
|
||||
let scope_url = url("https://example.test/app/");
|
||||
let mut request = ServiceWorkerRegistrationUpdate {
|
||||
registration_id,
|
||||
caller_version_id: None,
|
||||
storage_key: ServiceWorkerRegistrationKey::storage_key_for_scope_url(&scope_url),
|
||||
document_url: scope_url.join("page.html").unwrap(),
|
||||
request_client: test_request_client(&service),
|
||||
network_policy: WorkerNetworkPolicy::default(),
|
||||
worker_context_runtime: service.browser_context_runtime().worker_context_runtime(),
|
||||
broadcast_channel_top_level_site: None,
|
||||
indexed_db_manager: None,
|
||||
storage_bucket_store: None,
|
||||
completion: ServiceWorkerRegisterJob::Page {
|
||||
request_id: 7,
|
||||
document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1),
|
||||
completion_tx: queue.sender(),
|
||||
},
|
||||
};
|
||||
match invalid {
|
||||
"identity" => request.registration_id = ServiceWorkerRegistrationId(999),
|
||||
"partition" => request.storage_key = "another-partition".into(),
|
||||
"installing" => request.caller_version_id = Some(version_id),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
service.start_registration_update(request);
|
||||
let completion = pop_register_completion(&mut queue);
|
||||
assert_eq!(completion.request_id, 7);
|
||||
assert_eq!(
|
||||
completion.result.err().unwrap().kind,
|
||||
ServiceWorkerRegistrationErrorKind::InvalidState,
|
||||
"{invalid}"
|
||||
);
|
||||
let diagnostics = service.diagnostics_snapshot();
|
||||
assert_eq!(diagnostics.registration_count, 1);
|
||||
assert_eq!(diagnostics.queued_register_job_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_registration_update_revalidates_after_installation() {
|
||||
for unregister in [false, true] {
|
||||
let service = new_service_worker_runtime_service();
|
||||
let (registration_id, version_id) = insert_running_installing_version(&service);
|
||||
let mut queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new();
|
||||
let mut job = test_queued_register_job(
|
||||
&service,
|
||||
url("https://example.test/app/sw.js"),
|
||||
url("https://example.test/app/"),
|
||||
);
|
||||
job.update_registration_id = Some(registration_id);
|
||||
job.callbacks = vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id: 3,
|
||||
document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1),
|
||||
completion_tx: queue.sender(),
|
||||
}];
|
||||
service.start_queued_register_job(job);
|
||||
assert!(!queue.has_ready_task());
|
||||
assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 1);
|
||||
let progress = {
|
||||
let mut state = service.inner.state.lock();
|
||||
let registration = state.registrations.get_mut(®istration_id).unwrap();
|
||||
registration.installing_version_id = None;
|
||||
registration.active_version_id = Some(version_id);
|
||||
registration.pending_unregistration = unregister;
|
||||
let version = state.versions.get_mut(&version_id).unwrap();
|
||||
version.lifecycle_state = ServiceWorkerVersionLifecycleState::Activated;
|
||||
if !unregister {
|
||||
version.script_url = url("https://example.test/app/replaced.js");
|
||||
}
|
||||
service.advance_registration_job_queue_locked(&mut state, registration_id)
|
||||
};
|
||||
for action in progress {
|
||||
service.run_lifecycle_progress(action);
|
||||
}
|
||||
let error = pop_register_completion(&mut queue).result.err().unwrap();
|
||||
assert_eq!(
|
||||
error.kind,
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationErrorKind::Type
|
||||
);
|
||||
assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 0);
|
||||
assert_eq!(service.diagnostics_snapshot().version_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_queues_behind_an_installing_update_job() {
|
||||
let service = new_service_worker_runtime_service();
|
||||
let script_url = url("https://example.test/app/sw.js");
|
||||
let scope_url = url("https://example.test/app/");
|
||||
let registration_id = ServiceWorkerRegistrationId(1);
|
||||
let version_id = ServiceWorkerVersionId(1);
|
||||
insert_starting_version_with_register_job(
|
||||
&service,
|
||||
registration_id,
|
||||
version_id,
|
||||
script_url.clone(),
|
||||
scope_url.clone(),
|
||||
1,
|
||||
test_completion_sender(),
|
||||
);
|
||||
service
|
||||
.inner
|
||||
.state
|
||||
.lock()
|
||||
.registrations
|
||||
.get_mut(®istration_id)
|
||||
.unwrap()
|
||||
.pending_register_jobs
|
||||
.get_mut(&version_id)
|
||||
.unwrap()
|
||||
.is_update = true;
|
||||
let mut queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new();
|
||||
let mut job = test_queued_register_job(&service, script_url, scope_url);
|
||||
job.callbacks = vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id: 2,
|
||||
document_owner: crate::native_bridge::WindowDocumentOwner::for_test(1),
|
||||
completion_tx: queue.sender(),
|
||||
}];
|
||||
service.start_queued_register_job(job);
|
||||
assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 1);
|
||||
assert!(!queue.has_ready_task());
|
||||
let progress = {
|
||||
let mut state = service.inner.state.lock();
|
||||
let registration = state.registrations.get_mut(®istration_id).unwrap();
|
||||
registration.installing_version_id = None;
|
||||
registration.active_version_id = Some(version_id);
|
||||
service.advance_registration_job_queue_locked(&mut state, registration_id)
|
||||
};
|
||||
for action in progress {
|
||||
service.run_lifecycle_progress(action);
|
||||
}
|
||||
assert_eq!(pop_register_completion(&mut queue).request_id, 2);
|
||||
assert_eq!(service.diagnostics_snapshot().queued_register_job_count, 0);
|
||||
assert_eq!(
|
||||
service
|
||||
.diagnostics_snapshot()
|
||||
.pending_main_script_update_check_count,
|
||||
0
|
||||
);
|
||||
service.terminate_all_for_context_shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_forced_update_keeps_the_script_check() {
|
||||
let service = new_service_worker_runtime_service();
|
||||
let registration_id = ServiceWorkerRegistrationId(42);
|
||||
let version_id = ServiceWorkerVersionId(17);
|
||||
let script_url = url("https://example.test/app/sw.js");
|
||||
let scope_url = url("https://example.test/app/");
|
||||
insert_registered_version(
|
||||
&service,
|
||||
registration_id,
|
||||
version_id,
|
||||
script_url.clone(),
|
||||
scope_url.clone(),
|
||||
[],
|
||||
);
|
||||
make_version_persistable(&service, version_id);
|
||||
let mut job = test_queued_register_job(&service, script_url, scope_url);
|
||||
job.force_bypass_cache = true;
|
||||
job.skip_script_comparison = true;
|
||||
let mut state = service.inner.state.lock();
|
||||
state
|
||||
.job_coordinator
|
||||
.enqueue_register(job.registration_key(), job);
|
||||
let progress = service.advance_registration_job_queue_locked(&mut state, registration_id);
|
||||
let [LifecycleProgress::StartMainScriptUpdateCheck(check)] = progress.as_slice() else {
|
||||
panic!("queued forced update must not take the register shortcut");
|
||||
};
|
||||
assert!(check.1.skip_script_comparison);
|
||||
assert_eq!(
|
||||
check.1.main_script.cache_mode,
|
||||
moli_fetch::RequestCacheMode::Validate
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_registration_queues_same_scope_job_while_installing() {
|
||||
let service = new_service_worker_runtime_service();
|
||||
|
||||
@@ -334,7 +334,6 @@ impl ServiceWorkerRuntimeService {
|
||||
Some(ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
})
|
||||
}
|
||||
@@ -481,6 +480,7 @@ impl ServiceWorkerRuntimeService {
|
||||
};
|
||||
let request_client = newest_version.launch_config.request_client();
|
||||
ServiceWorkerQueuedRegisterJob {
|
||||
update_registration_id: None,
|
||||
script_url: newest_version.script_url.clone(),
|
||||
scope_url: registration.scope_url.clone(),
|
||||
document_url: newest_version.launch_config.document_url.clone(),
|
||||
@@ -493,7 +493,7 @@ impl ServiceWorkerRuntimeService {
|
||||
force_update_page_load_waiter_ids,
|
||||
request_client,
|
||||
network_policy: newest_version.launch_config.network_policy.clone(),
|
||||
browser_context_runtime,
|
||||
worker_context_runtime: browser_context_runtime.worker_context_runtime(),
|
||||
broadcast_channel_top_level_site: newest_version
|
||||
.launch_config
|
||||
.broadcast_channel_top_level_site
|
||||
|
||||
@@ -181,9 +181,6 @@ impl ServiceWorkerRuntimeService {
|
||||
&mut launch,
|
||||
debugger_release_consumed,
|
||||
);
|
||||
for notification in launch.lifecycle_notifications {
|
||||
notification.send();
|
||||
}
|
||||
launch
|
||||
.host
|
||||
.start_loading(self.clone(), launch.params, launch.preloaded_script);
|
||||
@@ -420,7 +417,6 @@ impl ServiceWorkerRuntimeService {
|
||||
Some(ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
}),
|
||||
None,
|
||||
|
||||
@@ -363,7 +363,6 @@ impl ServiceWorkerRuntimeService {
|
||||
ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
},
|
||||
)))
|
||||
@@ -430,7 +429,6 @@ impl ServiceWorkerRuntimeService {
|
||||
ServiceWorkerMessageStart::Start(Box::new(ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
}))
|
||||
}
|
||||
@@ -492,7 +490,6 @@ impl ServiceWorkerRuntimeService {
|
||||
ServiceWorkerNotificationStart::Start(Box::new(ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
}))
|
||||
}
|
||||
@@ -556,7 +553,6 @@ impl ServiceWorkerRuntimeService {
|
||||
ServiceWorkerPushStart::Start(Box::new(ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
}))
|
||||
}
|
||||
@@ -618,7 +614,6 @@ impl ServiceWorkerRuntimeService {
|
||||
ServiceWorkerSyncStart::Start(Box::new(ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
}))
|
||||
}
|
||||
@@ -680,7 +675,6 @@ impl ServiceWorkerRuntimeService {
|
||||
ServiceWorkerPeriodicSyncStart::Start(Box::new(ServiceWorkerQueuedLaunch {
|
||||
params,
|
||||
host,
|
||||
lifecycle_notifications: Vec::new(),
|
||||
preloaded_script: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
use super::*;
|
||||
use crate::service_worker_runtime::{
|
||||
ServiceWorkerRegistrationErrorKind, ServiceWorkerRegistrationUpdate,
|
||||
};
|
||||
use moli_fetch::RequestCacheMode;
|
||||
|
||||
fn newest_exposed_version_id(
|
||||
state: &ServiceWorkerRuntimeState,
|
||||
registration: &ServiceWorkerRegistration,
|
||||
) -> Option<ServiceWorkerVersionId> {
|
||||
let checking = state
|
||||
.pending_main_script_update_checks
|
||||
.get(®istration.id)
|
||||
.map(|check| check.new_version_id);
|
||||
registration
|
||||
.installing_version_id
|
||||
.filter(|version_id| Some(*version_id) != checking)
|
||||
.or(registration.waiting_version_id)
|
||||
.or(registration.active_version_id)
|
||||
}
|
||||
|
||||
fn validate_queued_update(
|
||||
state: &ServiceWorkerRuntimeState,
|
||||
job: &ServiceWorkerQueuedRegisterJob,
|
||||
) -> Result<(), ServiceWorkerRegistrationError> {
|
||||
let Some(registration_id) = job.update_registration_id else {
|
||||
return Ok(());
|
||||
};
|
||||
let registration = state
|
||||
.registrations
|
||||
.get(®istration_id)
|
||||
.filter(|registration| {
|
||||
!registration.pending_unregistration && registration.key() == job.registration_key()
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
ServiceWorkerRegistrationError::type_error(
|
||||
"The ServiceWorker registration was unregistered.",
|
||||
)
|
||||
})?;
|
||||
let newest = newest_exposed_version_id(state, registration)
|
||||
.and_then(|id| state.versions.get(&id))
|
||||
.ok_or_else(|| {
|
||||
ServiceWorkerRegistrationError::type_error(
|
||||
"The ServiceWorker registration has no worker.",
|
||||
)
|
||||
})?;
|
||||
if newest.script_url != job.script_url {
|
||||
return Err(ServiceWorkerRegistrationError::type_error(
|
||||
"The newest ServiceWorker script URL changed before update().",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn same_registration_options_fast_path_snapshot(
|
||||
state: &ServiceWorkerRuntimeState,
|
||||
registration: &ServiceWorkerRegistration,
|
||||
@@ -28,6 +79,77 @@ fn same_registration_options_fast_path_snapshot(
|
||||
}
|
||||
|
||||
impl ServiceWorkerRuntimeService {
|
||||
pub(crate) fn start_registration_update(&self, request: ServiceWorkerRegistrationUpdate) {
|
||||
let newest = {
|
||||
let state = self.inner.state.lock();
|
||||
(|| {
|
||||
let registration = state
|
||||
.registrations
|
||||
.get(&request.registration_id)
|
||||
.filter(|registration| registration.storage_key == request.storage_key)
|
||||
.ok_or_else(|| {
|
||||
ServiceWorkerRegistrationError::new(
|
||||
ServiceWorkerRegistrationErrorKind::InvalidState,
|
||||
"The ServiceWorker registration has no worker.",
|
||||
)
|
||||
})?;
|
||||
let newest = newest_exposed_version_id(&state, registration)
|
||||
.and_then(|id| state.versions.get(&id))
|
||||
.ok_or_else(|| {
|
||||
ServiceWorkerRegistrationError::new(
|
||||
ServiceWorkerRegistrationErrorKind::InvalidState,
|
||||
"The ServiceWorker registration has no worker.",
|
||||
)
|
||||
})?;
|
||||
if request
|
||||
.caller_version_id
|
||||
.and_then(|id| state.versions.get(&id))
|
||||
.is_some_and(|caller| {
|
||||
caller.lifecycle_state == ServiceWorkerVersionLifecycleState::Installing
|
||||
})
|
||||
{
|
||||
return Err(ServiceWorkerRegistrationError::new(
|
||||
ServiceWorkerRegistrationErrorKind::InvalidState,
|
||||
"An installing ServiceWorker cannot call update().",
|
||||
));
|
||||
}
|
||||
Ok((
|
||||
registration.scope_url.clone(),
|
||||
newest.script_url.clone(),
|
||||
newest.script_kind,
|
||||
registration.update_via_cache,
|
||||
))
|
||||
})()
|
||||
};
|
||||
let (scope_url, script_url, script_kind, update_via_cache) = match newest {
|
||||
Ok(newest) => newest,
|
||||
Err(error) => {
|
||||
request.completion.send(Err(error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.start_queued_register_job(ServiceWorkerQueuedRegisterJob {
|
||||
update_registration_id: Some(request.registration_id),
|
||||
script_url,
|
||||
scope_url,
|
||||
document_url: request.document_url,
|
||||
storage_key: request.storage_key,
|
||||
script_kind,
|
||||
update_via_cache,
|
||||
force_bypass_cache: false,
|
||||
skip_script_comparison: false,
|
||||
skip_waiting_after_install: false,
|
||||
force_update_page_load_waiter_ids: Vec::new(),
|
||||
request_client: request.request_client,
|
||||
network_policy: request.network_policy,
|
||||
worker_context_runtime: request.worker_context_runtime,
|
||||
broadcast_channel_top_level_site: request.broadcast_channel_top_level_site,
|
||||
indexed_db_manager: request.indexed_db_manager,
|
||||
storage_bucket_store: request.storage_bucket_store,
|
||||
callbacks: vec![request.completion],
|
||||
});
|
||||
}
|
||||
|
||||
fn record_force_update_page_load_devtools_message_locked(
|
||||
&self,
|
||||
state: &mut ServiceWorkerRuntimeState,
|
||||
@@ -109,6 +231,7 @@ impl ServiceWorkerRuntimeService {
|
||||
register_completion_tx: RendererPageServiceWorkerTaskSender,
|
||||
) {
|
||||
let queued_job = ServiceWorkerQueuedRegisterJob {
|
||||
update_registration_id: None,
|
||||
script_url,
|
||||
scope_url,
|
||||
document_url,
|
||||
@@ -121,11 +244,11 @@ impl ServiceWorkerRuntimeService {
|
||||
force_update_page_load_waiter_ids: Vec::new(),
|
||||
request_client,
|
||||
network_policy,
|
||||
browser_context_runtime,
|
||||
worker_context_runtime: browser_context_runtime.worker_context_runtime(),
|
||||
broadcast_channel_top_level_site,
|
||||
indexed_db_manager,
|
||||
storage_bucket_store,
|
||||
callbacks: vec![ServiceWorkerRegisterJob {
|
||||
callbacks: vec![ServiceWorkerRegisterJob::Page {
|
||||
request_id: register_request_id,
|
||||
document_owner: register_document_owner,
|
||||
completion_tx: register_completion_tx,
|
||||
@@ -138,6 +261,11 @@ impl ServiceWorkerRuntimeService {
|
||||
let (launch, update_check, completed_register_callbacks) = {
|
||||
let mut state = self.inner.state.lock();
|
||||
let registration_key = queued_job.registration_key();
|
||||
if let Err(error) = validate_queued_update(&state, &queued_job) {
|
||||
drop(state);
|
||||
ServiceWorkerRegisterJob::send_all(queued_job.callbacks, Err(error));
|
||||
return;
|
||||
}
|
||||
self.restore_stored_registration_for_queued_job_locked(&mut state, &queued_job);
|
||||
let registration_id = state
|
||||
.registrations
|
||||
@@ -190,14 +318,19 @@ impl ServiceWorkerRuntimeService {
|
||||
{
|
||||
registration.pending_unregistration = false;
|
||||
}
|
||||
let can_coalesce_with_installing = installing_version_id
|
||||
.and_then(|version_id| state.versions.get(&version_id))
|
||||
.is_some_and(|version| {
|
||||
version.script_url == queued_job.script_url
|
||||
&& version.script_kind == queued_job.script_kind
|
||||
&& registration_scope_url == queued_job.scope_url
|
||||
&& registration_update_via_cache == queued_job.update_via_cache
|
||||
});
|
||||
let can_coalesce_with_installing = queued_job.update_registration_id.is_none()
|
||||
&& installing_version_id
|
||||
.and_then(|version_id| state.versions.get(&version_id))
|
||||
.is_some_and(|version| {
|
||||
version.script_url == queued_job.script_url
|
||||
&& version.script_kind == queued_job.script_kind
|
||||
&& registration_scope_url == queued_job.scope_url
|
||||
&& registration_update_via_cache == queued_job.update_via_cache
|
||||
&& !state.registrations[®istration_id]
|
||||
.pending_register_jobs
|
||||
.get(&version.id)
|
||||
.is_some_and(|job| job.is_update)
|
||||
});
|
||||
let pending_update_check_matches = state
|
||||
.pending_main_script_update_checks
|
||||
.get(®istration_id)
|
||||
@@ -514,6 +647,7 @@ impl ServiceWorkerRuntimeService {
|
||||
),
|
||||
};
|
||||
let update_check_params = ServiceWorkerScriptUpdateCheckParams {
|
||||
script_kind: queued_job.script_kind,
|
||||
main_script: load_params,
|
||||
newest_main_body_sha256: newest_body_sha256.clone(),
|
||||
imported_scripts,
|
||||
@@ -616,16 +750,12 @@ impl ServiceWorkerRuntimeService {
|
||||
register_callbacks,
|
||||
queued_job.skip_waiting_after_install,
|
||||
);
|
||||
pending_register_job.is_update = queued_job.update_registration_id.is_some();
|
||||
pending_register_job.start_current_moli_job();
|
||||
let registration = state.registrations.get_mut(®istration_id)?;
|
||||
registration
|
||||
.pending_register_jobs
|
||||
.insert(version_id, pending_register_job);
|
||||
let lifecycle_notifications = lifecycle_notifications_for_registration_locked(
|
||||
state,
|
||||
registration_id,
|
||||
vec![ServiceWorkerLifecycleClientEvent::UpdateFound],
|
||||
);
|
||||
let request_client = launch_config.request_client();
|
||||
Some(ServiceWorkerQueuedLaunch {
|
||||
params: ServiceWorkerLaunchParams {
|
||||
@@ -645,7 +775,6 @@ impl ServiceWorkerRuntimeService {
|
||||
pause_evaluation_until_debugger: false,
|
||||
},
|
||||
host,
|
||||
lifecycle_notifications,
|
||||
preloaded_script,
|
||||
})
|
||||
}
|
||||
@@ -681,6 +810,7 @@ impl ServiceWorkerRuntimeService {
|
||||
register_callbacks,
|
||||
queued_job.skip_waiting_after_install,
|
||||
);
|
||||
pending_register_job.is_update = queued_job.update_registration_id.is_some();
|
||||
pending_register_job.start_current_moli_job();
|
||||
registration
|
||||
.pending_register_jobs
|
||||
@@ -691,11 +821,6 @@ impl ServiceWorkerRuntimeService {
|
||||
host.run_identity(),
|
||||
&queued_job,
|
||||
);
|
||||
let lifecycle_notifications = lifecycle_notifications_for_registration_locked(
|
||||
state,
|
||||
registration_id,
|
||||
vec![ServiceWorkerLifecycleClientEvent::UpdateFound],
|
||||
);
|
||||
let request_client = launch_config.request_client();
|
||||
Some(ServiceWorkerQueuedLaunch {
|
||||
params: ServiceWorkerLaunchParams {
|
||||
@@ -715,7 +840,6 @@ impl ServiceWorkerRuntimeService {
|
||||
pause_evaluation_until_debugger: false,
|
||||
},
|
||||
host,
|
||||
lifecycle_notifications,
|
||||
preloaded_script: Some(preloaded_script),
|
||||
})
|
||||
}
|
||||
@@ -837,6 +961,39 @@ impl ServiceWorkerRuntimeService {
|
||||
match queued_job {
|
||||
ServiceWorkerQueuedJob::Register(queued_job) => {
|
||||
let queued_job = *queued_job;
|
||||
if let Err(error) = validate_queued_update(state, &queued_job) {
|
||||
progress.push(LifecycleProgress::RegisterFailed((
|
||||
queued_job.callbacks,
|
||||
error,
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
// A register queued behind update must wait for that job,
|
||||
// then reuse the matching registration without another fetch.
|
||||
if queued_job.update_registration_id.is_none()
|
||||
&& !queued_job.force_bypass_cache
|
||||
&& !queued_job.skip_script_comparison
|
||||
&& let Some(snapshot) =
|
||||
state
|
||||
.registrations
|
||||
.get(®istration_id)
|
||||
.and_then(|registration| {
|
||||
same_registration_options_fast_path_snapshot(
|
||||
state,
|
||||
registration,
|
||||
&queued_job.script_url,
|
||||
queued_job.script_kind,
|
||||
queued_job.update_via_cache,
|
||||
false,
|
||||
)
|
||||
})
|
||||
{
|
||||
progress.push(LifecycleProgress::RegisterCompleted(Box::new((
|
||||
queued_job.callbacks,
|
||||
snapshot,
|
||||
))));
|
||||
continue;
|
||||
}
|
||||
if let Some(update_check) = self.start_main_script_update_check_locked(
|
||||
state,
|
||||
registration_id,
|
||||
|
||||
@@ -66,7 +66,7 @@ impl ServiceWorkerRuntimeService {
|
||||
return;
|
||||
};
|
||||
let loaded_main_script_resource = script_resource.is_some();
|
||||
let (lifecycle_start, register_completion, pending_events) = {
|
||||
let (lifecycle_start, register_completion, notifications, worker_hosts, pending_events) = {
|
||||
let mut state = self.inner.state.lock();
|
||||
let (registration_id, lifecycle_state, pending_events, run) = {
|
||||
let Some(version) = state.versions.get_mut(&version_id) else {
|
||||
@@ -149,19 +149,90 @@ impl ServiceWorkerRuntimeService {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (mut notifications, worker_hosts) = if lifecycle_start.is_some() {
|
||||
let notifications = lifecycle_notifications_for_registration_locked(
|
||||
&state,
|
||||
registration_id,
|
||||
vec![ServiceWorkerLifecycleClientEvent::UpdateFound],
|
||||
);
|
||||
let hosts = state
|
||||
.versions
|
||||
.values()
|
||||
.filter_map(|version| {
|
||||
if version.registration_id != registration_id {
|
||||
return None;
|
||||
}
|
||||
match &version.running_state {
|
||||
ServiceWorkerVersionRunningState::Running { host } => {
|
||||
Some(host.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(notifications, hosts)
|
||||
} else {
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
// A first register() creates its JS registration when the completion is
|
||||
// delivered. Queue updatefound for that document after the completion,
|
||||
// even though it cannot have a registration watcher yet.
|
||||
if let Some((callbacks, snapshot)) = ®ister_completion {
|
||||
for callback in callbacks {
|
||||
let ServiceWorkerRegisterJob::Page {
|
||||
document_owner,
|
||||
completion_tx,
|
||||
..
|
||||
} = callback
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if notifications
|
||||
.iter()
|
||||
.any(|notification| notification.watcher.document_owner == *document_owner)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
notifications.push(ServiceWorkerLifecycleNotificationDelivery {
|
||||
watcher: ServiceWorkerLifecycleWatcher {
|
||||
scope_url: snapshot.scope_url().clone(),
|
||||
storage_key: state.registrations[®istration_id].storage_key.clone(),
|
||||
document_owner: *document_owner,
|
||||
completion_tx: completion_tx.clone(),
|
||||
},
|
||||
registration: snapshot.clone(),
|
||||
events: vec![ServiceWorkerLifecycleClientEvent::UpdateFound],
|
||||
});
|
||||
}
|
||||
}
|
||||
state.record_target_started(version_id, run);
|
||||
(lifecycle_start, register_completion, pending_events)
|
||||
(
|
||||
lifecycle_start,
|
||||
register_completion,
|
||||
notifications,
|
||||
worker_hosts,
|
||||
pending_events,
|
||||
)
|
||||
};
|
||||
if let Some(start) = lifecycle_start
|
||||
&& let Some(progress) = Self::lifecycle_start_to_progress(start)
|
||||
{
|
||||
self.run_lifecycle_progress(progress);
|
||||
}
|
||||
if let Some((callbacks, snapshot)) = register_completion
|
||||
&& !callbacks.is_empty()
|
||||
{
|
||||
ServiceWorkerRegisterJob::send_all(callbacks, Ok(snapshot));
|
||||
}
|
||||
// Script evaluation must succeed before updatefound. Deliver it on each
|
||||
// existing worker loop before install (and before page listeners can send
|
||||
// another event to the active worker).
|
||||
for host in worker_hosts {
|
||||
host.dispatch_registration_update_found();
|
||||
}
|
||||
for notification in notifications {
|
||||
notification.send();
|
||||
}
|
||||
if let Some(start) = lifecycle_start
|
||||
&& let Some(progress) = Self::lifecycle_start_to_progress(start)
|
||||
{
|
||||
self.run_lifecycle_progress(progress);
|
||||
}
|
||||
for event in pending_events {
|
||||
match event {
|
||||
ServiceWorkerPendingStartEvent::Fetch(event) => {
|
||||
@@ -981,6 +1052,12 @@ fn registration_error_for_update_check_failure(
|
||||
failure: ServiceWorkerScriptUpdateCheckFailure,
|
||||
) -> ServiceWorkerRegistrationError {
|
||||
match failure.status {
|
||||
ServiceWorkerScriptUpdateCheckFailureStatus::Security => {
|
||||
ServiceWorkerRegistrationError::new(
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationErrorKind::Security,
|
||||
failure.message,
|
||||
)
|
||||
}
|
||||
ServiceWorkerScriptUpdateCheckFailureStatus::ScriptLoadFailed => {
|
||||
registration_error_for_script_load_failure(failure.message)
|
||||
}
|
||||
@@ -1002,6 +1079,8 @@ fn registration_error_for_script_load_failure(message: String) -> ServiceWorkerR
|
||||
);
|
||||
}
|
||||
if normalized.contains("cross-origin")
|
||||
|| normalized.contains("script mime type")
|
||||
|| normalized.contains("blocked by x-content-type-options nosniff")
|
||||
|| normalized.contains("service-worker-allowed")
|
||||
|| normalized.contains("not under the max scope allowed")
|
||||
|| normalized.contains("disallowed escape")
|
||||
|
||||
@@ -132,7 +132,6 @@ impl ServiceWorkerRegistrationSnapshot {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn registration_id(&self) -> ServiceWorkerRegistrationId {
|
||||
self.registration_id
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ enum ServiceWorkerRuntimeCompletionKind {
|
||||
},
|
||||
MainScriptUpdateCheckCompleted {
|
||||
registration_id: ServiceWorkerRegistrationId,
|
||||
result: ServiceWorkerScriptUpdateCheckCompletion,
|
||||
result: Box<ServiceWorkerScriptUpdateCheckCompletion>,
|
||||
},
|
||||
LifecycleEventCompleted {
|
||||
completion: ServiceWorkerLifecycleCompletion,
|
||||
@@ -219,7 +219,7 @@ impl ServiceWorkerRuntimeCompletion {
|
||||
runtime_service,
|
||||
kind: ServiceWorkerRuntimeCompletionKind::MainScriptUpdateCheckCompleted {
|
||||
registration_id,
|
||||
result,
|
||||
result: Box::new(result),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -637,7 +637,7 @@ impl ServiceWorkerRuntimeCompletion {
|
||||
result,
|
||||
} => self
|
||||
.runtime_service
|
||||
.finish_main_script_update_check_completed(registration_id, result),
|
||||
.finish_main_script_update_check_completed(registration_id, *result),
|
||||
ServiceWorkerRuntimeCompletionKind::LifecycleEventCompleted { completion } => {
|
||||
self.runtime_service
|
||||
.finish_lifecycle_event_completed(completion);
|
||||
|
||||
@@ -713,7 +713,6 @@ pub(super) enum LifecycleProgress {
|
||||
pub(super) struct ServiceWorkerQueuedLaunch {
|
||||
pub(super) params: ServiceWorkerLaunchParams,
|
||||
pub(super) host: SharedRendererServiceWorkerHost,
|
||||
pub(super) lifecycle_notifications: Vec<ServiceWorkerLifecycleNotificationDelivery>,
|
||||
pub(super) preloaded_script: Option<LoadedServiceWorkerScript>,
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ pub(super) fn materialize_worker_import_source(
|
||||
csp.check_url(script_url, ContentSecurityPolicyRedirectStatus::NoRedirect)
|
||||
.map_err(WorkerImportScriptError::network)?;
|
||||
}
|
||||
let (service_worker, cached, can_import_new) = {
|
||||
let (service_worker, cached, updated, can_import_new) = {
|
||||
let state = state.borrow();
|
||||
(
|
||||
matches!(
|
||||
@@ -76,10 +76,21 @@ pub(super) fn materialize_worker_import_source(
|
||||
.service_worker_script_resources
|
||||
.get(script_url)
|
||||
.cloned(),
|
||||
state
|
||||
.service_worker_updated_script_resources
|
||||
.get(script_url)
|
||||
.cloned(),
|
||||
state.service_worker_can_import_new_scripts,
|
||||
)
|
||||
};
|
||||
if service_worker {
|
||||
let from_update_check = cached.is_none() && updated.is_some();
|
||||
let cached = match cached.clone() {
|
||||
Some(resource) => Some(resource),
|
||||
None => updated
|
||||
.transpose()
|
||||
.map_err(WorkerImportScriptError::network)?,
|
||||
};
|
||||
if let Some(resource) = &cached
|
||||
&& let Some(script) = &resource.classic_script
|
||||
{
|
||||
@@ -89,6 +100,13 @@ pub(super) fn materialize_worker_import_source(
|
||||
.map_err(WorkerImportScriptError::network)?;
|
||||
}
|
||||
}
|
||||
if from_update_check {
|
||||
state
|
||||
.borrow_mut()
|
||||
.service_worker_script_resources
|
||||
.insert(script_url.clone(), resource.clone());
|
||||
report_service_worker_imported_script_loaded(state, resource.clone());
|
||||
}
|
||||
return Ok(WorkerImportScriptSource {
|
||||
final_url: resource.final_url.clone(),
|
||||
source: script.source.clone(),
|
||||
|
||||
@@ -178,6 +178,10 @@ pub(super) const WORKER_EXCEPTION_LINE_SLOT: &str = "__moliWorkerExceptionLine";
|
||||
pub(super) const WORKER_EXCEPTION_COLUMN_SLOT: &str = "__moliWorkerExceptionColumn";
|
||||
const SERVICE_WORKER_REGISTRATION_SCOPE_SLOT: &str = "__moliServiceWorkerRegistrationScope";
|
||||
const SERVICE_WORKER_REGISTRATION_ID_SLOT: &str = "__moliServiceWorkerRegistrationId";
|
||||
const SERVICE_WORKER_GLOBAL_REGISTRATION_SLOT: &str = "__moliServiceWorkerGlobalRegistration";
|
||||
const SERVICE_WORKER_REGISTRATION_EVENTS_SLOT: &str = "__moliServiceWorkerRegistrationEvents";
|
||||
const SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT: &str =
|
||||
"__moliServiceWorkerRegistrationOnUpdateFound";
|
||||
const SERVICE_WORKER_VERSION_ID_SLOT: &str = "__moliServiceWorkerVersionId";
|
||||
const SERVICE_WORKER_WORKER_EVENTS_SLOT: &str = "__moliServiceWorkerWorkerEvents";
|
||||
const SERVICE_WORKER_NAVIGATION_PRELOAD_MANAGER_SCOPE_SLOT: &str =
|
||||
@@ -655,6 +659,13 @@ struct ServiceWorkerGlobalRegistrationDeclaration<'scope> {
|
||||
#[webapi(data_property, readonly)]
|
||||
scope: String,
|
||||
|
||||
#[webapi(
|
||||
accessor_property = "onupdatefound",
|
||||
getter = service_worker_registration_onupdatefound_getter,
|
||||
setter = service_worker_registration_onupdatefound_setter
|
||||
)]
|
||||
onupdatefound: (),
|
||||
|
||||
#[webapi(
|
||||
accessor_property = "installing",
|
||||
getter = service_worker_registration_installing_getter
|
||||
@@ -676,6 +687,9 @@ struct ServiceWorkerGlobalRegistrationDeclaration<'scope> {
|
||||
#[webapi(method, callback = service_worker_registration_unregister_callback, length = 0)]
|
||||
unregister: (),
|
||||
|
||||
#[webapi(method, callback = service_worker_registration_update_callback, length = 0)]
|
||||
update: (),
|
||||
|
||||
#[webapi(
|
||||
method = "showNotification",
|
||||
callback = service_worker_registration_show_notification_callback,
|
||||
@@ -986,6 +1000,11 @@ pub(super) struct PendingServiceWorkerShowNotification {
|
||||
pub(super) resolver: v8::Global<v8::PromiseResolver>,
|
||||
}
|
||||
|
||||
pub(super) struct PendingServiceWorkerUpdate {
|
||||
pub(super) resolver: v8::Global<v8::PromiseResolver>,
|
||||
pub(super) registration: v8::Global<v8::Object>,
|
||||
}
|
||||
|
||||
pub(super) struct PendingServiceWorkerGetNotifications {
|
||||
pub(super) resolver: v8::Global<v8::PromiseResolver>,
|
||||
}
|
||||
@@ -1549,6 +1568,7 @@ pub(crate) struct WorkerGlobalState {
|
||||
/// Responses in this ServiceWorker version's script resource map, keyed by
|
||||
/// requested URL. Dedicated and Shared Workers leave this map empty.
|
||||
pub(super) service_worker_script_resources: HashMap<Url, crate::worker::WorkerScriptResource>,
|
||||
pub(super) service_worker_updated_script_resources: crate::worker::WorkerScriptUpdateResources,
|
||||
pub(super) service_worker_can_import_new_scripts: bool,
|
||||
/// Referrer policy parsed from the top-level worker script response.
|
||||
pub(super) referrer_policy: Option<String>,
|
||||
@@ -1659,6 +1679,8 @@ pub(crate) struct WorkerGlobalState {
|
||||
/// In-flight Service Worker `registration.showNotification()` requests keyed by request id.
|
||||
pub(super) pending_service_worker_show_notifications:
|
||||
HashMap<u64, PendingServiceWorkerShowNotification>,
|
||||
pub(super) pending_service_worker_updates: HashMap<u64, PendingServiceWorkerUpdate>,
|
||||
pub(super) service_worker_update_request_ids: WorkerServiceWorkerRequestIdAllocator,
|
||||
/// In-flight Service Worker `registration.getNotifications()` requests keyed by request id.
|
||||
pub(super) pending_service_worker_get_notifications:
|
||||
HashMap<u64, PendingServiceWorkerGetNotifications>,
|
||||
@@ -2523,6 +2545,45 @@ pub(super) fn drain_service_worker_clients_open_window_result(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn drain_service_worker_update_result(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
state: &Rc<RefCell<WorkerGlobalState>>,
|
||||
request_id: u64,
|
||||
result: Result<
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationSnapshot,
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationError,
|
||||
>,
|
||||
) {
|
||||
let Some(pending) = state
|
||||
.borrow_mut()
|
||||
.pending_service_worker_updates
|
||||
.remove(&request_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let resolver = v8::Local::new(scope, &pending.resolver);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let registration = v8::Local::new(scope, &pending.registration);
|
||||
let _ = resolver.resolve(scope, registration.into());
|
||||
}
|
||||
Err(error) => {
|
||||
let exception = if error.kind.rejects_as_type_error_for_update() {
|
||||
let message = v8_string(scope, &error.message)
|
||||
.unwrap_or_else(|| v8str(scope, "ServiceWorker update failed"));
|
||||
v8::Exception::type_error(scope, message)
|
||||
} else {
|
||||
crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
&error.message,
|
||||
error.kind.dom_exception_name(),
|
||||
)
|
||||
};
|
||||
let _ = resolver.reject(scope, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn drain_service_worker_show_notification_result(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
state: &Rc<RefCell<WorkerGlobalState>>,
|
||||
@@ -3265,6 +3326,12 @@ fn install_service_worker_global_runtime<'s>(
|
||||
) -> Result<()> {
|
||||
let registration =
|
||||
build_service_worker_global_registration(scope, registration_id, version_id, scope_url)?;
|
||||
set_private_value(
|
||||
scope,
|
||||
global,
|
||||
SERVICE_WORKER_GLOBAL_REGISTRATION_SLOT,
|
||||
registration.into(),
|
||||
);
|
||||
let clients = ServiceWorkerClientsDeclaration::default()
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to build service worker clients: {error}"))?;
|
||||
@@ -3303,10 +3370,12 @@ fn build_service_worker_global_registration<'s>(
|
||||
build_service_worker_global_navigation_preload_manager(scope, scope_url)?;
|
||||
let registration = ServiceWorkerGlobalRegistrationDeclaration {
|
||||
scope: scope_url.as_str().to_owned(),
|
||||
onupdatefound: (),
|
||||
installing: (),
|
||||
waiting: (),
|
||||
active: (),
|
||||
unregister: (),
|
||||
update: (),
|
||||
show_notification: (),
|
||||
get_notifications: (),
|
||||
sync: sync_manager,
|
||||
@@ -3316,6 +3385,13 @@ fn build_service_worker_global_registration<'s>(
|
||||
}
|
||||
.bind(scope)
|
||||
.map_err(|error| anyhow!("failed to build service worker registration: {error:?}"))?;
|
||||
install_simple_event_target_methods(
|
||||
scope,
|
||||
registration,
|
||||
SERVICE_WORKER_REGISTRATION_EVENTS_SLOT,
|
||||
false,
|
||||
);
|
||||
install_simple_event_target_ordered_handlers(scope, registration);
|
||||
let scope_value = v8_string(scope, scope_url.as_str())
|
||||
.ok_or_else(|| anyhow!("failed to allocate service worker registration scope"))?;
|
||||
set_private_value(
|
||||
@@ -3341,6 +3417,77 @@ fn build_service_worker_global_registration<'s>(
|
||||
Ok(registration)
|
||||
}
|
||||
|
||||
fn service_worker_registration_onupdatefound_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
rv.set(
|
||||
get_private_value(
|
||||
scope,
|
||||
args.this(),
|
||||
SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT,
|
||||
)
|
||||
.unwrap_or_else(|| v8::null(scope).into()),
|
||||
);
|
||||
}
|
||||
|
||||
fn service_worker_registration_onupdatefound_setter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let value = args.get(0);
|
||||
let active = value.is_function();
|
||||
let value = if active {
|
||||
value
|
||||
} else {
|
||||
v8::null(scope).into()
|
||||
};
|
||||
set_private_value(
|
||||
scope,
|
||||
args.this(),
|
||||
SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT,
|
||||
value,
|
||||
);
|
||||
simple_object_event_set_ordered_handler(
|
||||
scope,
|
||||
args.this(),
|
||||
SERVICE_WORKER_REGISTRATION_EVENTS_SLOT,
|
||||
"updatefound",
|
||||
SERVICE_WORKER_REGISTRATION_ONUPDATEFOUND_SLOT,
|
||||
active,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_service_worker_registration_update_found<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let Some(registration) =
|
||||
get_private_value(scope, global, SERVICE_WORKER_GLOBAL_REGISTRATION_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Ok(prototype) =
|
||||
crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "Event")
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let event = v8::Object::new(scope);
|
||||
let _ = event.set_prototype(scope, prototype.into());
|
||||
crate::context_bootstrap::initialize_event_object(scope, event, "updatefound", false, false);
|
||||
crate::context_bootstrap::mark_event_trusted(scope, event);
|
||||
crate::context_bootstrap::dispatch_simple_event_target_event(
|
||||
scope,
|
||||
registration,
|
||||
SERVICE_WORKER_REGISTRATION_EVENTS_SLOT,
|
||||
"updatefound",
|
||||
event,
|
||||
);
|
||||
}
|
||||
|
||||
fn build_service_worker_global_navigation_preload_manager<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
scope_url: &Url,
|
||||
@@ -3450,6 +3597,98 @@ pub(super) fn build_service_worker_global_service_worker<'s>(
|
||||
Ok(worker)
|
||||
}
|
||||
|
||||
fn service_worker_registration_update_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
return;
|
||||
};
|
||||
rv.set(resolver.get_promise(scope).into());
|
||||
let registration_id =
|
||||
get_private_value(scope, args.this(), SERVICE_WORKER_REGISTRATION_ID_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::BigInt>::try_from(value).ok())
|
||||
.map(|value| value.u64_value())
|
||||
.filter(|(_, lossless)| *lossless)
|
||||
.map(|(value, _)| {
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationId::from_u64_for_binding(
|
||||
value,
|
||||
)
|
||||
});
|
||||
let Some(registration_id) = registration_id else {
|
||||
let _ = resolver.reject(
|
||||
scope,
|
||||
v8::Exception::type_error(
|
||||
scope,
|
||||
v8str(
|
||||
scope,
|
||||
"Illegal invocation of ServiceWorkerRegistration.update",
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(state) = get_worker_state(scope) else {
|
||||
return;
|
||||
};
|
||||
let Some(runtime) = worker_service_worker_runtime(scope) else {
|
||||
let error = crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"The registration runtime is unavailable.",
|
||||
"InvalidStateError",
|
||||
);
|
||||
let _ = resolver.reject(scope, error);
|
||||
return;
|
||||
};
|
||||
let request = {
|
||||
let mut state = state.borrow_mut();
|
||||
let caller_version_id = match state.global_kind {
|
||||
super::thread::WorkerGlobalKind::Service { version_id, .. } => Some(version_id),
|
||||
_ => None,
|
||||
};
|
||||
let Some(document_url) = state.current_script_url.clone() else {
|
||||
return;
|
||||
};
|
||||
let request_id = state.service_worker_update_request_ids.allocate();
|
||||
state.pending_service_worker_updates.insert(
|
||||
request_id,
|
||||
PendingServiceWorkerUpdate {
|
||||
resolver: v8::Global::new(scope, resolver),
|
||||
registration: v8::Global::new(scope, args.this()),
|
||||
},
|
||||
);
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationUpdate {
|
||||
registration_id,
|
||||
caller_version_id,
|
||||
storage_key: state.storage_key.serialized_storage_key(),
|
||||
document_url,
|
||||
request_client: state.loader.request_client().clone(),
|
||||
network_policy: super::handle::WorkerNetworkPolicy {
|
||||
secure_context: state.secure_context,
|
||||
permission_overrides: state.permission_overrides.clone(),
|
||||
extra_http_headers: state.extra_http_headers.clone(),
|
||||
network_offline: state.network_offline,
|
||||
blocked_url_patterns: state.blocked_url_patterns.clone(),
|
||||
network_partition_key: state.network_partition_key.clone(),
|
||||
fetch_subresource_interception_enabled: state
|
||||
.fetch_subresource_interception_enabled,
|
||||
fetch_subresource_interception_resource_type: state
|
||||
.fetch_subresource_interception_resource_type,
|
||||
},
|
||||
worker_context_runtime: state.worker_context_runtime.clone(),
|
||||
broadcast_channel_top_level_site: Some(state.storage_key.top_level_site().to_owned()),
|
||||
indexed_db_manager: state.indexed_db_manager.clone(),
|
||||
storage_bucket_store: state.storage_bucket_store.clone(),
|
||||
completion: crate::service_worker_runtime::ServiceWorkerRegisterJob::Worker {
|
||||
request_id,
|
||||
completion_tx: state.worker_wake_tx.clone(),
|
||||
},
|
||||
}
|
||||
};
|
||||
runtime.start_registration_update(request);
|
||||
}
|
||||
|
||||
fn service_worker_registration_unregister_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
|
||||
@@ -109,6 +109,18 @@ pub(crate) enum WorkerMessage {
|
||||
ServiceWorkerPeriodicSyncEvent(Box<ServiceWorkerPeriodicSyncEvent>),
|
||||
/// Dispatch `navigator.serviceWorker` `controllerchange` in this worker client.
|
||||
ServiceWorkerControllerChange,
|
||||
/// Dispatch updatefound on the original registration in this service worker realm.
|
||||
ServiceWorkerRegistrationUpdateFound,
|
||||
/// Settle a registration.update() promise on the requesting worker loop.
|
||||
ServiceWorkerUpdateResult {
|
||||
request_id: u64,
|
||||
result: Box<
|
||||
Result<
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationSnapshot,
|
||||
crate::service_worker_runtime::ServiceWorkerRegistrationError,
|
||||
>,
|
||||
>,
|
||||
},
|
||||
/// Resolve a Service Worker `SyncManager.register()` request in the worker.
|
||||
ServiceWorkerSyncRegistrationResult(ServiceWorkerSyncRegistrationResult),
|
||||
/// Resolve a Service Worker `SyncManager.getTags()` request in the worker.
|
||||
@@ -942,6 +954,12 @@ impl WorkerHandle {
|
||||
.send(WorkerMessage::ServiceWorkerLifecycleEvent(Box::new(event)));
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_service_worker_registration_update_found(&self) {
|
||||
let _ = self
|
||||
.tx
|
||||
.send(WorkerMessage::ServiceWorkerRegistrationUpdateFound);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_service_worker_fetch_event(&self, event: ServiceWorkerFetchEvent) {
|
||||
let _ = self
|
||||
.tx
|
||||
|
||||
@@ -22,6 +22,9 @@ mod script_mime;
|
||||
mod thread;
|
||||
mod timer_callback;
|
||||
|
||||
pub(crate) type WorkerScriptUpdateResources =
|
||||
std::collections::HashMap<url::Url, Result<WorkerScriptResource, String>>;
|
||||
|
||||
pub(crate) use thread::perform_callback_cleanup_checkpoint_if_worker;
|
||||
|
||||
pub(crate) use data_url::decode_data_url_script_source;
|
||||
|
||||
@@ -4090,6 +4090,7 @@ fn maybe_send_service_worker_lifecycle_completion(
|
||||
let completion = pending.completion.clone();
|
||||
if completion.kind == ServiceWorkerLifecycleEventKind::Install {
|
||||
state.service_worker_can_import_new_scripts = false;
|
||||
state.service_worker_updated_script_resources.clear();
|
||||
}
|
||||
state
|
||||
.pending_service_worker_lifecycle_events
|
||||
|
||||
@@ -84,6 +84,7 @@ use super::global_scope::{
|
||||
drain_service_worker_push_get_subscription_result, drain_service_worker_push_subscribe_result,
|
||||
drain_service_worker_push_unsubscribe_result, drain_service_worker_show_notification_result,
|
||||
drain_service_worker_sync_get_tags_result, drain_service_worker_sync_registration_result,
|
||||
drain_service_worker_update_result,
|
||||
drain_worker_fetch_completion, drain_worker_opfs_completion, drain_worker_webcrypto_completion,
|
||||
drain_worker_xhr_event, fail_pending_worker_csp_report, fail_pending_worker_fetch,
|
||||
fail_pending_worker_fetch_auth, fail_pending_worker_fetch_response, fail_pending_worker_xhr,
|
||||
@@ -204,6 +205,7 @@ pub(crate) struct WorkerSpawnOptions {
|
||||
pub(crate) creator_storage_key: Option<MoliStorageKey>,
|
||||
pub(crate) service_worker_runtime: Option<ServiceWorkerRuntimeService>,
|
||||
pub(crate) service_worker_script_resources: Vec<WorkerScriptResource>,
|
||||
pub(crate) service_worker_updated_script_resources: crate::worker::WorkerScriptUpdateResources,
|
||||
pub(crate) service_worker_can_import_new_scripts: bool,
|
||||
pub(crate) reserved_service_worker_client_id: Option<ServiceWorkerClientId>,
|
||||
pub(crate) indexed_db_manager: Option<crate::context_bootstrap::WeakIndexedDbManager>,
|
||||
@@ -341,6 +343,7 @@ impl WorkerSpawnOptions {
|
||||
creator_storage_key: None,
|
||||
service_worker_runtime: None,
|
||||
service_worker_script_resources: Vec::new(),
|
||||
service_worker_updated_script_resources: Default::default(),
|
||||
service_worker_can_import_new_scripts: true,
|
||||
reserved_service_worker_client_id: None,
|
||||
indexed_db_manager: None,
|
||||
@@ -469,6 +472,14 @@ impl WorkerSpawnOptions {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_service_worker_updated_script_resources(
|
||||
mut self,
|
||||
resources: crate::worker::WorkerScriptUpdateResources,
|
||||
) -> Self {
|
||||
self.service_worker_updated_script_resources = resources;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_storage_key_top_level_site(self, top_level_site: Option<String>) -> Self {
|
||||
self.with_broadcast_channel_top_level_site(top_level_site)
|
||||
}
|
||||
@@ -584,6 +595,7 @@ fn worker_has_pending_async(state: &Rc<RefCell<WorkerGlobalState>>) -> bool {
|
||||
|| !state.pending_service_worker_client_focuses.is_empty()
|
||||
|| !state.pending_service_worker_clients_open_windows.is_empty()
|
||||
|| !state.pending_service_worker_show_notifications.is_empty()
|
||||
|| !state.pending_service_worker_updates.is_empty()
|
||||
|| !state.pending_service_worker_get_notifications.is_empty()
|
||||
|| !state.pending_service_worker_sync_registrations.is_empty()
|
||||
|| !state.pending_service_worker_sync_get_tags.is_empty()
|
||||
@@ -1364,6 +1376,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa
|
||||
creator_storage_key,
|
||||
service_worker_runtime,
|
||||
service_worker_script_resources,
|
||||
service_worker_updated_script_resources,
|
||||
service_worker_can_import_new_scripts,
|
||||
reserved_service_worker_client_id,
|
||||
indexed_db_manager,
|
||||
@@ -1418,6 +1431,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa
|
||||
creator_storage_key,
|
||||
service_worker_runtime,
|
||||
service_worker_script_resources,
|
||||
service_worker_updated_script_resources,
|
||||
service_worker_can_import_new_scripts,
|
||||
reserved_service_worker_client_id,
|
||||
indexed_db_manager,
|
||||
@@ -1554,6 +1568,7 @@ async fn worker_main(
|
||||
creator_storage_key: Option<MoliStorageKey>,
|
||||
service_worker_runtime: Option<ServiceWorkerRuntimeService>,
|
||||
service_worker_script_resources: Vec<WorkerScriptResource>,
|
||||
service_worker_updated_script_resources: crate::worker::WorkerScriptUpdateResources,
|
||||
service_worker_can_import_new_scripts: bool,
|
||||
reserved_service_worker_client_id: Option<ServiceWorkerClientId>,
|
||||
indexed_db_manager: Option<crate::context_bootstrap::WeakIndexedDbManager>,
|
||||
@@ -1678,6 +1693,7 @@ async fn worker_main(
|
||||
.into_iter()
|
||||
.map(|resource| (resource.request_url.clone(), resource))
|
||||
.collect(),
|
||||
service_worker_updated_script_resources,
|
||||
service_worker_can_import_new_scripts,
|
||||
referrer_policy,
|
||||
module_static_import_content_security_policies,
|
||||
@@ -1735,6 +1751,8 @@ async fn worker_main(
|
||||
pending_service_worker_client_focuses: std::collections::HashMap::new(),
|
||||
pending_service_worker_clients_open_windows: std::collections::HashMap::new(),
|
||||
pending_service_worker_show_notifications: std::collections::HashMap::new(),
|
||||
pending_service_worker_updates: std::collections::HashMap::new(),
|
||||
service_worker_update_request_ids: Default::default(),
|
||||
pending_service_worker_get_notifications: std::collections::HashMap::new(),
|
||||
pending_service_worker_sync_registrations: std::collections::HashMap::new(),
|
||||
pending_service_worker_sync_get_tags: std::collections::HashMap::new(),
|
||||
@@ -2503,6 +2521,37 @@ async fn worker_main(
|
||||
perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope);
|
||||
drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx);
|
||||
}
|
||||
WorkerLoopWake::Message(Some(WorkerMessage::ServiceWorkerUpdateResult {
|
||||
request_id,
|
||||
result,
|
||||
})) => {
|
||||
if pending_module_bootstrap.is_some() {
|
||||
pending_bootstrap_messages
|
||||
.push_back(WorkerMessage::ServiceWorkerUpdateResult { request_id, result });
|
||||
continue;
|
||||
}
|
||||
let scope = pin!(v8::HandleScope::new(worker_isolate.worker_isolate_mut()));
|
||||
let scope = &mut scope.init();
|
||||
let ctx = v8::Local::new(scope, &context);
|
||||
let scope = &mut v8::ContextScope::new(scope, ctx);
|
||||
drain_service_worker_update_result(scope, &state, request_id, *result);
|
||||
perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope);
|
||||
drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx);
|
||||
}
|
||||
WorkerLoopWake::Message(Some(WorkerMessage::ServiceWorkerRegistrationUpdateFound)) => {
|
||||
if pending_module_bootstrap.is_some() {
|
||||
pending_bootstrap_messages
|
||||
.push_back(WorkerMessage::ServiceWorkerRegistrationUpdateFound);
|
||||
continue;
|
||||
}
|
||||
let scope = pin!(v8::HandleScope::new(worker_isolate.worker_isolate_mut()));
|
||||
let scope = &mut scope.init();
|
||||
let ctx = v8::Local::new(scope, &context);
|
||||
let scope = &mut v8::ContextScope::new(scope, ctx);
|
||||
super::global_scope::dispatch_service_worker_registration_update_found(scope);
|
||||
perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope);
|
||||
drain_worker_dynamic_module_imports(scope, &state, &module_graph_fetch_tx);
|
||||
}
|
||||
WorkerLoopWake::Message(Some(WorkerMessage::ServiceWorkerShowNotificationResult(
|
||||
result,
|
||||
))) => {
|
||||
|
||||
Reference in New Issue
Block a user