Skip to content

Commit

Permalink
LibWeb: Implement most of Service Worker registration
Browse files Browse the repository at this point in the history
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.

(cherry picked from commit 7faebb2702a2a19fc401fde1856a0d90861d2904)
  • Loading branch information
ADKaster authored and nico committed Nov 22, 2024
1 parent 00593c6 commit 9448fe1
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -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",
]
}
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions Userland/Libraries/LibWeb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,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
Expand Down
178 changes: 169 additions & 9 deletions Userland/Libraries/LibWeb/ServiceWorker/Job.cpp
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@ladybird.org>
* Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/VM.h>
#include <LibURL/URL.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/SecureContexts/AbstractOperations.h>
#include <LibWeb/ServiceWorker/Job.h>
#include <LibWeb/ServiceWorker/Registration.h>
#include <LibWeb/WebIDL/Promise.h>

namespace Web::ServiceWorker {

static void run_job(JS::VM&, JobQueue&);
static void finish_job(JS::VM&, JS::NonnullGCPtr<Job>);
static void resolve_job_promise(JS::NonnullGCPtr<Job>, Optional<Registration const&>, JS::Value = JS::js_null());
template<typename Error>
static void reject_job_promise(JS::NonnullGCPtr<Job>, FlyString message);
static void register_(JS::VM&, JS::NonnullGCPtr<Job>);
static void update(JS::VM&, JS::NonnullGCPtr<Job>);
static void unregister(JS::VM&, JS::NonnullGCPtr<Job>);

JS_DEFINE_ALLOCATOR(Job);

// https://w3c.github.io/ServiceWorker/#create-job
// https://w3c.github.io/ServiceWorker/#create-job-algorithm
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);
Expand Down Expand Up @@ -54,14 +66,73 @@ static HashMap<ByteString, JobQueue>& scope_to_job_queue_map()
return map;
}

// https://w3c.github.io/ServiceWorker/#register-algorithm
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());
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<WebIDL::SecurityError>(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<WebIDL::SecurityError>(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<WebIDL::SecurityError>(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> job)
Expand All @@ -84,7 +155,7 @@ static void unregister(JS::VM& vm, JS::NonnullGCPtr<Job> 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.
Expand Down Expand Up @@ -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> 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> job, Optional<Registration const&>, 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<typename Error>
static void reject_job_promise(JS::NonnullGCPtr<Job> 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> 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.
Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/ServiceWorker/Job.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@ladybird.org>
* Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down Expand Up @@ -73,7 +73,7 @@ struct Job : public JS::Cell {
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
// https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
void schedule_job(JS::VM&, JS::NonnullGCPtr<Job>);

}
98 changes: 98 additions & 0 deletions Userland/Libraries/LibWeb/ServiceWorker/Registration.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/HashMap.h>
#include <LibWeb/ServiceWorker/Registration.h>

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<RegistrationKey, Registration> 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 &registration; }).value_or(nullptr) != this;
}

// https://w3c.github.io/ServiceWorker/#get-registration-algorithm
Optional<Registration&> Registration::get(StorageAPI::StorageKey const& key, Optional<URL::URL> 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<Web::ServiceWorker::RegistrationKey> : public DefaultTraits<Web::ServiceWorker::RegistrationKey> {
static unsigned hash(Web::ServiceWorker::RegistrationKey const& key)
{
return pair_int_hash(Traits<Web::StorageAPI::StorageKey>::hash(key.key), Traits<ByteString>::hash(key.serialized_scope_url));
}
};
}
Loading

0 comments on commit 9448fe1

Please sign in to comment.