From 29416befe640eae7bf44b72e3f4ad42a1e397701 Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Thu, 3 Oct 2024 20:17:49 -0600 Subject: [PATCH] LibWeb: Add ServiceWorker job registration and execution Now we can register jobs and they will be executed on the event loop "later". This doesn't feel like the right place to execute them, but the spec needs some updates in this regard anyway. --- .../Userland/Libraries/LibWeb/BUILD.gn | 1 + .../Libraries/LibWeb/ServiceWorker/BUILD.gn | 5 + .../ServiceWorker/service-worker-register.txt | 2 +- .../service-worker-register.html | 27 +-- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + .../LibWeb/HTML/ServiceWorkerContainer.cpp | 21 ++- .../Libraries/LibWeb/ServiceWorker/Job.cpp | 165 ++++++++++++++++++ Userland/Libraries/LibWeb/ServiceWorker/Job.h | 79 +++++++++ 8 files changed, 281 insertions(+), 20 deletions(-) create mode 100644 Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn create mode 100644 Userland/Libraries/LibWeb/ServiceWorker/Job.cpp create mode 100644 Userland/Libraries/LibWeb/ServiceWorker/Job.h diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn index 8334feedf5b..cc19094aef3 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn @@ -333,6 +333,7 @@ shared_library("LibWeb") { "SVG", "SecureContexts", "Selection", + "ServiceWorker", "StorageAPI", "Streams", "UIEvents", diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn new file mode 100644 index 00000000000..6c84f4069ac --- /dev/null +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/ServiceWorker/BUILD.gn @@ -0,0 +1,5 @@ +source_set("ServiceWorker") { + configs += [ "//Userland/Libraries/LibWeb:configs" ] + deps = [ "//Userland/Libraries/LibWeb:all_generated" ] + sources = [ "Job.cpp" ] +} diff --git a/Tests/LibWeb/Text/expected/ServiceWorker/service-worker-register.txt b/Tests/LibWeb/Text/expected/ServiceWorker/service-worker-register.txt index ad8ded6ef83..d539488e8bc 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(ServiceWorkerContainer::start_register is not implemented in LibJS) +ServiceWorker registration failed: InternalError: TODO(Service Worker registration is not implemented in LibJS) diff --git a/Tests/LibWeb/Text/input/ServiceWorker/service-worker-register.html b/Tests/LibWeb/Text/input/ServiceWorker/service-worker-register.html index d0e5b0933fa..61de5302b43 100644 --- a/Tests/LibWeb/Text/input/ServiceWorker/service-worker-register.html +++ b/Tests/LibWeb/Text/input/ServiceWorker/service-worker-register.html @@ -2,19 +2,22 @@ diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index a38e282817f..20c98892ba9 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -633,6 +633,7 @@ set(SOURCES ResizeObserver/ResizeObserverEntry.cpp ResizeObserver/ResizeObserverSize.cpp SecureContexts/AbstractOperations.cpp + ServiceWorker/Job.cpp SRI/SRI.cpp StorageAPI/NavigatorStorage.cpp StorageAPI/StorageKey.cpp diff --git a/Userland/Libraries/LibWeb/HTML/ServiceWorkerContainer.cpp b/Userland/Libraries/LibWeb/HTML/ServiceWorkerContainer.cpp index 7a1ff4df393..aeb6f0cf714 100644 --- a/Userland/Libraries/LibWeb/HTML/ServiceWorkerContainer.cpp +++ b/Userland/Libraries/LibWeb/HTML/ServiceWorkerContainer.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include namespace Web::HTML { @@ -79,7 +80,7 @@ JS::NonnullGCPtr ServiceWorkerContainer::register_(String script_ur } // https://w3c.github.io/ServiceWorker/#start-register-algorithm -void ServiceWorkerContainer::start_register(Optional scope_url, URL::URL script_url, JS::NonnullGCPtr promise, EnvironmentSettingsObject& client, URL::URL, Bindings::WorkerType, Bindings::ServiceWorkerUpdateViaCache) +void ServiceWorkerContainer::start_register(Optional scope_url, URL::URL script_url, JS::NonnullGCPtr promise, EnvironmentSettingsObject& client, URL::URL referrer, Bindings::WorkerType worker_type, Bindings::ServiceWorkerUpdateViaCache update_via_cache) { auto& realm = this->realm(); auto& vm = realm.vm(); @@ -153,14 +154,20 @@ void ServiceWorkerContainer::start_register(Optional scope_url, URL::U return; } - // FIXME: Schedule the job // 11. Let job be the result of running Create Job with register, storage key, scopeURL, scriptURL, promise, and client. - // 12. Set job’s worker type to workerType. - // 13. Set job’s update via cache to updateViaCache. - // 14. Set job’s referrer to referrer. - // 15. Invoke Schedule Job with job. + auto job = ServiceWorker::Job::create(vm, ServiceWorker::Job::Type::Register, storage_key.value(), scope_url.value(), script_url, promise, client); - WebIDL::reject_promise(realm, promise, *vm.throw_completion(JS::ErrorType::NotImplemented, "ServiceWorkerContainer::start_register"sv).value()); + // 12. Set job’s worker type to workerType. + job->worker_type = worker_type; + + // 13. Set job’s update via cache to updateViaCache. + job->update_via_cache = update_via_cache; + + // 14. Set job’s referrer to referrer. + job->referrer = move(referrer); + + // 15. Invoke Schedule Job with job. + ServiceWorker::schedule_job(vm, job); } #undef __ENUMERATE diff --git a/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp b/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp new file mode 100644 index 00000000000..34fac4179b6 --- /dev/null +++ b/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::ServiceWorker { + +JS_DEFINE_ALLOCATOR(Job); + +// https://w3c.github.io/ServiceWorker/#create-job +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); +} + +Job::Job(Job::Type type, StorageAPI::StorageKey storage_key, URL::URL scope_url, URL::URL script_url, JS::GCPtr promise, JS::GCPtr client) + : job_type(type) + , storage_key(move(storage_key)) + , scope_url(move(scope_url)) + , script_url(move(script_url)) + , client(client) + , job_promise(promise) +{ + // 8. If client is not null, set job’s referrer to client’s creation URL. + if (client) + referrer = client->creation_url; +} + +Job::~Job() = default; + +void Job::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + + visitor.visit(client); + visitor.visit(job_promise); + for (auto& job : list_of_equivalent_jobs) + visitor.visit(job); +} + +// FIXME: Does this need to be a 'user agent' level thing? Or can we have one per renderer process? +// https://w3c.github.io/ServiceWorker/#dfn-scope-to-job-queue-map +static HashMap& scope_to_job_queue_map() +{ + static HashMap map; + return map; +} + +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()); + } +} + +static void update(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 update"sv).value()); + } +} + +static void unregister(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 unregistration"sv).value()); + } +} + +// https://w3c.github.io/ServiceWorker/#run-job +static void run_job(JS::VM& vm, JobQueue& job_queue) +{ + // 1. Assert: jobQueue is not empty. + VERIFY(!job_queue.is_empty()); + + // 2. Queue a task to run these steps: + auto job_run_steps = JS::create_heap_function(vm.heap(), [&vm, &job_queue] { + // 1. Let job be the first item in jobQueue. + auto& job = job_queue.first(); + + // FIXME: Do these really need to be in parallel to the HTML event loop? Sounds fishy + switch (job->job_type) { + case Job::Type::Register: + // 2. If job’s job type is register, run Register with job in parallel. + register_(vm, job); + break; + case Job::Type::Update: + // 3. If job’s job type is update, run Update with job in parallel. + update(vm, job); + break; + case Job::Type::Unregister: + // 4. If job’s job type is unregister, run Unregister with job in parallel. + unregister(vm, job); + break; + } + }); + + // FIXME: How does the user agent ensure this happens? Is this a normative note? + // Spec-Note: + // For a register job and an update job, the user agent delays queuing a task for running the job + // until after a DOMContentLoaded event has been dispatched to the document that initiated the job. + + // FIXME: Spec should be updated to avoid 'queue a task' and use 'queue a global task' instead + // FIXME: On which task source? On which event loop? On behalf of which document? + HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, job_run_steps); +} + +// https://w3c.github.io/ServiceWorker/#schedule-job +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. + 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. + // 4. Set jobQueue to scope to job queue map[jobScope]. + auto& job_queue = scope_to_job_queue_map().ensure(job_scope, [&vm] { + return JobQueue(vm.heap()); + }); + + // 5. If jobQueue is empty, then: + if (job_queue.is_empty()) { + // 2. Set job’s containing job queue to jobQueue, and enqueue job to jobQueue. + job->containing_job_queue = &job_queue; + job_queue.append(job); + run_job(vm, job_queue); + } + // 6. Else: + else { + // 1. Let lastJob be the element at the back of jobQueue. + auto& last_job = job_queue.last(); + + // 2. If job is equivalent to lastJob and lastJob’s job promise has not settled, append job to lastJob’s list of equivalent jobs. + // FIXME: There's no WebIDL AO that corresponds to checking if an ECMAScript promise has settled + if (job == last_job && !verify_cast(*job->job_promise->promise()).is_handled()) { + last_job->list_of_equivalent_jobs.append(job); + } + // 3. Else, set job’s containing job queue to jobQueue, and enqueue job to jobQueue. + else { + job->containing_job_queue = &job_queue; + job_queue.append(job); + } + } +} + +} diff --git a/Userland/Libraries/LibWeb/ServiceWorker/Job.h b/Userland/Libraries/LibWeb/ServiceWorker/Job.h new file mode 100644 index 00000000000..754d50c853c --- /dev/null +++ b/Userland/Libraries/LibWeb/ServiceWorker/Job.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web::ServiceWorker { + +struct Job; +using JobQueue = JS::MarkedVector>; + +// https://w3c.github.io/ServiceWorker/#dfn-job +// FIXME: Consider not making this GC allocated, and give a special JobQueue class responsibility for its referenced GC objects +struct Job : public JS::Cell { + JS_CELL(Job, JS::Cell) + JS_DECLARE_ALLOCATOR(Job); + +public: + enum class Type : u8 { + Register, + Update, + Unregister, + }; + + // https://w3c.github.io/ServiceWorker/#create-job + static JS::NonnullGCPtr create(JS::VM&, Type, StorageAPI::StorageKey, URL::URL scope_url, URL::URL script_url, JS::GCPtr, JS::GCPtr client); + + virtual ~Job() override; + + Type job_type; // https://w3c.github.io/ServiceWorker/#dfn-job-type + StorageAPI::StorageKey storage_key; // https://w3c.github.io/ServiceWorker/#job-storage-key + URL::URL scope_url; + URL::URL script_url; + Bindings::WorkerType worker_type = Bindings::WorkerType::Classic; + // FIXME: The spec sometimes omits setting update_via_cache after CreateJob. Default to the default value for ServiceWorkerRegistrations + Bindings::ServiceWorkerUpdateViaCache update_via_cache = Bindings::ServiceWorkerUpdateViaCache::Imports; + JS::GCPtr client = nullptr; + Optional referrer; + // FIXME: Spec just references this as an ECMAScript promise https://github.com/w3c/ServiceWorker/issues/1731 + JS::GCPtr job_promise = nullptr; + RawPtr containing_job_queue = nullptr; + Vector> list_of_equivalent_jobs; + bool force_cache_bypass = false; + + // https://w3c.github.io/ServiceWorker/#dfn-job-equivalent + friend bool operator==(Job const& a, Job const& b) + { + if (a.job_type != b.job_type) + return false; + switch (a.job_type) { + case Type::Register: + case Type::Update: + return a.scope_url == b.scope_url + && a.script_url == b.script_url + && a.worker_type == b.worker_type + && a.update_via_cache == b.update_via_cache; + case Type::Unregister: + return a.scope_url == b.scope_url; + } + } + +private: + virtual void visit_edges(JS::Cell::Visitor& visitor) override; + + Job(Type, StorageAPI::StorageKey, URL::URL scope_url, URL::URL script_url, JS::GCPtr, JS::GCPtr client); +}; + +// https://w3c.github.io/ServiceWorker/#schedule-job +void schedule_job(JS::VM&, JS::NonnullGCPtr); + +}