mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-25 00:50:22 +00:00
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:
parent
4a43d0ac98
commit
29416befe6
Notes:
github-actions[bot]
2024-10-04 05:09:03 +00:00
Author: https://github.com/ADKaster Commit: https://github.com/LadybirdBrowser/ladybird/commit/29416befe64 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1622
8 changed files with 281 additions and 20 deletions
|
@ -333,6 +333,7 @@ shared_library("LibWeb") {
|
|||
"SVG",
|
||||
"SecureContexts",
|
||||
"Selection",
|
||||
"ServiceWorker",
|
||||
"StorageAPI",
|
||||
"Streams",
|
||||
"UIEvents",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
source_set("ServiceWorker") {
|
||||
configs += [ "//Userland/Libraries/LibWeb:configs" ]
|
||||
deps = [ "//Userland/Libraries/LibWeb:all_generated" ]
|
||||
sources = [ "Job.cpp" ]
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 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::InternalError>(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
|
||||
|
|
165
Userland/Libraries/LibWeb/ServiceWorker/Job.cpp
Normal file
165
Userland/Libraries/LibWeb/ServiceWorker/Job.cpp
Normal 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 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<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 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> 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<JS::Promise>(*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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
79
Userland/Libraries/LibWeb/ServiceWorker/Job.h
Normal file
79
Userland/Libraries/LibWeb/ServiceWorker/Job.h
Normal 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>);
|
||||
|
||||
}
|
Loading…
Reference in a new issue