ladybird/Libraries/LibWeb/ServiceWorker/Job.cpp
Shannon Booth f87041bf3a LibGC+Everywhere: Factor out a LibGC from LibJS
Resulting in a massive rename across almost everywhere! Alongside the
namespace change, we now have the following names:

 * JS::NonnullGCPtr -> GC::Ref
 * JS::GCPtr -> GC::Ptr
 * JS::HeapFunction -> GC::Function
 * JS::CellImpl -> GC::Cell
 * JS::Handle -> GC::Root
2024-11-15 14:49:20 +01:00

605 lines
31 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/Heap.h>
#include <LibJS/Runtime/VM.h>
#include <LibURL/URL.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
#include <LibWeb/Fetch/Response.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/Fetching.h>
#include <LibWeb/HTML/Scripting/ModuleMap.h>
#include <LibWeb/HTML/Scripting/ModuleScript.h>
#include <LibWeb/HTML/Scripting/Script.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&, GC::Ref<Job>);
static void resolve_job_promise(GC::Ref<Job>, Optional<Registration const&>, JS::Value = JS::js_null());
template<typename Error>
static void reject_job_promise(GC::Ref<Job>, String message);
static void register_(JS::VM&, GC::Ref<Job>);
static void update(JS::VM&, GC::Ref<Job>);
static void unregister(JS::VM&, GC::Ref<Job>);
GC_DEFINE_ALLOCATOR(Job);
// https://w3c.github.io/ServiceWorker/#create-job-algorithm
GC::Ref<Job> Job::create(JS::VM& vm, Job::Type type, StorageAPI::StorageKey storage_key, URL::URL scope_url, URL::URL script_url, GC::Ptr<WebIDL::Promise> promise, GC::Ptr<HTML::EnvironmentSettingsObject> client)
{
return vm.heap().allocate<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, GC::Ptr<WebIDL::Promise> promise, GC::Ptr<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;
}
// https://w3c.github.io/ServiceWorker/#register-algorithm
static void register_(JS::VM& vm, GC::Ref<Job> job)
{
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 jobs 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"_string);
// 2. Invoke Finish Job with job and abort these steps.
finish_job(vm, job);
return;
}
// 2. If jobs script url's origin and jobs 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"_string);
// 2. Invoke Finish Job with job and abort these steps.
finish_job(vm, job);
return;
}
// 3. If jobs scope url's origin and jobs 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"_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 jobs storage key and jobs 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, jobs script url equals newestWorkers script url,
// jobs worker type equals newestWorkers type, and jobs update via cache mode's value equals registrations 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 jobs storage key, jobs scope url, and jobs 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);
}
// Used to share internal Update algorithm state b/w fetch callbacks
class UpdateAlgorithmState : JS::Cell {
GC_CELL(UpdateAlgorithmState, JS::Cell);
public:
static GC::Ref<UpdateAlgorithmState> create(JS::VM& vm)
{
return vm.heap().allocate<UpdateAlgorithmState>();
}
OrderedHashMap<URL::URL, GC::Ref<Fetch::Infrastructure::Response>>& updated_resource_map() { return m_map; }
bool has_updated_resources() const { return m_has_updated_resources; }
void set_has_updated_resources(bool b) { m_has_updated_resources = b; }
private:
UpdateAlgorithmState() = default;
virtual void visit_edges(JS::Cell::Visitor& visitor) override
{
Base::visit_edges(visitor);
visitor.visit(m_map);
}
OrderedHashMap<URL::URL, GC::Ref<Fetch::Infrastructure::Response>> m_map;
bool m_has_updated_resources { false };
};
// https://w3c.github.io/ServiceWorker/#update
static void update(JS::VM& vm, GC::Ref<Job> job)
{
// 1. Let registration be the result of running Get Registration given jobs storage key and jobs scope url.
auto registration = Registration::get(job->storage_key, job->scope_url);
// 2. If registration is null, then:
if (!registration.has_value()) {
// 1. Invoke Reject Job Promise with job and TypeError.
reject_job_promise<JS::TypeError>(job, "Service Worker registration not found on update"_string);
// 2. Invoke Finish Job with job and abort these steps.
finish_job(vm, job);
return;
}
// 3. Let newestWorker be the result of running Get Newest Worker algorithm passing registration as the argument.
auto* newest_worker = registration->newest_worker();
// 4. If jobs job type is update, and newestWorker is not null and its script url does not equal jobs script url, then:
if (job->job_type == Job::Type::Update && newest_worker != nullptr && newest_worker->script_url != job->script_url) {
// 1. Invoke Reject Job Promise with job and TypeError.
reject_job_promise<JS::TypeError>(job, "Service Worker script URL mismatch on update"_string);
// 2. Invoke Finish Job with job and abort these steps.
finish_job(vm, job);
return;
}
// 5. Let hasUpdatedResources be false.
// 6. Let updatedResourceMap be an ordered map where the keys are URLs and the values are responses.
auto state = UpdateAlgorithmState::create(vm);
// Fetch time, with a few caveats:
// - The spec says to use the 'to be created environment settings object for this service worker'
// - Soft-Update has no client
// To perform the fetch hook given request, run the following steps:
auto perform_the_fetch_hook_function = [&registration = *registration, job, newest_worker, state](GC::Ref<Fetch::Infrastructure::Request> request, HTML::TopLevelModule top_level, Fetch::Infrastructure::FetchAlgorithms::ProcessResponseConsumeBodyFunction process_custom_fetch_response) -> WebIDL::ExceptionOr<void> {
// FIXME: Soft-Update has no client
auto& realm = job->client->realm();
auto& vm = realm.vm();
// 1. Append `Service-Worker`/`script` to requests header list.
// Note: See https://w3c.github.io/ServiceWorker/#service-worker
request->header_list()->append(Fetch::Infrastructure::Header::from_string_pair("Service-Worker"sv, "script"sv));
// 2. Set requests cache mode to "no-cache" if any of the following are true:
// - registrations update via cache mode is not "all".
// - jobs force bypass cache flag is set.
// - newestWorker is not null and registration is stale.
if (registration.update_via_cache() != Bindings::ServiceWorkerUpdateViaCache::All
|| job->force_cache_bypass
|| (newest_worker != nullptr && registration.is_stale())) {
request->set_cache_mode(Fetch::Infrastructure::Request::CacheMode::NoCache);
}
// 3. Set requests service-workers mode to "none".
request->set_service_workers_mode(Fetch::Infrastructure::Request::ServiceWorkersMode::None);
Web::Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
fetch_algorithms_input.process_response_consume_body = move(process_custom_fetch_response);
// 4. If the isTopLevel flag is unset, then return the result of fetching request.
// FIXME: Needs spec issue, this wording is confusing and contradicts the way perform the fetch hook is used in `run a worker`
if (top_level == HTML::TopLevelModule::No) {
TRY(Web::Fetch::Fetching::fetch(realm, request, Web::Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input))));
return {};
}
// 5. Set request's redirect mode to "error".
request->set_redirect_mode(Fetch::Infrastructure::Request::RedirectMode::Error);
// 6. Fetch request, and asynchronously wait to run the remaining steps as part of fetchs processResponse for the response response.
// Note: The rest of the steps are in the processCustomFetchResponse algorithm
// FIXME: Needs spec issue to mention the existence of processCustomFetchResponse, same as step 4
// FIXME: Is there a better way to 'wait' for the fetch's processResponse to complete?
// Is this actually what the spec wants us to do?
IGNORE_USE_IN_ESCAPING_LAMBDA auto process_response_completion_result = Optional<WebIDL::ExceptionOr<void>> {};
fetch_algorithms_input.process_response = [request, job, state, newest_worker, &realm, &registration, &process_response_completion_result](GC::Ref<Fetch::Infrastructure::Response> response) mutable -> void {
// 7. Extract a MIME type from the responses header list. If s MIME type (ignoring parameters) is not a JavaScript MIME type, then:
auto mime_type = response->header_list()->extract_mime_type();
if (!mime_type.has_value() || !mime_type->is_javascript()) {
// 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
reject_job_promise<WebIDL::SecurityError>(job, "Service Worker script response is not a JavaScript MIME type"_string);
// 2. Asynchronously complete these steps with a network error.
process_response_completion_result = WebIDL::NetworkError::create(realm, "Service Worker script response is not a JavaScript MIME type"_string);
return;
}
// 8. Let serviceWorkerAllowed be the result of extracting header list values given `Service-Worker-Allowed` and responses header list.
// Note: See the definition of the Service-Worker-Allowed header in Appendix B: Extended HTTP headers. https://w3c.github.io/ServiceWorker/#service-worker-allowed
auto service_worker_allowed = Fetch::Infrastructure::extract_header_list_values("Service-Worker-Allowed"sv.bytes(), response->header_list());
// 9. Set policyContainer to the result of creating a policy container from a fetch response given response.
// FIXME: CSP not implemented yet
// 10. If serviceWorkerAllowed is failure, then:
if (service_worker_allowed.has<Fetch::Infrastructure::ExtractHeaderParseFailure>()) {
// FIXME: Should we reject the job promise with a security error here?
// 1. Asynchronously complete these steps with a network error.
process_response_completion_result = WebIDL::NetworkError::create(realm, "Failed to extract Service-Worker-Allowed header from fetch response"_string);
return;
}
// 11. Let scopeURL be registrations scope url.
auto const& scope_url = registration.scope_url();
// 12. Let maxScopeString be null.
auto max_scope_string = Optional<ByteString> {};
auto join_paths_with_slash = [](URL::URL const& url) -> ByteString {
StringBuilder builder;
builder.append('/');
for (auto const& component : url.paths()) {
builder.append(component);
builder.append('/');
}
return builder.to_byte_string();
};
// 13. If serviceWorkerAllowed is null, then:
if (service_worker_allowed.has<Empty>()) {
// 1. Let resolvedScope be the result of parsing "./" using jobs script url as the base URL.
auto resolved_scope = DOMURL::parse("./"sv, job->script_url);
// 2. Set maxScopeString to "/", followed by the strings in resolvedScopes path (including empty strings), separated from each other by "/".
max_scope_string = join_paths_with_slash(resolved_scope);
}
// 14. Else:
else {
// 1. Let maxScope be the result of parsing serviceWorkerAllowed using jobs script url as the base URL.
auto max_scope = DOMURL::parse(service_worker_allowed.get<Vector<ByteBuffer>>()[0], job->script_url);
// 2. If maxScopes origin is jobs script url's origin, then:
if (max_scope.origin().is_same_origin(job->script_url.origin())) {
// 1. Set maxScopeString to "/", followed by the strings in maxScopes path (including empty strings), separated from each other by "/".
max_scope_string = join_paths_with_slash(max_scope);
}
}
// 15. Let scopeString be "/", followed by the strings in scopeURLs path (including empty strings), separated from each other by "/".
auto scope_string = join_paths_with_slash(scope_url);
// 16. If maxScopeString is null or scopeString does not start with maxScopeString, then:
if (!max_scope_string.has_value() || !scope_string.starts_with(max_scope_string.value())) {
// 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
reject_job_promise<WebIDL::SecurityError>(job, "Service Worker script scope does not match Service-Worker-Allowed header"_string);
// 2. Asynchronously complete these steps with a network error.
process_response_completion_result = WebIDL::NetworkError::create(realm, "Service Worker script scope does not match Service-Worker-Allowed header"_string);
return;
}
// 17. Let url be requests url.
auto& url = request->url();
// 18. Set updatedResourceMap[url] to response.
state->updated_resource_map().set(url, response);
// 19. If responses cache state is not "local", set registrations last update check time to the current time.
if (response->cache_state() != Fetch::Infrastructure::Response::CacheState::Local)
registration.set_last_update_check_time(MonotonicTime::now());
// 20. Set hasUpdatedResources to true if any of the following are true:
// - newestWorker is null.
// - newestWorkers script url is not url or newestWorkers type is not jobs worker type.
// - FIXME: newestWorkers script resource map[url]'s body is not byte-for-byte identical with responses body.
if (newest_worker == nullptr
|| newest_worker->script_url != url
|| newest_worker->worker_type != job->worker_type) {
state->set_has_updated_resources(true);
}
// FIXME: 21. If hasUpdatedResources is false and newestWorkers classic scripts imported flag is set, then:
// 22. Asynchronously complete these steps with response.
process_response_completion_result = WebIDL::ExceptionOr<void> {};
};
auto fetch_controller = TRY(Web::Fetch::Fetching::fetch(realm, request, Web::Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input))));
// FIXME: This feels.. uncomfortable but it should work to block the current task until the fetch has progressed past our processResponse hook or aborted
auto& event_loop = job->client ? job->client->responsible_event_loop() : HTML::main_thread_event_loop();
event_loop.spin_until(GC::create_function(realm.heap(), [fetch_controller, &realm, &process_response_completion_result]() -> bool {
if (process_response_completion_result.has_value())
return true;
if (fetch_controller->state() == Fetch::Infrastructure::FetchController::State::Terminated || fetch_controller->state() == Fetch::Infrastructure::FetchController::State::Aborted) {
process_response_completion_result = WebIDL::AbortError::create(realm, "Service Worker fetch was terminated or aborted"_string);
return true;
}
return false;
}));
return process_response_completion_result.release_value();
};
auto perform_the_fetch_hook = HTML::create_perform_the_fetch_hook(vm.heap(), move(perform_the_fetch_hook_function));
// When the algorithm asynchronously completes, continue the rest of these steps, with script being the asynchronous completion value.
auto on_fetch_complete = HTML::create_on_fetch_script_complete(vm.heap(), [job, newest_worker, state, &registration = *registration, &vm](GC::Ptr<HTML::Script> script) -> void {
// If script is null or Is Async Module with scripts record, scripts base URL, and « » is true, then:
// FIXME: Reject async modules
if (!script) {
// 1. Invoke Reject Job Promise with job and TypeError.
reject_job_promise<JS::TypeError>(job, "Service Worker script is not a valid module"_string);
// 2. If newestWorker is null, then remove registration map[(registrations storage key, serialized scopeURL)].
if (newest_worker == nullptr)
Registration::remove(registration.storage_key(), registration.scope_url());
// 3. Invoke Finish Job with job and abort these steps.
finish_job(vm, job);
return;
}
// FIXME: Actually create service worker
// 10. Let worker be a new service worker.
// 11. Set workers script url to jobs script url, workers script resource to script, workers type to jobs worker type, and workers script resource map to updatedResourceMap.
(void)state;
// 12. Append url to workers set of used scripts.
// 13. Set workers script resources policy container to policyContainer.
// 14. Let forceBypassCache be true if jobs force bypass cache flag is set, and false otherwise.
// 15. Let runResult be the result of running the Run Service Worker algorithm with worker and forceBypassCache.
// 16. If runResult is failure or an abrupt completion, then:
// 17. Else, invoke Install algorithm with job, worker, and registration as its arguments.
if (job->client) {
auto& realm = job->client->realm();
auto context = HTML::TemporaryExecutionContext(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
WebIDL::reject_promise(realm, *job->job_promise, *vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Run Service Worker"sv).value());
finish_job(vm, job);
}
});
// 7. Switching on jobs worker type, run these substeps with the following options:
switch (job->worker_type) {
case Bindings::WorkerType::Classic:
// 1. Fetch a classic worker script given jobs serialized script url, jobs client, "serviceworker", and the to-be-created environment settings object for this service worker.
// FIXME: Credentials mode
// FIXME: Use a 'stub' service worker ESO as the fetch "environment"
(void)HTML::fetch_classic_worker_script(job->script_url, *job->client, Fetch::Infrastructure::Request::Destination::ServiceWorker, *job->client, perform_the_fetch_hook, on_fetch_complete);
break;
case Bindings::WorkerType::Module:
// 2. Fetch a module worker script graph given jobs serialized script url, jobs client, "serviceworker", "omit", and the to-be-created environment settings object for this service worker.
// FIXME: Credentials mode
// FIXME: Use a 'stub' service worker ESO as the fetch "environment"
(void)HTML::fetch_module_worker_script_graph(job->script_url, *job->client, Fetch::Infrastructure::Request::Destination::ServiceWorker, *job->client, perform_the_fetch_hook, on_fetch_complete);
}
}
static void unregister(JS::VM& vm, GC::Ref<Job> job)
{
// If there's no client, there won't be any promises to resolve
if (job->client) {
auto& realm = job->client->realm();
auto context = HTML::TemporaryExecutionContext(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
WebIDL::reject_promise(realm, *job->job_promise, *vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Service Worker unregistration"sv).value());
finish_job(vm, 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.
VERIFY(!job_queue.is_empty());
// 2. Queue a task to run these steps:
auto job_run_steps = GC::create_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/#finish-job-algorithm
static void finish_job(JS::VM& vm, GC::Ref<Job> job)
{
// 1. Let jobQueue be jobs 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(GC::Ref<Job> job, Optional<Registration const&>, JS::Value value)
{
// 1. If jobs client is not null, queue a task, on jobs 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, GC::create_function(realm.heap(), [&realm, job, value] {
HTML::TemporaryExecutionContext const context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// FIXME: Resolve to a ServiceWorkerRegistration platform object
// 1. Let convertedValue be null.
// 2. If jobs job type is either register or update, set convertedValue to the result of
// getting the service worker registration object that represents value in jobs client.
// 3. Else, set convertedValue to value, in jobs client's Realm.
// 4. Resolve jobs job promise with convertedValue.
WebIDL::resolve_promise(realm, *job->job_promise, value);
}));
}
// 2. For each equivalentJob in jobs list of equivalent jobs:
for (auto& equivalent_job : job->list_of_equivalent_jobs) {
// 1. If equivalentJobs client is null, continue.
if (!equivalent_job->client)
continue;
// 2. Queue a task, on equivalentJobs 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, GC::create_function(realm.heap(), [&realm, equivalent_job, value] {
HTML::TemporaryExecutionContext const context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// FIXME: Resolve to a ServiceWorkerRegistration platform object
// 1. Let convertedValue be null.
// 2. If equivalentJobs job type is either register or update, set convertedValue to the result of
// getting the service worker registration object that represents value in equivalentJobs client.
// 3. Else, set convertedValue to value, in equivalentJobs client's Realm.
// 4. Resolve equivalentJobs 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(GC::Ref<Job> job, String message)
{
// 1. If jobs client is not null, queue a task, on jobs client's responsible event loop using the DOM manipulation task source,
// to reject jobs job promise with a new exception with errorData and a user agent-defined message, in jobs 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, GC::create_function(realm.heap(), [&realm, job, message] {
HTML::TemporaryExecutionContext const context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
WebIDL::reject_promise(realm, *job->job_promise, Error::create(realm, message));
}));
}
// 2. For each equivalentJob in jobs list of equivalent jobs:
for (auto& equivalent_job : job->list_of_equivalent_jobs) {
// 1. If equivalentJobs client is null, continue.
if (!equivalent_job->client)
continue;
// 2. Queue a task, on equivalentJobs client's responsible event loop using the DOM manipulation task source,
// to reject equivalentJobs job promise with a new exception with errorData and a user agent-defined message,
// in equivalentJobs client's Realm.
auto& realm = equivalent_job->client->realm();
HTML::queue_a_task(HTML::Task::Source::DOMManipulation, equivalent_job->client->responsible_event_loop(), nullptr, GC::create_function(realm.heap(), [&realm, equivalent_job, message] {
HTML::TemporaryExecutionContext const context(realm, 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, GC::Ref<Job> job)
{
// 1. Let jobQueue be null.
// Note: See below for how we ensure job queue
// 2. Let jobScope be jobs 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.
// 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);
}
}
}
}