From 7faebb2702a2a19fc401fde1856a0d90861d2904 Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Mon, 7 Oct 2024 20:18:33 -0600 Subject: [PATCH] LibWeb: Implement most of Service Worker registration This approach will need some rework to be properly handled at the user agent level instead of per renderer process, but it's a start. --- .../Libraries/LibWeb/ServiceWorker/BUILD.gn | 6 +- .../ServiceWorker/service-worker-register.txt | 2 +- Userland/Libraries/LibWeb/CMakeLists.txt | 2 + .../Libraries/LibWeb/ServiceWorker/Job.cpp | 178 +++++++++++++++++- Userland/Libraries/LibWeb/ServiceWorker/Job.h | 4 +- .../LibWeb/ServiceWorker/Registration.cpp | 98 ++++++++++ .../LibWeb/ServiceWorker/Registration.h | 63 +++++++ .../LibWeb/ServiceWorker/ServiceWorker.cpp | 10 + .../LibWeb/ServiceWorker/ServiceWorker.h | 27 +++ 9 files changed, 377 insertions(+), 13 deletions(-) create mode 100644 Userland/Libraries/LibWeb/ServiceWorker/Registration.cpp create mode 100644 Userland/Libraries/LibWeb/ServiceWorker/Registration.h create mode 100644 Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.cpp create mode 100644 Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.h diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn index 6c84f4069ace..82f190399589 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn @@ -1,5 +1,9 @@ source_set("ServiceWorker") { configs += [ "//Userland/Libraries/LibWeb:configs" ] deps = [ "//Userland/Libraries/LibWeb:all_generated" ] - sources = [ "Job.cpp" ] + sources = [ + "Job.cpp", + "Registration.cpp", + "ServiceWorker.cpp", + ] } diff --git a/Tests/LibWeb/Text/expected/ServiceWorker/service-worker-register.txt b/Tests/LibWeb/Text/expected/ServiceWorker/service-worker-register.txt index d539488e8bc0..9b822998077e 100644 --- a/Tests/LibWeb/Text/expected/ServiceWorker/service-worker-register.txt +++ b/Tests/LibWeb/Text/expected/ServiceWorker/service-worker-register.txt @@ -1 +1 @@ -ServiceWorker registration failed: InternalError: TODO(Service Worker registration is not implemented in LibJS) +ServiceWorker registration failed: InternalError: TODO(Service Worker update is not implemented in LibJS) diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index cee90b3da34b..71e11a1b8e56 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -635,6 +635,8 @@ set(SOURCES ResizeObserver/ResizeObserverSize.cpp SecureContexts/AbstractOperations.cpp ServiceWorker/Job.cpp + ServiceWorker/Registration.cpp + ServiceWorker/ServiceWorker.cpp SRI/SRI.cpp StorageAPI/NavigatorStorage.cpp StorageAPI/StorageKey.cpp diff --git a/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp b/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp index 34fac4179b64..e217422579d9 100644 --- a/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp +++ b/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp @@ -1,21 +1,33 @@ /* - * Copyright (c) 2024, Andrew Kaster + * Copyright (c) 2024, Andrew Kaster * * SPDX-License-Identifier: BSD-2-Clause */ #include #include +#include #include #include +#include #include +#include #include namespace Web::ServiceWorker { +static void run_job(JS::VM&, JobQueue&); +static void finish_job(JS::VM&, JS::NonnullGCPtr); +static void resolve_job_promise(JS::NonnullGCPtr, Optional, JS::Value = JS::js_null()); +template +static void reject_job_promise(JS::NonnullGCPtr, FlyString message); +static void register_(JS::VM&, JS::NonnullGCPtr); +static void update(JS::VM&, JS::NonnullGCPtr); +static void unregister(JS::VM&, JS::NonnullGCPtr); + JS_DEFINE_ALLOCATOR(Job); -// https://w3c.github.io/ServiceWorker/#create-job +// https://w3c.github.io/ServiceWorker/#create-job-algorithm JS::NonnullGCPtr Job::create(JS::VM& vm, Job::Type type, StorageAPI::StorageKey storage_key, URL::URL scope_url, URL::URL script_url, JS::GCPtr promise, JS::GCPtr client) { return vm.heap().allocate_without_realm(type, move(storage_key), move(scope_url), move(script_url), promise, client); @@ -54,14 +66,73 @@ static HashMap& scope_to_job_queue_map() return map; } +// https://w3c.github.io/ServiceWorker/#register-algorithm static void register_(JS::VM& vm, JS::NonnullGCPtr job) { - // If there's no client, there won't be any promises to resolve - if (job->client) { - auto context = HTML::TemporaryExecutionContext(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); - auto& realm = *vm.current_realm(); - WebIDL::reject_promise(realm, *job->job_promise, *vm.throw_completion(JS::ErrorType::NotImplemented, "Service Worker registration"sv).value()); + auto script_origin = job->script_url.origin(); + auto scope_origin = job->scope_url.origin(); + auto referrer_origin = job->referrer->origin(); + + // 1. If the result of running potentially trustworthy origin with the origin of job’s script url as the argument is Not Trusted, then: + if (SecureContexts::Trustworthiness::NotTrustworthy == SecureContexts::is_origin_potentially_trustworthy(script_origin)) { + // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException. + reject_job_promise(job, "Service Worker registration has untrustworthy script origin"_fly_string); + + // 2. Invoke Finish Job with job and abort these steps. + finish_job(vm, job); + return; + } + + // 2. If job’s script url's origin and job’s referrer's origin are not same origin, then: + if (!script_origin.is_same_origin(referrer_origin)) { + // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException. + reject_job_promise(job, "Service Worker registration has incompatible script and referrer origins"_fly_string); + + // 2. Invoke Finish Job with job and abort these steps. + finish_job(vm, job); + return; } + + // 3. If job’s scope url's origin and job’s referrer's origin are not same origin, then: + if (!scope_origin.is_same_origin(referrer_origin)) { + // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException. + reject_job_promise(job, "Service Worker registration has incompatible scope and referrer origins"_fly_string); + + // 2. Invoke Finish Job with job and abort these steps. + finish_job(vm, job); + return; + } + + // 4. Let registration be the result of running Get Registration given job’s storage key and job’s scope url. + auto registration = Registration::get(job->storage_key, job->scope_url); + + // 5. If registration is not null, then: + if (registration.has_value()) { + // 1. Let newestWorker be the result of running the Get Newest Worker algorithm passing registration as the argument. + auto* newest_worker = registration->newest_worker(); + + // 2. If newestWorker is not null, job’s script url equals newestWorker’s script url, + // job’s worker type equals newestWorker’s type, and job’s update via cache mode's value equals registration’s update via cache mode, then: + if (newest_worker != nullptr + && job->script_url == newest_worker->script_url + && job->worker_type == newest_worker->worker_type + && job->update_via_cache == registration->update_via_cache()) { + // 1. Invoke Resolve Job Promise with job and registration. + resolve_job_promise(job, registration.value()); + + // 2. Invoke Finish Job with job and abort these steps. + finish_job(vm, job); + return; + } + } + // 6. Else: + else { + // 1. Invoke Set Registration algorithm with job’s storage key, job’s scope url, and job’s update via cache mode. + Registration::set(job->storage_key, job->scope_url, job->update_via_cache); + } + + // Invoke Update algorithm passing job as the argument. + update(vm, job); } static void update(JS::VM& vm, JS::NonnullGCPtr job) @@ -84,7 +155,7 @@ static void unregister(JS::VM& vm, JS::NonnullGCPtr job) } } -// https://w3c.github.io/ServiceWorker/#run-job +// https://w3c.github.io/ServiceWorker/#run-job-algorithm static void run_job(JS::VM& vm, JobQueue& job_queue) { // 1. Assert: jobQueue is not empty. @@ -122,13 +193,102 @@ static void run_job(JS::VM& vm, JobQueue& job_queue) HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, job_run_steps); } -// https://w3c.github.io/ServiceWorker/#schedule-job +// https://w3c.github.io/ServiceWorker/#finish-job-algorithm +static void finish_job(JS::VM& vm, JS::NonnullGCPtr job) +{ + // 1. Let jobQueue be job’s containing job queue. + auto& job_queue = *job->containing_job_queue; + + // 2. Assert: the first item in jobQueue is job. + VERIFY(job_queue.first() == job); + + // 3. Dequeue from jobQueue + (void)job_queue.take_first(); + + // 4. If jobQueue is not empty, invoke Run Job with jobQueue. + if (!job_queue.is_empty()) + run_job(vm, job_queue); +} + +// https://w3c.github.io/ServiceWorker/#resolve-job-promise-algorithm +static void resolve_job_promise(JS::NonnullGCPtr job, Optional, JS::Value value) +{ + // 1. If job’s client is not null, queue a task, on job’s client's responsible event loop using the DOM manipulation task source, to run the following substeps: + if (job->client) { + auto& realm = job->client->realm(); + HTML::queue_a_task(HTML::Task::Source::DOMManipulation, job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, job, value] { + HTML::TemporaryExecutionContext const context(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // FIXME: Resolve to a ServiceWorkerRegistration platform object + // 1. Let convertedValue be null. + // 2. If job’s job type is either register or update, set convertedValue to the result of + // getting the service worker registration object that represents value in job’s client. + // 3. Else, set convertedValue to value, in job’s client's Realm. + // 4. Resolve job’s job promise with convertedValue. + WebIDL::resolve_promise(realm, *job->job_promise, value); + })); + } + + // 2. For each equivalentJob in job’s list of equivalent jobs: + for (auto& equivalent_job : job->list_of_equivalent_jobs) { + // 1. If equivalentJob’s client is null, continue. + if (!equivalent_job->client) + continue; + + // 2. Queue a task, on equivalentJob’s client's responsible event loop using the DOM manipulation task source, + // to run the following substeps: + auto& realm = equivalent_job->client->realm(); + HTML::queue_a_task(HTML::Task::Source::DOMManipulation, equivalent_job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, equivalent_job, value] { + HTML::TemporaryExecutionContext const context(*equivalent_job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // FIXME: Resolve to a ServiceWorkerRegistration platform object + // 1. Let convertedValue be null. + // 2. If equivalentJob’s job type is either register or update, set convertedValue to the result of + // getting the service worker registration object that represents value in equivalentJob’s client. + // 3. Else, set convertedValue to value, in equivalentJob’s client's Realm. + // 4. Resolve equivalentJob’s job promise with convertedValue. + WebIDL::resolve_promise(realm, *equivalent_job->job_promise, value); + })); + } +} + +// https://w3c.github.io/ServiceWorker/#reject-job-promise-algorithm +template +static void reject_job_promise(JS::NonnullGCPtr job, FlyString message) +{ + // 1. If job’s client is not null, queue a task, on job’s client's responsible event loop using the DOM manipulation task source, + // to reject job’s job promise with a new exception with errorData and a user agent-defined message, in job’s client's Realm. + if (job->client) { + auto& realm = job->client->realm(); + HTML::queue_a_task(HTML::Task::Source::DOMManipulation, job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, job, message] { + HTML::TemporaryExecutionContext const context(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + WebIDL::reject_promise(realm, *job->job_promise, Error::create(realm, message)); + })); + } + + // 2. For each equivalentJob in job’s list of equivalent jobs: + for (auto& equivalent_job : job->list_of_equivalent_jobs) { + // 1. If equivalentJob’s client is null, continue. + if (!equivalent_job->client) + continue; + + // 2. Queue a task, on equivalentJob’s client's responsible event loop using the DOM manipulation task source, + // to reject equivalentJob’s job promise with a new exception with errorData and a user agent-defined message, + // in equivalentJob’s client's Realm. + auto& realm = equivalent_job->client->realm(); + HTML::queue_a_task(HTML::Task::Source::DOMManipulation, equivalent_job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, equivalent_job, message] { + HTML::TemporaryExecutionContext const context(*equivalent_job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + WebIDL::reject_promise(realm, *equivalent_job->job_promise, Error::create(realm, message)); + })); + } +} + +// https://w3c.github.io/ServiceWorker/#schedule-job-algorithm void schedule_job(JS::VM& vm, JS::NonnullGCPtr job) { // 1. Let jobQueue be null. // Note: See below for how we ensure job queue // 2. Let jobScope be job’s scope url, serialized. + // FIXME: Suspect that spec should specify to not use fragment here auto job_scope = job->scope_url.serialize(); // 3. If scope to job queue map[jobScope] does not exist, set scope to job queue map[jobScope] to a new job queue. diff --git a/Userland/Libraries/LibWeb/ServiceWorker/Job.h b/Userland/Libraries/LibWeb/ServiceWorker/Job.h index 754d50c853c4..5774b2a94601 100644 --- a/Userland/Libraries/LibWeb/ServiceWorker/Job.h +++ b/Userland/Libraries/LibWeb/ServiceWorker/Job.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Andrew Kaster + * Copyright (c) 2024, Andrew Kaster * * SPDX-License-Identifier: BSD-2-Clause */ @@ -73,7 +73,7 @@ struct Job : public JS::Cell { Job(Type, StorageAPI::StorageKey, URL::URL scope_url, URL::URL script_url, JS::GCPtr, JS::GCPtr client); }; -// https://w3c.github.io/ServiceWorker/#schedule-job +// https://w3c.github.io/ServiceWorker/#schedule-job-algorithm void schedule_job(JS::VM&, JS::NonnullGCPtr); } diff --git a/Userland/Libraries/LibWeb/ServiceWorker/Registration.cpp b/Userland/Libraries/LibWeb/ServiceWorker/Registration.cpp new file mode 100644 index 000000000000..1fd603e76fd2 --- /dev/null +++ b/Userland/Libraries/LibWeb/ServiceWorker/Registration.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::ServiceWorker { + +struct RegistrationKey { + StorageAPI::StorageKey key; + ByteString serialized_scope_url; + + constexpr bool operator==(RegistrationKey const&) const = default; +}; + +// FIXME: Surely this needs hooks to be cleared and manipulated at the UA level +// Does this need to be serialized to disk as well? +static HashMap s_registrations; + +Registration::Registration(StorageAPI::StorageKey storage_key, URL::URL scope, Bindings::ServiceWorkerUpdateViaCache update_via_cache) + : m_storage_key(move(storage_key)) + , m_scope_url(move(scope)) + , m_update_via_cache_mode(update_via_cache) +{ +} + +// https://w3c.github.io/ServiceWorker/#dfn-service-worker-registration-unregistered +bool Registration::is_unregistered() +{ + // A service worker registration is said to be unregistered if registration map[this service worker registration's (storage key, serialized scope url)] is not this service worker registration. + // FIXME: Suspect that spec should say to serialize without fragment + auto const key = RegistrationKey { m_storage_key, m_scope_url.serialize(URL::ExcludeFragment::Yes) }; + return s_registrations.get(key).map([](auto& registration) { return ®istration; }).value_or(nullptr) != this; +} + +// https://w3c.github.io/ServiceWorker/#get-registration-algorithm +Optional Registration::get(StorageAPI::StorageKey const& key, Optional scope) +{ + // 1. Run the following steps atomically. + // FIXME: What does this mean? Do we need a mutex? does it need to be 'locked' at the UA level? + + // 2. Let scopeString be the empty string. + ByteString scope_string; + + // 3. If scope is not null, set scopeString to serialized scope with the exclude fragment flag set. + if (scope.has_value()) + scope_string = scope.value().serialize(URL::ExcludeFragment::Yes); + + // 4. For each (entry storage key, entry scope) → registration of registration map: + // 1. If storage key equals entry storage key and scopeString matches entry scope, then return registration. + // 5. Return null. + return s_registrations.get({ key, scope_string }); +} + +// https://w3c.github.io/ServiceWorker/#set-registration-algorithm +Registration& Registration::set(StorageAPI::StorageKey const& storage_key, URL::URL const& scope, Bindings::ServiceWorkerUpdateViaCache update_via_cache) +{ + // FIXME: 1. Run the following steps atomically. + + // 2. Let scopeString be serialized scope with the exclude fragment flag set. + // 3. Let registration be a new service worker registration whose storage key is set to storage key, scope url is set to scope, and update via cache mode is set to updateViaCache. + // 4. Set registration map[(storage key, scopeString)] to registration. + // 5. Return registration. + + // FIXME: Is there a way to "ensure but always replace?" + auto key = RegistrationKey { storage_key, scope.serialize(URL::ExcludeFragment::Yes) }; + (void)s_registrations.set(key, Registration(storage_key, scope, update_via_cache)); + return s_registrations.get(key).value(); +} + +// https://w3c.github.io/ServiceWorker/#get-newest-worker +ServiceWorker* Registration::newest_worker() const +{ + // FIXME: 1. Run the following steps atomically. + + // 2. Let newestWorker be null. + // 3. If registration’s installing worker is not null, set newestWorker to registration’s installing worker. + // 4. If registration’s waiting worker is not null, set newestWorker to registration’s waiting worker. + // 5. If registration’s active worker is not null, set newestWorker to registration’s active worker. + // 6. Return newestWorker. + return m_installing_worker ? m_installing_worker : m_waiting_worker ? m_waiting_worker + : m_active_worker; +} + +} + +namespace AK { +template<> +struct Traits : public DefaultTraits { + static unsigned hash(Web::ServiceWorker::RegistrationKey const& key) + { + return pair_int_hash(Traits::hash(key.key), Traits::hash(key.serialized_scope_url)); + } +}; +} diff --git a/Userland/Libraries/LibWeb/ServiceWorker/Registration.h b/Userland/Libraries/LibWeb/ServiceWorker/Registration.h new file mode 100644 index 000000000000..fd2f8d0913d5 --- /dev/null +++ b/Userland/Libraries/LibWeb/ServiceWorker/Registration.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::ServiceWorker { + +// https://w3c.github.io/ServiceWorker/#dfn-service-worker-registration +// This class corresponds to "service worker registration", not "ServiceWorkerRegistration" +// FIXME: This object needs to live at the user-agent level, in LibWebView, not in LibWeb +// .. And it will need some way to synchronize updates to each 'client' (aka process aka ESO) +class Registration { + AK_MAKE_NONCOPYABLE(Registration); + AK_MAKE_DEFAULT_MOVABLE(Registration); + +public: + // https://w3c.github.io/ServiceWorker/#get-registration-algorithm + static Optional get(StorageAPI::StorageKey const&, Optional scope); + + // https://w3c.github.io/ServiceWorker/#set-registration-algorithm + static Registration& set(StorageAPI::StorageKey const&, URL::URL const&, Bindings::ServiceWorkerUpdateViaCache); + + bool is_unregistered(); + + StorageAPI::StorageKey const& storage_key() const { return m_storage_key; } + URL::URL const& scope_url() const { return m_scope_url; } + Bindings::ServiceWorkerUpdateViaCache update_via_cache() const { return m_update_via_cache_mode; } + + ServiceWorker* newest_worker() const; + +private: + Registration(StorageAPI::StorageKey, URL::URL, Bindings::ServiceWorkerUpdateViaCache); + + StorageAPI::StorageKey m_storage_key; // https://w3c.github.io/ServiceWorker/#service-worker-registration-storage-key + URL::URL m_scope_url; // https://w3c.github.io/ServiceWorker/#dfn-scope-url + + // NOTE: These are "service workers", not "HTML::ServiceWorker"s + ServiceWorker* m_installing_worker { nullptr }; // https://w3c.github.io/ServiceWorker/#dfn-installing-worker + ServiceWorker* m_waiting_worker { nullptr }; // https://w3c.github.io/ServiceWorker/#dfn-waiting-worker + ServiceWorker* m_active_worker { nullptr }; // https://w3c.github.io/ServiceWorker/#dfn-active-worker + + Optional m_last_update_check_time; // https://w3c.github.io/ServiceWorker/#dfn-last-update-check-time + Bindings::ServiceWorkerUpdateViaCache m_update_via_cache_mode = Bindings::ServiceWorkerUpdateViaCache::Imports; // https://w3c.github.io/ServiceWorker/#dfn-update-via-cache + // FIXME: A service worker registration has one or more task queues... https://w3c.github.io/ServiceWorker/#dfn-service-worker-registration-task-queue + // FIXME: Spec bug: A service worker registration has an associated NavigationPreloadManager object. + // This can't possibly be true. The association is the other way around. + + bool m_navigation_preload_enabled = { false }; // https://w3c.github.io/ServiceWorker/#service-worker-registration-navigation-preload-enabled-flag + ByteString m_navigation_preload_header_value; // https://w3c.github.io/ServiceWorker/#service-worker-registration-navigation-preload-header-value +}; + +} diff --git a/Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.cpp b/Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.cpp new file mode 100644 index 000000000000..203a1b2aa43c --- /dev/null +++ b/Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.cpp @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Web::ServiceWorker { +} diff --git a/Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.h b/Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.h new file mode 100644 index 000000000000..b1d14fac05f0 --- /dev/null +++ b/Userland/Libraries/LibWeb/ServiceWorker/ServiceWorker.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::ServiceWorker { + +// https://w3c.github.io/ServiceWorker/#dfn-service-worker +// This class corresponds to "service worker", not "ServiceWorker" +// FIXME: This should be owned and managed at the user agent level +// FIXME: A lot of the fields for this struct actually need to live in the Agent for the service worker in the WebWorker process +struct ServiceWorker { + Bindings::ServiceWorkerState state = Bindings::ServiceWorkerState::Parsed; // https://w3c.github.io/ServiceWorker/#dfn-state + URL::URL script_url; // https://w3c.github.io/ServiceWorker/#dfn-script-url + Bindings::WorkerType worker_type = Bindings::WorkerType::Classic; // https://w3c.github.io/ServiceWorker/#dfn-type + + // FIXME: A lot more fields after this... +}; + +}