/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 = [®istration = *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, ®istration, &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, ®istration = *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()() constUnexecuted 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()() constUnexecuted 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 | | } |