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.
This commit is contained in:
Andrew Kaster 2024-10-03 20:17:49 -06:00 committed by Andreas Kling
parent 4a43d0ac98
commit 29416befe6
Notes: github-actions[bot] 2024-10-04 05:09:03 +00:00
8 changed files with 281 additions and 20 deletions

View file

@ -333,6 +333,7 @@ shared_library("LibWeb") {
"SVG",
"SecureContexts",
"Selection",
"ServiceWorker",
"StorageAPI",
"Streams",
"UIEvents",

View file

@ -0,0 +1,5 @@
source_set("ServiceWorker") {
configs += [ "//Userland/Libraries/LibWeb:configs" ]
deps = [ "//Userland/Libraries/LibWeb:all_generated" ]
sources = [ "Job.cpp" ]
}

View file

@ -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)

View file

@ -2,19 +2,22 @@
<script src="../include.js"></script>
<script>
asyncTest(done => {
setTimeout(() => {
// We need to do this spoofing later in the event loop so that we don't end up
// telling the test runner the wrong URL in page_did_finish_loading.
spoofCurrentURL("https://example.com/service-worker-register.html");
spoofCurrentURL("https://example.com/service-worker-register.html");
let swPromise = navigator.serviceWorker.register("service-worker.js");
let swPromise = navigator.serviceWorker.register("service-worker.js");
swPromise
.then(registration => {
println(`ServiceWorker registration successful with scope: ${registration.scope}`);
done();
})
.catch(err => {
println(`ServiceWorker registration failed: ${err}`);
done();
});
swPromise
.then(registration => {
println(`ServiceWorker registration successful with scope: ${registration.scope}`);
done();
})
.catch(err => {
println(`ServiceWorker registration failed: ${err}`);
done();
});
}, 0);
});
</script>

View file

@ -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

View file

@ -11,6 +11,7 @@
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/ServiceWorkerContainer.h>
#include <LibWeb/ServiceWorker/Job.h>
#include <LibWeb/StorageAPI/StorageKey.h>
namespace Web::HTML {
@ -79,7 +80,7 @@ JS::NonnullGCPtr<JS::Promise> ServiceWorkerContainer::register_(String script_ur
}
// https://w3c.github.io/ServiceWorker/#start-register-algorithm
void ServiceWorkerContainer::start_register(Optional<URL::URL> scope_url, URL::URL script_url, JS::NonnullGCPtr<WebIDL::Promise> promise, EnvironmentSettingsObject& client, URL::URL, Bindings::WorkerType, Bindings::ServiceWorkerUpdateViaCache)
void ServiceWorkerContainer::start_register(Optional<URL::URL> scope_url, URL::URL script_url, JS::NonnullGCPtr<WebIDL::Promise> 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<URL::URL> 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 jobs worker type to workerType.
// 13. Set jobs update via cache to updateViaCache.
// 14. Set jobs 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::InternalError>(JS::ErrorType::NotImplemented, "ServiceWorkerContainer::start_register"sv).value());
// 12. Set jobs worker type to workerType.
job->worker_type = worker_type;
// 13. Set jobs update via cache to updateViaCache.
job->update_via_cache = update_via_cache;
// 14. Set jobs referrer to referrer.
job->referrer = move(referrer);
// 15. Invoke Schedule Job with job.
ServiceWorker::schedule_job(vm, job);
}
#undef __ENUMERATE

View file

@ -0,0 +1,165 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/ServiceWorker/Job.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::ServiceWorker {
JS_DEFINE_ALLOCATOR(Job);
// https://w3c.github.io/ServiceWorker/#create-job
JS::NonnullGCPtr<Job> Job::create(JS::VM& vm, Job::Type type, StorageAPI::StorageKey storage_key, URL::URL scope_url, URL::URL script_url, JS::GCPtr<WebIDL::Promise> promise, JS::GCPtr<HTML::EnvironmentSettingsObject> client)
{
return vm.heap().allocate_without_realm<Job>(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<WebIDL::Promise> promise, JS::GCPtr<HTML::EnvironmentSettingsObject> 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 jobs referrer to clients 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<ByteString, JobQueue>& scope_to_job_queue_map()
{
static HashMap<ByteString, JobQueue> map;
return map;
}
static void register_(JS::VM& vm, JS::NonnullGCPtr<Job> 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::InternalError>(JS::ErrorType::NotImplemented, "Service Worker registration"sv).value());
}
}
static void update(JS::VM& vm, JS::NonnullGCPtr<Job> 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::InternalError>(JS::ErrorType::NotImplemented, "Service Worker update"sv).value());
}
}
static void unregister(JS::VM& vm, JS::NonnullGCPtr<Job> 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::InternalError>(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 jobs job type is register, run Register with job in parallel.
register_(vm, job);
break;
case Job::Type::Update:
// 3. If jobs job type is update, run Update with job in parallel.
update(vm, job);
break;
case Job::Type::Unregister:
// 4. If jobs 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> job)
{
// 1. Let jobQueue be null.
// Note: See below for how we ensure job queue
// 2. Let jobScope be jobs 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 jobs 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 lastJobs job promise has not settled, append job to lastJobs 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<JS::Promise>(*job->job_promise->promise()).is_handled()) {
last_job->list_of_equivalent_jobs.append(job);
}
// 3. Else, set jobs containing job queue to jobQueue, and enqueue job to jobQueue.
else {
job->containing_job_queue = &job_queue;
job_queue.append(job);
}
}
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#include <LibURL/URL.h>
#include <LibWeb/Bindings/ServiceWorkerRegistrationPrototype.h>
#include <LibWeb/Bindings/WorkerPrototype.h>
#include <LibWeb/StorageAPI/StorageKey.h>
namespace Web::ServiceWorker {
struct Job;
using JobQueue = JS::MarkedVector<JS::NonnullGCPtr<Job>>;
// 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<Job> create(JS::VM&, Type, StorageAPI::StorageKey, URL::URL scope_url, URL::URL script_url, JS::GCPtr<WebIDL::Promise>, JS::GCPtr<HTML::EnvironmentSettingsObject> 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<HTML::EnvironmentSettingsObject> client = nullptr;
Optional<URL::URL> referrer;
// FIXME: Spec just references this as an ECMAScript promise https://github.com/w3c/ServiceWorker/issues/1731
JS::GCPtr<WebIDL::Promise> job_promise = nullptr;
RawPtr<JobQueue> containing_job_queue = nullptr;
Vector<JS::NonnullGCPtr<Job>> 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<WebIDL::Promise>, JS::GCPtr<HTML::EnvironmentSettingsObject> client);
};
// https://w3c.github.io/ServiceWorker/#schedule-job
void schedule_job(JS::VM&, JS::NonnullGCPtr<Job>);
}