Job.cpp 32 KB

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