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);
+
+}