diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn index e84871de80c..8732dbca104 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/BUILD.gn @@ -335,6 +335,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 f26cd4e637e..23d92fb773e 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -641,6 +641,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); + +}