Coverage Report

Created: 2026-05-16 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/ServiceWorker/Job.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <LibJS/Heap/Heap.h>
8
#include <LibJS/Runtime/VM.h>
9
#include <LibURL/URL.h>
10
#include <LibWeb/DOMURL/DOMURL.h>
11
#include <LibWeb/Fetch/Fetching/Fetching.h>
12
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
13
#include <LibWeb/Fetch/Response.h>
14
#include <LibWeb/HTML/Scripting/ClassicScript.h>
15
#include <LibWeb/HTML/Scripting/Environments.h>
16
#include <LibWeb/HTML/Scripting/Fetching.h>
17
#include <LibWeb/HTML/Scripting/ModuleMap.h>
18
#include <LibWeb/HTML/Scripting/ModuleScript.h>
19
#include <LibWeb/HTML/Scripting/Script.h>
20
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
21
#include <LibWeb/SecureContexts/AbstractOperations.h>
22
#include <LibWeb/ServiceWorker/Job.h>
23
#include <LibWeb/ServiceWorker/Registration.h>
24
#include <LibWeb/WebIDL/Promise.h>
25
26
namespace Web::ServiceWorker {
27
28
static void run_job(JS::VM&, JobQueue&);
29
static void finish_job(JS::VM&, JS::NonnullGCPtr<Job>);
30
static void resolve_job_promise(JS::NonnullGCPtr<Job>, Optional<Registration const&>, JS::Value = JS::js_null());
31
template<typename Error>
32
static void reject_job_promise(JS::NonnullGCPtr<Job>, String message);
33
static void register_(JS::VM&, JS::NonnullGCPtr<Job>);
34
static void update(JS::VM&, JS::NonnullGCPtr<Job>);
35
static void unregister(JS::VM&, JS::NonnullGCPtr<Job>);
36
37
JS_DEFINE_ALLOCATOR(Job);
38
39
// https://w3c.github.io/ServiceWorker/#create-job-algorithm
40
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)
41
0
{
42
0
    return vm.heap().allocate_without_realm<Job>(type, move(storage_key), move(scope_url), move(script_url), promise, client);
43
0
}
44
45
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)
46
0
    : job_type(type)
47
0
    , storage_key(move(storage_key))
48
0
    , scope_url(move(scope_url))
49
0
    , script_url(move(script_url))
50
0
    , client(client)
51
0
    , job_promise(promise)
52
0
{
53
    // 8. If client is not null, set job’s referrer to client’s creation URL.
54
0
    if (client)
55
0
        referrer = client->creation_url;
56
0
}
57
58
0
Job::~Job() = default;
59
60
void Job::visit_edges(JS::Cell::Visitor& visitor)
61
0
{
62
0
    Base::visit_edges(visitor);
63
64
0
    visitor.visit(client);
65
0
    visitor.visit(job_promise);
66
0
    for (auto& job : list_of_equivalent_jobs)
67
0
        visitor.visit(job);
68
0
}
69
70
// FIXME: Does this need to be a 'user agent' level thing? Or can we have one per renderer process?
71
// https://w3c.github.io/ServiceWorker/#dfn-scope-to-job-queue-map
72
static HashMap<ByteString, JobQueue>& scope_to_job_queue_map()
73
0
{
74
0
    static HashMap<ByteString, JobQueue> map;
75
0
    return map;
76
0
}
77
78
// https://w3c.github.io/ServiceWorker/#register-algorithm
79
static void register_(JS::VM& vm, JS::NonnullGCPtr<Job> job)
80
0
{
81
0
    auto script_origin = job->script_url.origin();
82
0
    auto scope_origin = job->scope_url.origin();
83
0
    auto referrer_origin = job->referrer->origin();
84
85
    // 1. If the result of running potentially trustworthy origin with the origin of job’s script url as the argument is Not Trusted, then:
86
0
    if (SecureContexts::Trustworthiness::NotTrustworthy == SecureContexts::is_origin_potentially_trustworthy(script_origin)) {
87
        // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
88
0
        reject_job_promise<WebIDL::SecurityError>(job, "Service Worker registration has untrustworthy script origin"_string);
89
90
        // 2. Invoke Finish Job with job and abort these steps.
91
0
        finish_job(vm, job);
92
0
        return;
93
0
    }
94
95
    // 2. If job’s script url's origin and job’s referrer's origin are not same origin, then:
96
0
    if (!script_origin.is_same_origin(referrer_origin)) {
97
        // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
98
0
        reject_job_promise<WebIDL::SecurityError>(job, "Service Worker registration has incompatible script and referrer origins"_string);
99
100
        // 2. Invoke Finish Job with job and abort these steps.
101
0
        finish_job(vm, job);
102
0
        return;
103
0
    }
104
105
    // 3. If job’s scope url's origin and job’s referrer's origin are not same origin, then:
106
0
    if (!scope_origin.is_same_origin(referrer_origin)) {
107
        // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
108
0
        reject_job_promise<WebIDL::SecurityError>(job, "Service Worker registration has incompatible scope and referrer origins"_string);
109
110
        // 2. Invoke Finish Job with job and abort these steps.
111
0
        finish_job(vm, job);
112
0
        return;
113
0
    }
114
115
    // 4. Let registration be the result of running Get Registration given job’s storage key and job’s scope url.
116
0
    auto registration = Registration::get(job->storage_key, job->scope_url);
117
118
    // 5. If registration is not null, then:
119
0
    if (registration.has_value()) {
120
        // 1. Let newestWorker be the result of running the Get Newest Worker algorithm passing registration as the argument.
121
0
        auto* newest_worker = registration->newest_worker();
122
123
        // 2. If newestWorker is not null, job’s script url equals newestWorker’s script url,
124
        //    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:
125
0
        if (newest_worker != nullptr
126
0
            && job->script_url == newest_worker->script_url
127
0
            && job->worker_type == newest_worker->worker_type
128
0
            && job->update_via_cache == registration->update_via_cache()) {
129
            // 1. Invoke Resolve Job Promise with job and registration.
130
0
            resolve_job_promise(job, registration.value());
131
132
            // 2. Invoke Finish Job with job and abort these steps.
133
0
            finish_job(vm, job);
134
0
            return;
135
0
        }
136
0
    }
137
    // 6. Else:
138
0
    else {
139
        // 1. Invoke Set Registration algorithm with job’s storage key, job’s scope url, and job’s update via cache mode.
140
0
        Registration::set(job->storage_key, job->scope_url, job->update_via_cache);
141
0
    }
142
143
    // Invoke Update algorithm passing job as the argument.
144
0
    update(vm, job);
145
0
}
146
147
// Used to share internal Update algorithm state b/w fetch callbacks
148
class UpdateAlgorithmState : JS::Cell {
149
    JS_CELL(UpdateAlgorithmState, JS::Cell);
150
151
public:
152
    static JS::NonnullGCPtr<UpdateAlgorithmState> create(JS::VM& vm)
153
0
    {
154
0
        return vm.heap().allocate_without_realm<UpdateAlgorithmState>();
155
0
    }
156
157
0
    OrderedHashMap<URL::URL, JS::NonnullGCPtr<Fetch::Infrastructure::Response>>& updated_resource_map() { return m_map; }
158
0
    bool has_updated_resources() const { return m_has_updated_resources; }
159
0
    void set_has_updated_resources(bool b) { m_has_updated_resources = b; }
160
161
private:
162
0
    UpdateAlgorithmState() = default;
163
164
    virtual void visit_edges(JS::Cell::Visitor& visitor) override
165
0
    {
166
0
        Base::visit_edges(visitor);
167
0
        visitor.visit(m_map);
168
0
    }
169
170
    OrderedHashMap<URL::URL, JS::NonnullGCPtr<Fetch::Infrastructure::Response>> m_map;
171
    bool m_has_updated_resources { false };
172
};
173
174
// https://w3c.github.io/ServiceWorker/#update
175
static void update(JS::VM& vm, JS::NonnullGCPtr<Job> job)
176
0
{
177
    // 1. Let registration be the result of running Get Registration given job’s storage key and job’s scope url.
178
0
    auto registration = Registration::get(job->storage_key, job->scope_url);
179
180
    // 2. If registration is null, then:
181
0
    if (!registration.has_value()) {
182
        // 1. Invoke Reject Job Promise with job and TypeError.
183
0
        reject_job_promise<JS::TypeError>(job, "Service Worker registration not found on update"_string);
184
185
        // 2. Invoke Finish Job with job and abort these steps.
186
0
        finish_job(vm, job);
187
0
        return;
188
0
    }
189
190
    // 3. Let newestWorker be the result of running Get Newest Worker algorithm passing registration as the argument.
191
0
    auto* newest_worker = registration->newest_worker();
192
193
    // 4. If job’s job type is update, and newestWorker is not null and its script url does not equal job’s script url, then:
194
0
    if (job->job_type == Job::Type::Update && newest_worker != nullptr && newest_worker->script_url != job->script_url) {
195
        // 1. Invoke Reject Job Promise with job and TypeError.
196
0
        reject_job_promise<JS::TypeError>(job, "Service Worker script URL mismatch on update"_string);
197
198
        // 2. Invoke Finish Job with job and abort these steps.
199
0
        finish_job(vm, job);
200
0
        return;
201
0
    }
202
203
    // 5. Let hasUpdatedResources be false.
204
    // 6. Let updatedResourceMap be an ordered map where the keys are URLs and the values are responses.
205
0
    auto state = UpdateAlgorithmState::create(vm);
206
207
    // Fetch time, with a few caveats:
208
    // - The spec says to use the 'to be created environment settings object for this service worker'
209
    // - Soft-Update has no client
210
211
    // To perform the fetch hook given request, run the following steps:
212
0
    auto perform_the_fetch_hook_function = [&registration = *registration, job, newest_worker, state](JS::NonnullGCPtr<Fetch::Infrastructure::Request> request, HTML::TopLevelModule top_level, Fetch::Infrastructure::FetchAlgorithms::ProcessResponseConsumeBodyFunction process_custom_fetch_response) -> WebIDL::ExceptionOr<void> {
213
        // FIXME: Soft-Update has no client
214
0
        auto& realm = job->client->realm();
215
0
        auto& vm = realm.vm();
216
217
        // 1. Append `Service-Worker`/`script` to request’s header list.
218
        // Note: See https://w3c.github.io/ServiceWorker/#service-worker
219
0
        request->header_list()->append(Fetch::Infrastructure::Header::from_string_pair("Service-Worker"sv, "script"sv));
220
221
        // 2. Set request’s cache mode to "no-cache" if any of the following are true:
222
        //  - registration’s update via cache mode is not "all".
223
        //  - job’s force bypass cache flag is set.
224
        //  - newestWorker is not null and registration is stale.
225
0
        if (registration.update_via_cache() != Bindings::ServiceWorkerUpdateViaCache::All
226
0
            || job->force_cache_bypass
227
0
            || (newest_worker != nullptr && registration.is_stale())) {
228
0
            request->set_cache_mode(Fetch::Infrastructure::Request::CacheMode::NoCache);
229
0
        }
230
231
        // 3. Set request’s service-workers mode to "none".
232
0
        request->set_service_workers_mode(Fetch::Infrastructure::Request::ServiceWorkersMode::None);
233
234
0
        Web::Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
235
0
        fetch_algorithms_input.process_response_consume_body = move(process_custom_fetch_response);
236
237
        // 4. If the isTopLevel flag is unset, then return the result of fetching request.
238
        // FIXME: Needs spec issue, this wording is confusing and contradicts the way perform the fetch hook is used in `run a worker`
239
0
        if (top_level == HTML::TopLevelModule::No) {
240
0
            TRY(Web::Fetch::Fetching::fetch(realm, request, Web::Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input))));
241
0
            return {};
242
0
        }
243
244
        // 5. Set request's redirect mode to "error".
245
0
        request->set_redirect_mode(Fetch::Infrastructure::Request::RedirectMode::Error);
246
247
        // 6. Fetch request, and asynchronously wait to run the remaining steps as part of fetch’s processResponse for the response response.
248
        // Note: The rest of the steps are in the processCustomFetchResponse algorithm
249
        // FIXME: Needs spec issue to mention the existence of processCustomFetchResponse, same as step 4
250
251
        // FIXME: Is there a better way to 'wait' for the fetch's processResponse to complete?
252
        //        Is this actually what the spec wants us to do?
253
0
        IGNORE_USE_IN_ESCAPING_LAMBDA auto process_response_completion_result = Optional<WebIDL::ExceptionOr<void>> {};
254
255
0
        fetch_algorithms_input.process_response = [request, job, state, newest_worker, &realm, &registration, &process_response_completion_result](JS::NonnullGCPtr<Fetch::Infrastructure::Response> response) mutable -> void {
256
            // 7. Extract a MIME type from the response’s header list. If s MIME type (ignoring parameters) is not a JavaScript MIME type, then:
257
0
            auto mime_type = response->header_list()->extract_mime_type();
258
0
            if (!mime_type.has_value() || !mime_type->is_javascript()) {
259
                // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
260
0
                reject_job_promise<WebIDL::SecurityError>(job, "Service Worker script response is not a JavaScript MIME type"_string);
261
262
                // 2. Asynchronously complete these steps with a network error.
263
0
                process_response_completion_result = WebIDL::NetworkError::create(realm, "Service Worker script response is not a JavaScript MIME type"_string);
264
0
                return;
265
0
            }
266
267
            // 8. Let serviceWorkerAllowed be the result of extracting header list values given `Service-Worker-Allowed` and response’s header list.
268
            // Note: See the definition of the Service-Worker-Allowed header in Appendix B: Extended HTTP headers. https://w3c.github.io/ServiceWorker/#service-worker-allowed
269
0
            auto service_worker_allowed = Fetch::Infrastructure::extract_header_list_values("Service-Worker-Allowed"sv.bytes(), response->header_list());
270
271
            // 9. Set policyContainer to the result of creating a policy container from a fetch response given response.
272
            // FIXME: CSP not implemented yet
273
274
            // 10. If serviceWorkerAllowed is failure, then:
275
0
            if (service_worker_allowed.has<Fetch::Infrastructure::ExtractHeaderParseFailure>()) {
276
                // FIXME: Should we reject the job promise with a security error here?
277
278
                // 1. Asynchronously complete these steps with a network error.
279
0
                process_response_completion_result = WebIDL::NetworkError::create(realm, "Failed to extract Service-Worker-Allowed header from fetch response"_string);
280
0
                return;
281
0
            }
282
283
            // 11. Let scopeURL be registration’s scope url.
284
0
            auto const& scope_url = registration.scope_url();
285
286
            // 12. Let maxScopeString be null.
287
0
            auto max_scope_string = Optional<ByteString> {};
288
289
0
            auto join_paths_with_slash = [](URL::URL const& url) -> ByteString {
290
0
                StringBuilder builder;
291
0
                builder.append('/');
292
0
                for (auto const& component : url.paths()) {
293
0
                    builder.append(component);
294
0
                    builder.append('/');
295
0
                }
296
0
                return builder.to_byte_string();
297
0
            };
298
299
            // 13. If serviceWorkerAllowed is null, then:
300
0
            if (service_worker_allowed.has<Empty>()) {
301
                // 1. Let resolvedScope be the result of parsing "./" using job’s script url as the base URL.
302
0
                auto resolved_scope = DOMURL::parse("./"sv, job->script_url);
303
304
                // 2. Set maxScopeString to "/", followed by the strings in resolvedScope’s path (including empty strings), separated from each other by "/".
305
0
                max_scope_string = join_paths_with_slash(resolved_scope);
306
0
            }
307
            // 14. Else:
308
0
            else {
309
                // 1. Let maxScope be the result of parsing serviceWorkerAllowed using job’s script url as the base URL.
310
0
                auto max_scope = DOMURL::parse(service_worker_allowed.get<Vector<ByteBuffer>>()[0], job->script_url);
311
312
                // 2. If maxScope’s origin is job’s script url's origin, then:
313
0
                if (max_scope.origin().is_same_origin(job->script_url.origin())) {
314
                    // 1. Set maxScopeString to "/", followed by the strings in maxScope’s path (including empty strings), separated from each other by "/".
315
0
                    max_scope_string = join_paths_with_slash(max_scope);
316
0
                }
317
0
            }
318
319
            // 15. Let scopeString be "/", followed by the strings in scopeURL’s path (including empty strings), separated from each other by "/".
320
0
            auto scope_string = join_paths_with_slash(scope_url);
321
322
            // 16. If maxScopeString is null or scopeString does not start with maxScopeString, then:
323
0
            if (!max_scope_string.has_value() || !scope_string.starts_with(max_scope_string.value())) {
324
                // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
325
0
                reject_job_promise<WebIDL::SecurityError>(job, "Service Worker script scope does not match Service-Worker-Allowed header"_string);
326
327
                // 2. Asynchronously complete these steps with a network error.
328
0
                process_response_completion_result = WebIDL::NetworkError::create(realm, "Service Worker script scope does not match Service-Worker-Allowed header"_string);
329
0
                return;
330
0
            }
331
332
            // 17. Let url be request’s url.
333
0
            auto& url = request->url();
334
335
            // 18. Set updatedResourceMap[url] to response.
336
0
            state->updated_resource_map().set(url, response);
337
338
            // 19. If response’s cache state is not "local", set registration’s last update check time to the current time.
339
0
            if (response->cache_state() != Fetch::Infrastructure::Response::CacheState::Local)
340
0
                registration.set_last_update_check_time(MonotonicTime::now());
341
342
            // 20. Set hasUpdatedResources to true if any of the following are true:
343
            // - newestWorker is null.
344
            // - newestWorker’s script url is not url or newestWorker’s type is not job’s worker type.
345
            // - FIXME: newestWorker’s script resource map[url]'s body is not byte-for-byte identical with response’s body.
346
0
            if (newest_worker == nullptr
347
0
                || newest_worker->script_url != url
348
0
                || newest_worker->worker_type != job->worker_type) {
349
0
                state->set_has_updated_resources(true);
350
0
            }
351
352
            // FIXME: 21. If hasUpdatedResources is false and newestWorker’s classic scripts imported flag is set, then:
353
354
            // 22. Asynchronously complete these steps with response.
355
0
            process_response_completion_result = WebIDL::ExceptionOr<void> {};
356
0
        };
357
358
0
        auto fetch_controller = TRY(Web::Fetch::Fetching::fetch(realm, request, Web::Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input))));
359
360
        // FIXME: This feels.. uncomfortable but it should work to block the current task until the fetch has progressed past our processResponse hook or aborted
361
0
        auto& event_loop = job->client ? job->client->responsible_event_loop() : HTML::main_thread_event_loop();
362
0
        event_loop.spin_until([fetch_controller, &realm, &process_response_completion_result]() -> bool {
363
0
            if (process_response_completion_result.has_value())
364
0
                return true;
365
0
            if (fetch_controller->state() == Fetch::Infrastructure::FetchController::State::Terminated || fetch_controller->state() == Fetch::Infrastructure::FetchController::State::Aborted) {
366
0
                process_response_completion_result = WebIDL::AbortError::create(realm, "Service Worker fetch was terminated or aborted"_string);
367
0
                return true;
368
0
            }
369
0
            return false;
370
0
        });
371
372
0
        return process_response_completion_result.release_value();
373
0
    };
374
0
    auto perform_the_fetch_hook = HTML::create_perform_the_fetch_hook(vm.heap(), move(perform_the_fetch_hook_function));
375
376
    // When the algorithm asynchronously completes, continue the rest of these steps, with script being the asynchronous completion value.
377
0
    auto on_fetch_complete = HTML::create_on_fetch_script_complete(vm.heap(), [job, newest_worker, state, &registration = *registration, &vm](JS::GCPtr<HTML::Script> script) -> void {
378
        // If script is null or Is Async Module with script’s record, script’s base URL, and « » is true, then:
379
        // FIXME: Reject async modules
380
0
        if (!script) {
381
            // 1. Invoke Reject Job Promise with job and TypeError.
382
0
            reject_job_promise<JS::TypeError>(job, "Service Worker script is not a valid module"_string);
383
384
            // 2. If newestWorker is null, then remove registration map[(registration’s storage key, serialized scopeURL)].
385
0
            if (newest_worker == nullptr)
386
0
                Registration::remove(registration.storage_key(), registration.scope_url());
387
388
            // 3. Invoke Finish Job with job and abort these steps.
389
0
            finish_job(vm, job);
390
0
            return;
391
0
        }
392
393
        // FIXME: Actually create service worker
394
        // 10. Let worker be a new service worker.
395
        // 11. Set worker’s script url to job’s script url, worker’s script resource to script, worker’s type to job’s worker type, and worker’s script resource map to updatedResourceMap.
396
0
        (void)state;
397
        // 12. Append url to worker’s set of used scripts.
398
        // 13. Set worker’s script resource’s policy container to policyContainer.
399
        // 14. Let forceBypassCache be true if job’s force bypass cache flag is set, and false otherwise.
400
        // 15. Let runResult be the result of running the Run Service Worker algorithm with worker and forceBypassCache.
401
        // 16. If runResult is failure or an abrupt completion, then:
402
        // 17. Else, invoke Install algorithm with job, worker, and registration as its arguments.
403
0
        if (job->client) {
404
0
            auto context = HTML::TemporaryExecutionContext(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
405
0
            auto& realm = *vm.current_realm();
406
0
            WebIDL::reject_promise(realm, *job->job_promise, *vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Run Service Worker"sv).value());
407
0
            finish_job(vm, job);
408
0
        }
409
0
    });
410
411
    // 7. Switching on job’s worker type, run these substeps with the following options:
412
0
    switch (job->worker_type) {
413
0
    case Bindings::WorkerType::Classic:
414
        // 1. Fetch a classic worker script given job’s serialized script url, job’s client, "serviceworker", and the to-be-created environment settings object for this service worker.
415
        // FIXME: Credentials mode
416
        // FIXME: Use a 'stub' service worker ESO as the fetch "environment"
417
0
        (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);
418
0
        break;
419
0
    case Bindings::WorkerType::Module:
420
        // 2. Fetch a module worker script graph given job’s serialized script url, job’s client, "serviceworker", "omit", and the to-be-created environment settings object for this service worker.
421
        // FIXME: Credentials mode
422
        // FIXME: Use a 'stub' service worker ESO as the fetch "environment"
423
0
        (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);
424
0
    }
425
0
}
426
427
static void unregister(JS::VM& vm, JS::NonnullGCPtr<Job> job)
428
0
{
429
    // If there's no client, there won't be any promises to resolve
430
0
    if (job->client) {
431
0
        auto context = HTML::TemporaryExecutionContext(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
432
0
        auto& realm = *vm.current_realm();
433
0
        WebIDL::reject_promise(realm, *job->job_promise, *vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Service Worker unregistration"sv).value());
434
0
        finish_job(vm, job);
435
0
    }
436
0
}
437
438
// https://w3c.github.io/ServiceWorker/#run-job-algorithm
439
static void run_job(JS::VM& vm, JobQueue& job_queue)
440
0
{
441
    // 1. Assert: jobQueue is not empty.
442
0
    VERIFY(!job_queue.is_empty());
443
444
    // 2. Queue a task to run these steps:
445
0
    auto job_run_steps = JS::create_heap_function(vm.heap(), [&vm, &job_queue] {
446
        // 1. Let job be the first item in jobQueue.
447
0
        auto& job = job_queue.first();
448
449
        // FIXME: Do these really need to be in parallel to the HTML event loop? Sounds fishy
450
0
        switch (job->job_type) {
451
0
        case Job::Type::Register:
452
            // 2. If job’s job type is register, run Register with job in parallel.
453
0
            register_(vm, job);
454
0
            break;
455
0
        case Job::Type::Update:
456
            // 3. If job’s job type is update, run Update with job in parallel.
457
0
            update(vm, job);
458
0
            break;
459
0
        case Job::Type::Unregister:
460
            // 4. If job’s job type is unregister, run Unregister with job in parallel.
461
0
            unregister(vm, job);
462
0
            break;
463
0
        }
464
0
    });
465
466
    // FIXME: How does the user agent ensure this happens? Is this a normative note?
467
    // Spec-Note:
468
    // For a register job and an update job, the user agent delays queuing a task for running the job
469
    // until after a DOMContentLoaded event has been dispatched to the document that initiated the job.
470
471
    // FIXME: Spec should be updated to avoid 'queue a task' and use 'queue a global task' instead
472
    // FIXME: On which task source? On which event loop? On behalf of which document?
473
0
    HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, job_run_steps);
474
0
}
475
476
// https://w3c.github.io/ServiceWorker/#finish-job-algorithm
477
static void finish_job(JS::VM& vm, JS::NonnullGCPtr<Job> job)
478
0
{
479
    // 1. Let jobQueue be job’s containing job queue.
480
0
    auto& job_queue = *job->containing_job_queue;
481
482
    // 2. Assert: the first item in jobQueue is job.
483
0
    VERIFY(job_queue.first() == job);
484
485
    // 3. Dequeue from jobQueue
486
0
    (void)job_queue.take_first();
487
488
    // 4. If jobQueue is not empty, invoke Run Job with jobQueue.
489
0
    if (!job_queue.is_empty())
490
0
        run_job(vm, job_queue);
491
0
}
492
493
// https://w3c.github.io/ServiceWorker/#resolve-job-promise-algorithm
494
static void resolve_job_promise(JS::NonnullGCPtr<Job> job, Optional<Registration const&>, JS::Value value)
495
0
{
496
    // 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:
497
0
    if (job->client) {
498
0
        auto& realm = job->client->realm();
499
0
        HTML::queue_a_task(HTML::Task::Source::DOMManipulation, job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, job, value] {
500
0
            HTML::TemporaryExecutionContext const context(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
501
            // FIXME: Resolve to a ServiceWorkerRegistration platform object
502
            // 1. Let convertedValue be null.
503
            // 2. If job’s job type is either register or update, set convertedValue to the result of
504
            //    getting the service worker registration object that represents value in job’s client.
505
            // 3. Else, set convertedValue to value, in job’s client's Realm.
506
            // 4. Resolve job’s job promise with convertedValue.
507
0
            WebIDL::resolve_promise(realm, *job->job_promise, value);
508
0
        }));
509
0
    }
510
511
    // 2. For each equivalentJob in job’s list of equivalent jobs:
512
0
    for (auto& equivalent_job : job->list_of_equivalent_jobs) {
513
        // 1. If equivalentJob’s client is null, continue.
514
0
        if (!equivalent_job->client)
515
0
            continue;
516
517
        // 2. Queue a task, on equivalentJob’s client's responsible event loop using the DOM manipulation task source,
518
        //    to run the following substeps:
519
0
        auto& realm = equivalent_job->client->realm();
520
0
        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] {
521
0
            HTML::TemporaryExecutionContext const context(*equivalent_job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
522
            // FIXME: Resolve to a ServiceWorkerRegistration platform object
523
            // 1. Let convertedValue be null.
524
            // 2. If equivalentJob’s job type is either register or update, set convertedValue to the result of
525
            //    getting the service worker registration object that represents value in equivalentJob’s client.
526
            // 3. Else, set convertedValue to value, in equivalentJob’s client's Realm.
527
            // 4. Resolve equivalentJob’s job promise with convertedValue.
528
0
            WebIDL::resolve_promise(realm, *equivalent_job->job_promise, value);
529
0
        }));
530
0
    }
531
0
}
532
533
// https://w3c.github.io/ServiceWorker/#reject-job-promise-algorithm
534
template<typename Error>
535
static void reject_job_promise(JS::NonnullGCPtr<Job> job, String message)
536
0
{
537
    // 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,
538
    //    to reject job’s job promise with a new exception with errorData and a user agent-defined message, in job’s client's Realm.
539
0
    if (job->client) {
540
0
        auto& realm = job->client->realm();
541
0
        HTML::queue_a_task(HTML::Task::Source::DOMManipulation, job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, job, message] {
542
0
            HTML::TemporaryExecutionContext const context(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
543
0
            WebIDL::reject_promise(realm, *job->job_promise, Error::create(realm, message));
544
0
        }));
Unexecuted instantiation: Job.cpp:Web::ServiceWorker::reject_job_promise<Web::WebIDL::SecurityError>(JS::NonnullGCPtr<Web::ServiceWorker::Job>, AK::String)::{lambda()#1}::operator()() const
Unexecuted instantiation: Job.cpp:Web::ServiceWorker::reject_job_promise<JS::TypeError>(JS::NonnullGCPtr<Web::ServiceWorker::Job>, AK::String)::{lambda()#1}::operator()() const
545
0
    }
546
547
    // 2. For each equivalentJob in job’s list of equivalent jobs:
548
0
    for (auto& equivalent_job : job->list_of_equivalent_jobs) {
549
        // 1. If equivalentJob’s client is null, continue.
550
0
        if (!equivalent_job->client)
551
0
            continue;
552
553
        // 2. Queue a task, on equivalentJob’s client's responsible event loop using the DOM manipulation task source,
554
        //    to reject equivalentJob’s job promise with a new exception with errorData and a user agent-defined message,
555
        //    in equivalentJob’s client's Realm.
556
0
        auto& realm = equivalent_job->client->realm();
557
0
        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] {
558
0
            HTML::TemporaryExecutionContext const context(*equivalent_job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
559
0
            WebIDL::reject_promise(realm, *equivalent_job->job_promise, Error::create(realm, message));
560
0
        }));
Unexecuted instantiation: Job.cpp:Web::ServiceWorker::reject_job_promise<Web::WebIDL::SecurityError>(JS::NonnullGCPtr<Web::ServiceWorker::Job>, AK::String)::{lambda()#2}::operator()() const
Unexecuted instantiation: Job.cpp:Web::ServiceWorker::reject_job_promise<JS::TypeError>(JS::NonnullGCPtr<Web::ServiceWorker::Job>, AK::String)::{lambda()#2}::operator()() const
561
0
    }
562
0
}
Unexecuted instantiation: Job.cpp:void Web::ServiceWorker::reject_job_promise<Web::WebIDL::SecurityError>(JS::NonnullGCPtr<Web::ServiceWorker::Job>, AK::String)
Unexecuted instantiation: Job.cpp:void Web::ServiceWorker::reject_job_promise<JS::TypeError>(JS::NonnullGCPtr<Web::ServiceWorker::Job>, AK::String)
563
564
// https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
565
void schedule_job(JS::VM& vm, JS::NonnullGCPtr<Job> job)
566
0
{
567
    // 1. Let jobQueue be null.
568
    // Note: See below for how we ensure job queue
569
570
    // 2. Let jobScope be job’s scope url, serialized.
571
    // FIXME: Suspect that spec should specify to not use fragment here
572
0
    auto job_scope = job->scope_url.serialize();
573
574
    // 3. If scope to job queue map[jobScope] does not exist, set scope to job queue map[jobScope] to a new job queue.
575
    // 4. Set jobQueue to scope to job queue map[jobScope].
576
0
    auto& job_queue = scope_to_job_queue_map().ensure(job_scope, [&vm] {
577
0
        return JobQueue(vm.heap());
578
0
    });
579
580
    // 5. If jobQueue is empty, then:
581
0
    if (job_queue.is_empty()) {
582
        // 2. Set job’s containing job queue to jobQueue, and enqueue job to jobQueue.
583
0
        job->containing_job_queue = &job_queue;
584
0
        job_queue.append(job);
585
0
        run_job(vm, job_queue);
586
0
    }
587
    // 6. Else:
588
0
    else {
589
        // 1. Let lastJob be the element at the back of jobQueue.
590
0
        auto& last_job = job_queue.last();
591
592
        // 2. If job is equivalent to lastJob and lastJob’s job promise has not settled, append job to lastJob’s list of equivalent jobs.
593
        // FIXME: There's no WebIDL AO that corresponds to checking if an ECMAScript promise has settled
594
0
        if (job == last_job && !verify_cast<JS::Promise>(*job->job_promise->promise()).is_handled()) {
595
0
            last_job->list_of_equivalent_jobs.append(job);
596
0
        }
597
        // 3. Else, set job’s containing job queue to jobQueue, and enqueue job to jobQueue.
598
0
        else {
599
0
            job->containing_job_queue = &job_queue;
600
0
            job_queue.append(job);
601
0
        }
602
0
    }
603
0
}
604
605
}