ladybird/Libraries/LibWeb/ServiceWorker/Cache.cpp

952 lines
47 KiB
C++
Raw Normal View History

/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Array.h>
#include <LibWeb/Bindings/CachePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
2026-04-02 17:58:21 -04:00
#include <LibWeb/DOM/AbortSignal.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
2026-04-02 15:54:45 -04:00
#include <LibWeb/Fetch/Response.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/ServiceWorker/Cache.h>
#include <LibWeb/ServiceWorker/ServiceWorkerGlobalScope.h>
2026-04-02 15:54:45 -04:00
#include <LibWeb/Streams/ReadableStream.h>
#include <LibWeb/Streams/ReadableStreamDefaultReader.h>
namespace Web::ServiceWorker {
GC_DEFINE_ALLOCATOR(Cache);
GC_DEFINE_ALLOCATOR(CacheBatchOperation);
Cache::Cache(JS::Realm& realm, GC::Ref<RequestResponseList> request_response_list)
: Bindings::PlatformObject(realm)
, m_request_response_list(request_response_list)
{
}
void Cache::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(Cache);
}
void Cache::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_request_response_list);
}
// https://w3c.github.io/ServiceWorker/#cache-match
GC::Ref<WebIDL::Promise> Cache::match(Fetch::RequestInfo request, CacheQueryOptions options)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let promise be a new promise.
auto promise = WebIDL::create_promise(realm);
// 2. Run these substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, promise, request = move(request), options]() {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Let p be the result of running the algorithm specified in matchAll(request, options) method with request and options.
// 2. Wait until p settles.
WebIDL::react_to_promise(match_all(move(request), options),
// 4. Else if p resolves with an array, responses, then:
GC::create_function(realm.heap(), [&realm, promise](JS::Value value) -> WebIDL::ExceptionOr<JS::Value> {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. If responses is an empty array, then:
if (auto& responses = value.as<JS::Array>(); responses.indexed_array_like_size() == 0) {
// 1. Resolve promise with undefined.
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
}
// 2. Else:
else {
// 1. Resolve promise with the first element of responses.
auto first_element = responses.indexed_get(0).release_value();
WebIDL::resolve_promise(realm, promise, first_element.value);
}
return JS::js_undefined();
}),
// 3. If p rejects with an exception, then:
GC::create_function(realm.heap(), [&realm, promise](JS::Value exception) -> WebIDL::ExceptionOr<JS::Value> {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Reject promise with that exception.
WebIDL::reject_promise(realm, promise, exception);
return JS::js_undefined();
}));
}));
// 3. Return promise.
return promise;
}
// https://w3c.github.io/ServiceWorker/#cache-matchall
GC::Ref<WebIDL::Promise> Cache::match_all(Optional<Fetch::RequestInfo> request, CacheQueryOptions options)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let r be null.
GC::Ptr<Fetch::Infrastructure::Request> inner_request;
// 2. If the optional argument request is not omitted, then:
if (request.has_value()) {
TRY(request->visit(
// 1. If request is a Request object, then:
[&](GC::Root<Fetch::Request> const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
// 1. Set r to requests request.
inner_request = request->request();
// 2. If rs method is not `GET` and options.ignoreMethod is false, return a promise resolved with an
// empty array.
if (inner_request->method() != "GET"sv && !options.ignore_method)
return WebIDL::create_resolved_promise(realm, MUST(JS::Array::create(realm, 0)));
return {};
},
// 2. Else if request is a string, then:
[&](String const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
// 1. Set r to the associated request of the result of invoking the initial value of Request as
// constructor with request as its argument. If this throws an exception, return a promise rejected
// with that exception.
auto request_object = Fetch::Request::construct_impl(realm, request);
if (request_object.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, request_object.release_error());
inner_request = request_object.value()->request();
return {};
}));
}
// 3. Let realm be thiss relevant realm.
// 4. Let promise be a new promise.
auto promise = WebIDL::create_promise(realm);
// 5. Run these substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, inner_request, promise, request = move(request), options]() {
// 1. Let responses be an empty list.
auto responses = realm.heap().allocate<GC::HeapVector<GC::Ref<Fetch::Infrastructure::Response>>>();
// 2. If the optional argument request is omitted, then:
if (!request.has_value()) {
// 1. For each requestResponse of the relevant request response list:
for (auto& request_response : m_request_response_list->elements()) {
// 1. Add a copy of requestResponses response to responses.
responses->elements().append(request_response->response->clone(realm));
}
}
// 3. Else:
else {
// 1. Let requestResponses be the result of running Query Cache with r and options.
auto request_responses = query_cache(*inner_request, options);
// 2. For each requestResponse of requestResponses:
for (auto request_response : request_responses->elements()) {
// 1. Add a copy of requestResponses response to responses.
// NB: No need to copy. Query Cache creates a copy, and the requestResponses list is dropped hereafter.
responses->elements().append(request_response->response);
}
}
// 3. For each response of responses:
for (auto response : responses->elements()) {
// 1. If responses type is "opaque" and cross-origin resource policy check with promises relevant settings
// objects origin, promises relevant settings object, "", and responses internal response returns
// blocked, then reject promise with a TypeError and abort these steps.
if (response->type() == Fetch::Infrastructure::Response::Type::Opaque) {
// FIXME: Perform the cross-origin resource policy check.
}
}
// 4. Queue a task, on promises relevant settings objects responsible event loop using the DOM manipulation
// task source, to perform the following steps:
HTML::queue_a_task(
HTML::Task::Source::DOMManipulation,
HTML::relevant_settings_object(promise->promise()).responsible_event_loop(),
{},
GC::create_function(realm.heap(), [&realm, promise, responses]() {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Let responseList be a list.
auto response_list = realm.heap().allocate<GC::HeapVector<JS::Value>>();
// 2. For each response of responses:
for (auto response : responses->elements()) {
// 1. Add a new Response object associated with response and a new Headers object whose guard is
// "immutable" to responseList.
response_list->elements().append(Fetch::Response::create(realm, response, Fetch::Headers::Guard::Immutable));
}
// 3. Resolve promise with a frozen array created from responseList, in realm.
WebIDL::resolve_promise(realm, promise, JS::Array::create_from(realm, response_list->elements()));
}));
}));
// 6. Return promise.
return promise;
}
// https://w3c.github.io/ServiceWorker/#cache-add
GC::Ref<WebIDL::Promise> Cache::add(Fetch::RequestInfo request)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let requests be an array containing only request.
// 2. Let responseArrayPromise be the result of running the algorithm specified in addAll(requests) passing requests
// as the argument.
auto promise = add_all({ { request } });
// 3. Return the result of reacting to responseArrayPromise with a fulfillment handler that returns undefined.
return WebIDL::upon_fulfillment(promise, GC::create_function(realm.heap(), [](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
return JS::js_undefined();
}));
}
// https://w3c.github.io/ServiceWorker/#cache-addAll
GC::Ref<WebIDL::Promise> Cache::add_all(ReadonlySpan<Fetch::RequestInfo> requests)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let responsePromises be an empty list.
auto response_promises = realm.heap().allocate<GC::HeapVector<GC::Ref<WebIDL::Promise>>>();
// 2. Let requestList be an empty list.
auto request_list = realm.heap().allocate<GC::HeapVector<GC::Ref<Fetch::Infrastructure::Request>>>();
// 3. For each request whose type is Request in requests:
for (auto const& request_info : requests) {
if (auto const* request = request_info.get_pointer<GC::Root<Fetch::Request>>()) {
// 1. Let r be requests request.
auto inner_request = (*request)->request();
// 2. If rs urls scheme is not one of "http" and "https", or rs method is not `GET`, return a promise
// rejected with a TypeError.
if (!inner_request->url().scheme().is_one_of("http"sv, "https"sv) || inner_request->method() != "GET"sv)
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Request must be a GET request with an HTTP(S) URL"sv));
}
}
// 4. Let fetchControllers be a list of fetch controllers.
auto fetch_controllers = realm.heap().allocate<GC::HeapVector<GC::Ref<Fetch::Infrastructure::FetchController>>>();
// 5. For each request in requests:
for (auto const& request_info : requests) {
// 1. Let r be the associated request of the result of invoking the initial value of Request as constructor with
// request as its argument. If this throws an exception, return a promise rejected with that exception.
auto result = Fetch::Request::construct_impl(realm, request_info);
if (result.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, result.release_error());
auto inner_request = result.value()->request();
// 2. If rs urls scheme is not one of "http" and "https", then:
if (!inner_request->url().scheme().is_one_of("http"sv, "https"sv)) {
// 1. For each fetchController of fetchControllers, abort fetchController.
for (auto fetch_controller : fetch_controllers->elements())
fetch_controller->abort(realm, {});
// 2. Return a promise rejected with a TypeError.
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Request must have an HTTP(S) URL"sv));
}
// 3. If rs clients global object is a ServiceWorkerGlobalScope object, set requests service-workers mode to "none".
if (is<ServiceWorkerGlobalScope>(inner_request->client()->global_object()))
inner_request->set_service_workers_mode(Fetch::Infrastructure::Request::ServiceWorkersMode::None);
// 4. Set rs initiator to "fetch" and destination to "subresource".
// FIXME: Spec issue: There is no "fetch" initiator (spec probably wants initiator type). And there is no
// "subresource" destination (so we set it to the "empty string" destination for now).
// https://github.com/w3c/ServiceWorker/issues/1718
inner_request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Fetch);
inner_request->set_destination({});
// 5. Add r to requestList.
request_list->elements().append(inner_request);
// 6. Let responsePromise be a new promise.
auto response_promise = WebIDL::create_promise(realm);
// 7. Run the following substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, fetch_controllers, inner_request, response_promise]() {
// * Append the result of fetching r.
Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
// * To processResponse for response, run these substeps:
fetch_algorithms_input.process_response = [&realm, fetch_controllers, response_promise](GC::Ref<Fetch::Infrastructure::Response> response) {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
bool did_reject_promise = false;
// 1. If responses type is "error", or responses status is not an ok status or is 206, reject
// responsePromise with a TypeError.
auto status = response->status();
if (response->type() == Fetch::Infrastructure::Response::Type::Error || !Fetch::Infrastructure::is_ok_status(status) || status == 206) {
WebIDL::reject_promise(realm, response_promise, JS::TypeError::create(realm, "Fetch request failed"sv));
did_reject_promise = true;
}
// 2. Else if responses header list contains a header named `Vary`, then:
else if (response->header_list()->contains("Vary"sv)) {
// 1. Let fieldValues be the list containing the elements corresponding to the field-values of the
// Vary header.
// 2. For each fieldValue of fieldValues:
response->header_list()->for_each_vary_header([&](StringView field_value) {
// 1. If fieldValue matches "*", then:
if (field_value == "*"sv) {
// 1. Reject responsePromise with a TypeError.
WebIDL::reject_promise(realm, response_promise, JS::TypeError::create(realm, "Vary '*' is not supported"sv));
did_reject_promise = true;
// 2. For each fetchController of fetchControllers, abort fetchController.
for (auto fetch_controller : fetch_controllers->elements())
fetch_controller->abort(realm, {});
// 3. Abort these steps.
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
}
if (did_reject_promise)
return;
// * To processResponseEndOfBody for response, run these substeps:
// FIXME: Spec issue? processResponseEndOfBody is not invoked until the response's body is read, but
// doing so here locks the body's stream, thus cloning the response during batch operations fails.
// So we resolve the pending promise here, which seems to work fine.
// 1. If responses aborted flag is set, reject responsePromise with an "AbortError" DOMException and
// abort these steps.
if (response->aborted()) {
WebIDL::reject_promise(realm, response_promise, WebIDL::AbortError::create(realm, "Fetch request was aborted"_utf16));
return;
}
// 2. Resolve responsePromise with response.
WebIDL::resolve_promise(realm, response_promise, response);
};
auto fetch_controller = Fetch::Fetching::fetch(realm, inner_request, Fetch::Infrastructure::FetchAlgorithms::create(realm.vm(), move(fetch_algorithms_input)));
fetch_controllers->elements().append(fetch_controller);
// Note: The cache commit is allowed when the responses body is fully received.
}));
// 8. Add responsePromise to responsePromises.
response_promises->elements().append(response_promise);
}
// 6. Let p be the result of getting a promise to wait for all of responsePromises.
auto promise = WebIDL::get_promise_for_wait_for_all(realm, response_promises->elements());
// 7. Return the result of reacting to p with a fulfillment handler that, when called with argument responses, performs the following substeps:
return WebIDL::upon_fulfillment(promise, GC::create_function(realm.heap(), [this, &realm, request_list](JS::Value result) -> WebIDL::ExceptionOr<JS::Value> {
HTML::TemporaryExecutionContext context { realm };
auto& responses = result.as<JS::Array>();
// 1. Let operations be an empty list.
auto operations = realm.heap().allocate<GC::HeapVector<GC::Ref<CacheBatchOperation>>>();
// 2. Let index be zero.
// 3. For each response in responses:
for (size_t index = 0; index < responses.indexed_array_like_size(); ++index) {
auto& response = as<Fetch::Infrastructure::Response>(responses.indexed_get(index)->value.as_cell());
// 1. Let operation be a cache batch operation.
auto operation = realm.heap().allocate<CacheBatchOperation>(
// 2. Set operations type to "put".
CacheBatchOperation::Type::Put,
// 3. Set operations request to requestList[index].
request_list->elements()[index],
// 4. Set operations response to response.
response);
// 5. Append operation to operations.
operations->elements().append(operation);
// 6. Increment index by one.
}
// 4. Let realm be thiss relevant realm.
// 5. Let cacheJobPromise be a new promise.
auto cache_job_promise = WebIDL::create_promise(realm);
// 6. Run the following substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, operations, cache_job_promise]() {
// 1. Let errorData be null.
// 2. Invoke Batch Cache Operations with operations. If this throws an exception, set errorData to the
// exception.
auto error_data = batch_cache_operations(operations);
// 3. Queue a task, on cacheJobPromises relevant settings objects responsible event loop using the DOM
// manipulation task source, to perform the following substeps:
HTML::queue_a_task(
HTML::Task::Source::DOMManipulation,
HTML::relevant_settings_object(cache_job_promise->promise()).responsible_event_loop(),
{},
GC::create_function(realm.heap(), [&realm, cache_job_promise, error_data = move(error_data)]() mutable {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. If errorData is null, resolve cacheJobPromise with undefined.
if (!error_data.is_error())
WebIDL::resolve_promise(realm, cache_job_promise, JS::js_undefined());
// 2. Else, reject cacheJobPromise with a new exception with errorData, in realm.
else
WebIDL::reject_promise_with_exception(realm, cache_job_promise, error_data.release_error());
}));
}));
// 7. Return cacheJobPromise.
return cache_job_promise->promise();
}));
}
2026-04-02 15:54:45 -04:00
// https://w3c.github.io/ServiceWorker/#cache-put
GC::Ref<WebIDL::Promise> Cache::put(Fetch::RequestInfo request, GC::Ref<Fetch::Response> response)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let innerRequest be null.
GC::Ptr<Fetch::Infrastructure::Request> inner_request;
TRY(request.visit(
// 2. If request is a Request object, then set innerRequest to requests request.
[&](GC::Root<Fetch::Request> const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
inner_request = request->request();
return {};
},
// 3. Else:
[&](String const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
// 1. Let requestObj be the result of invoking Requests constructor with request as its argument. If this
// throws an exception, return a promise rejected with exception.
auto request_object = Fetch::Request::construct_impl(realm, request);
if (request_object.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, request_object.release_error());
inner_request = request_object.value()->request();
return {};
}));
// 4. If innerRequests urls scheme is not one of "http" and "https", or innerRequests method is not `GET`, return
// a promise rejected with a TypeError.
if (!inner_request->url().scheme().is_one_of("http"sv, "https"sv) || inner_request->method() != "GET"sv)
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Request must be a GET request with an HTTP(S) URL"sv));
// 5. Let innerResponse be responses response.
auto inner_response = response->response();
// 6. If innerResponses status is 206, return a promise rejected with a TypeError.
if (inner_response->status() == 206)
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Partial responses are not supported"sv));
// 7. If innerResponses header list contains a header named `Vary`, then:
// 1. Let fieldValues be the list containing the items corresponding to the Vary headers field-values.
// 2. For each fieldValue in fieldValues:
bool found_vary_wildcard = false;
inner_response->header_list()->for_each_vary_header([&](StringView field_value) {
// 1. If fieldValue matches "*", return a promise rejected with a TypeError.
found_vary_wildcard = field_value == "*"sv;
return found_vary_wildcard ? IterationDecision::Break : IterationDecision::Continue;
});
if (found_vary_wildcard)
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Vary '*' is not supported"sv));
// 8. If innerResponses body is disturbed or locked, return a promise rejected with a TypeError.
if (auto body = inner_response->body()) {
if (auto stream = body->stream(); stream->is_disturbed() || stream->is_locked())
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Response's body stream is disturbed or locked"sv));
}
// 9. Let clonedResponse be a clone of innerResponse.
auto cloned_response = inner_response->clone(realm);
// 10. Let bodyReadPromise be a promise resolved with undefined.
auto body_read_promise = WebIDL::create_resolved_promise(realm, JS::js_undefined());
// 11. If innerResponses body is non-null, run these substeps:
if (auto body = inner_response->body()) {
// 1. Let stream be innerResponses bodys stream.
auto stream = body->stream();
// 2. Let reader be the result of getting a reader for stream.
auto reader = MUST(stream->get_a_reader());
// 3. Set bodyReadPromise to the result of reading all bytes from reader.
body_read_promise = reader->read_all_bytes_deprecated();
}
// Note: This ensures that innerResponses body is locked, and we have a full buffered copy of the body in
// clonedResponse. An implementation could optimize by streaming directly to disk rather than memory.
// 12. Let operations be an empty list.
auto operations = realm.heap().allocate<GC::HeapVector<GC::Ref<CacheBatchOperation>>>();
// 13. Let operation be a cache batch operation.
auto operation = realm.heap().allocate<CacheBatchOperation>(
// 14. Set operations type to "put".
CacheBatchOperation::Type::Put,
// 15. Set operations request to innerRequest.
*inner_request,
// 16. Set operations response to clonedResponse.
cloned_response);
// 17. Append operation to operations.
operations->elements().append(operation);
// 18. Let realm be thiss relevant realm.
// 19. Return the result of the fulfillment of bodyReadPromise:
return WebIDL::upon_fulfillment(body_read_promise, GC::create_function(realm.heap(), [this, &realm, operations](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
HTML::TemporaryExecutionContext context { realm };
// 1. Let cacheJobPromise be a new promise.
auto cache_job_promise = WebIDL::create_promise(realm);
// 2. Return cacheJobPromise and run these steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, operations, cache_job_promise]() {
// 1. Let errorData be null.
// 2. Invoke Batch Cache Operations with operations. If this throws an exception, set errorData to the exception.
auto error_data = batch_cache_operations(operations);
// 3. Queue a task, on cacheJobPromises relevant settings objects responsible event loop using the DOM
// manipulation task source, to perform the following substeps:
HTML::queue_a_task(
HTML::Task::Source::DOMManipulation,
HTML::relevant_settings_object(cache_job_promise->promise()).responsible_event_loop(),
{},
GC::create_function(realm.heap(), [&realm, cache_job_promise, error_data = move(error_data)]() mutable {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. If errorData is null, resolve cacheJobPromise with undefined.
if (!error_data.is_error())
WebIDL::resolve_promise(realm, cache_job_promise, JS::js_undefined());
// 2. Else, reject cacheJobPromise with a new exception with errorData, in realm.
else
WebIDL::reject_promise_with_exception(realm, cache_job_promise, error_data.release_error());
}));
}));
return cache_job_promise->promise();
}));
}
2026-04-02 19:26:06 -04:00
// https://w3c.github.io/ServiceWorker/#cache-delete
GC::Ref<WebIDL::Promise> Cache::delete_(Fetch::RequestInfo request, CacheQueryOptions options)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let r be null.
GC::Ptr<Fetch::Infrastructure::Request> inner_request;
TRY(request.visit(
// 2. If request is a Request object, then:
[&](GC::Root<Fetch::Request> const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
// 1. Set r to requests request.
inner_request = request->request();
// 2. If rs method is not `GET` and options.ignoreMethod is false, return a promise resolved with false.
if (inner_request->method() != "GET"sv && !options.ignore_method)
return WebIDL::create_resolved_promise(realm, JS::Value { false });
return {};
},
// 3. Else if request is a string, then:
[&](String const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
// 1. Set r to the associated request of the result of invoking the initial value of Request as constructor
// with request as its argument. If this throws an exception, return a promise rejected with that
// exception.
auto request_object = Fetch::Request::construct_impl(realm, request);
if (request_object.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, request_object.release_error());
inner_request = request_object.value()->request();
return {};
}));
// 4. Let operations be an empty list.
auto operations = realm.heap().allocate<GC::HeapVector<GC::Ref<CacheBatchOperation>>>();
// 5. Let operation be a cache batch operation.
auto operation = realm.heap().allocate<CacheBatchOperation>(
// 6. Set operations type to "delete".
CacheBatchOperation::Type::Delete,
// 7. Set operations request to r.
*inner_request,
nullptr,
// 8. Set operations options to options.
options);
// 9. Append operation to operations.
operations->elements().append(operation);
// 10. Let realm be thiss relevant realm.
// 11. Let cacheJobPromise be a new promise.
auto cache_job_promise = WebIDL::create_promise(realm);
// 12. Run the following substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, operations, cache_job_promise]() {
// 1. Let errorData be null.
// 2. Let requestResponses be the result of running Batch Cache Operations with operations. If this throws an
// exception, set errorData to the exception.
auto error_data = batch_cache_operations(operations);
// 3. Queue a task, on cacheJobPromises relevant settings objects responsible event loop using the DOM
// manipulation task source, to perform the following substeps:
HTML::queue_a_task(
HTML::Task::Source::DOMManipulation,
HTML::relevant_settings_object(cache_job_promise->promise()).responsible_event_loop(),
{},
GC::create_function(realm.heap(), [&realm, cache_job_promise, error_data = move(error_data)]() mutable {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. If errorData is null, then:
if (!error_data.is_error()) {
// 1. If requestResponses is not empty, resolve cacheJobPromise with true.
// 2. Else, resolve cacheJobPromise with false.
WebIDL::resolve_promise(realm, cache_job_promise, JS::Value { error_data.value() });
}
// 2. Else, reject cacheJobPromise with a new exception with errorData, in realm.
else {
WebIDL::reject_promise_with_exception(realm, cache_job_promise, error_data.release_error());
}
}));
}));
// 13. Return cacheJobPromise.
return cache_job_promise;
}
2026-04-02 17:58:21 -04:00
// https://w3c.github.io/ServiceWorker/#cache-keys
GC::Ref<WebIDL::Promise> Cache::keys(Optional<Fetch::RequestInfo> request, CacheQueryOptions options)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let r be null.
GC::Ptr<Fetch::Infrastructure::Request> inner_request;
// 2. If the optional argument request is not omitted, then:
if (request.has_value()) {
TRY(request->visit(
// 1. If request is a Request object, then:
[&](GC::Root<Fetch::Request> const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
// 1. Set r to requests request.
inner_request = request->request();
// 2. If rs method is not `GET` and options.ignoreMethod is false, return a promise resolved with an
// empty array.
if (inner_request->method() != "GET"sv && !options.ignore_method)
return WebIDL::create_resolved_promise(realm, MUST(JS::Array::create(realm, 0)));
return {};
},
// 2. Else if request is a string, then:
[&](String const& request) -> ErrorOr<void, GC::Ref<WebIDL::Promise>> {
// 1. Set r to the associated request of the result of invoking the initial value of Request as
// constructor with request as its argument. If this throws an exception, return a promise rejected
// with that exception.
auto request_object = Fetch::Request::construct_impl(realm, request);
if (request_object.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, request_object.release_error());
inner_request = request_object.value()->request();
return {};
}));
}
// 3. Let realm be thiss relevant realm.
// 4. Let promise be a new promise.
auto promise = WebIDL::create_promise(realm);
// 5. Run these substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm, inner_request, promise, request = move(request), options]() {
// 1. Let requests be an empty list.
auto requests = realm.heap().allocate<GC::HeapVector<GC::Ref<Fetch::Infrastructure::Request>>>();
// 2. If the optional argument request is omitted, then:
if (!request.has_value()) {
// 1. For each requestResponse of the relevant request response list:
for (auto& request_response : m_request_response_list->elements()) {
// 1. Add requestResponses request to requests.
requests->elements().append(request_response->request);
}
}
// 3. Else:
else {
// 1. Let requestResponses be the result of running Query Cache with r and options.
auto request_responses = query_cache(*inner_request, options);
// 2. For each requestResponse of requestResponses:
for (auto request_response : request_responses->elements()) {
// 1. Add requestResponses request to requests.
requests->elements().append(request_response->request);
}
}
// 4. Queue a task, on promises relevant settings objects responsible event loop using the DOM manipulation
// task source, to perform the following steps:
HTML::queue_a_task(
HTML::Task::Source::DOMManipulation,
HTML::relevant_settings_object(promise->promise()).responsible_event_loop(),
{},
GC::create_function(realm.heap(), [&realm, promise, requests]() {
HTML::TemporaryExecutionContext context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Let requestList be a list.
auto request_list = realm.heap().allocate<GC::HeapVector<JS::Value>>();
// 2. For each request of requests:
for (auto request : requests->elements()) {
// 1. Add a new Request object associated with request and a new associated Headers object whose
// guard is "immutable" to requestList.
request_list->elements().append(Fetch::Request::create(realm, request, Fetch::Headers::Guard::Immutable, MUST(DOM::AbortSignal::construct_impl(realm))));
}
// 3. Resolve promise with a frozen array created from requestList, in realm.
WebIDL::resolve_promise(realm, promise, JS::Array::create_from(realm, request_list->elements()));
}));
}));
// 6. Return promise.
return promise;
}
// https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
static bool request_matches_cached_item(
GC::Ref<Fetch::Infrastructure::Request> request_query,
GC::Ref<Fetch::Infrastructure::Request> request,
GC::Ptr<Fetch::Infrastructure::Response> response,
CacheQueryOptions options)
{
// 1. If options["ignoreMethod"] is false and requests method is not `GET`, return false.
if (!options.ignore_method && request->method() != "GET"sv)
return false;
// 2. Let queryURL be requestQuerys url.
auto query_url = request_query->url();
// 3. Let cachedURL be requests url.
auto cached_url = request->url();
// 4. If options["ignoreSearch"] is true, then:
if (options.ignore_search) {
// 1. Set cachedURLs query to the empty string.
cached_url.set_query(String {});
// 2. Set queryURLs query to the empty string.
query_url.set_query(String {});
}
// 5. If queryURL does not equal cachedURL with the exclude fragment flag set, then return false.
if (!query_url.equals(cached_url, URL::ExcludeFragment::Yes))
return false;
// 6. If response is null, options["ignoreVary"] is true, or responses header list does not contain `Vary`, then
// return true.
if (!response || options.ignore_vary || !response->header_list()->contains("Vary"sv))
return true;
// 7. Let fieldValues be the list containing the elements corresponding to the field-values of the Vary header for
// the value of the header with name `Vary`.
// 8. For each fieldValue in fieldValues:
bool matches = true;
response->header_list()->for_each_vary_header([&](StringView field_value) {
// 1. If fieldValue matches "*", or the combined value given fieldValue and requests header list does not match
// the combined value given fieldValue and requestQuerys header list, then return false.
matches = field_value == "*"sv
|| request->header_list()->get(field_value) == request_query->header_list()->get(field_value);
return matches ? IterationDecision::Break : IterationDecision::Continue;
});
// 9. Return true.
return matches;
}
// https://w3c.github.io/ServiceWorker/#query-cache-algorithm
GC::Ref<RequestResponseList> Cache::query_cache(GC::Ref<Fetch::Infrastructure::Request> request_query, CacheQueryOptions options, GC::Ptr<RequestResponseList> target_storage, CloneCache clone_cache)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let resultList be an empty list.
auto result_list = realm.heap().allocate<RequestResponseList>();
// 2. Let storage be null.
// 3. If the optional argument targetStorage is omitted, set storage to the relevant request response list.
// 4. Else, set storage to targetStorage.
auto& storage = target_storage ? *target_storage : *m_request_response_list;
// 5. For each requestResponse of storage:
for (auto request_response : storage.elements()) {
// 1. Let cachedRequest be requestResponses request.
auto cached_request = request_response->request;
// 2. Let cachedResponse be requestResponses response.
auto cached_response = request_response->response;
// 3. If Request Matches Cached Item with requestQuery, cachedRequest, cachedResponse, and options returns true, then:
if (request_matches_cached_item(request_query, cached_request, cached_response, options)) {
// AD-HOC: Not all callers actually need a copy. In Batch Cache Operations, it becomes much easier to remove
// matching items from the cache if we can do a pointer comparison.
if (clone_cache == CloneCache::No) {
result_list->elements().append(request_response);
continue;
}
// 1. Let requestCopy be a copy of cachedRequest.
auto request_copy = cached_request->clone(realm);
// 2. Let responseCopy be a copy of cachedResponse.
auto response_copy = cached_response->clone(realm);
// 3. Add requestCopy/responseCopy to resultList.
result_list->elements().append(realm.heap().allocate<RequestResponse>(request_copy, response_copy));
}
}
// 6. Return resultList.
return result_list;
}
// https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
WebIDL::ExceptionOr<bool> Cache::batch_cache_operations(GC::Ref<GC::HeapVector<GC::Ref<CacheBatchOperation>>> operations)
{
auto& realm = HTML::relevant_realm(*this);
// 1. Let cache be the relevant request response list.
auto cache = m_request_response_list;
// 2. Let backupCache be a new request response list that is a copy of cache.
auto backup_cache = realm.heap().allocate<RequestResponseList>();
for (auto request_response : cache->elements()) {
auto backup_request = request_response->request->clone(realm);
auto backup_response = request_response->response->clone(realm);
auto backup_request_response = realm.heap().allocate<RequestResponse>(backup_request, backup_response);
backup_cache->elements().append(backup_request_response);
}
// 3. Let addedItems be an empty list.
auto added_items = realm.heap().allocate<RequestResponseList>();
// 4. Try running the following substeps atomically:
auto result = [&]() -> WebIDL::ExceptionOr<bool> {
// 1. Let resultList be an empty list.
// NB: This result list is unused, only Cache.delete needs to know if there were any results.
bool removed_any_items = false;
auto remove_items_from_cache = [&](auto request_responses) {
removed_any_items |= cache->elements().remove_all_matching([&](GC::Ref<RequestResponse> request_response) {
return request_responses->elements().contains_slow(request_response);
});
};
// 2. For each operation in operations:
for (auto operation : operations->elements()) {
// 1. If operations type matches neither "delete" nor "put", throw a TypeError.
if (operation->type != CacheBatchOperation::Type::Delete && operation->type != CacheBatchOperation::Type::Put)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Batch operation type must be 'delete' or 'put'"sv };
// 2. If operations type matches "delete" and operations response is not null, throw a TypeError.
if (operation->type == CacheBatchOperation::Type::Delete && operation->response)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Batch operations of type 'delete' must not have a response"sv };
// 3. If the result of running Query Cache with operations request, operations options, and addedItems is
// not empty, throw an "InvalidStateError" DOMException.
if (!query_cache(operation->request, operation->options, added_items, CloneCache::No)->elements().is_empty())
return WebIDL::InvalidStateError::create(realm, "Batch operation requests must be unique"_utf16);
// 4. Let requestResponses be an empty list.
GC::Ptr<RequestResponseList> request_responses;
// 5. If operations type matches "delete", then:
if (operation->type == CacheBatchOperation::Type::Delete) {
// 1. Set requestResponses to the result of running Query Cache with operations request and operations options.
request_responses = query_cache(operation->request, operation->options, {}, CloneCache::No);
// 2. For each requestResponse in requestResponses:
// 1. Remove the item whose value matches requestResponse from cache.
remove_items_from_cache(request_responses);
}
// 6. Else if operations type matches "put", then:
else if (operation->type == CacheBatchOperation::Type::Put) {
// 1. If operations response is null, throw a TypeError.
if (!operation->response)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Batch operations of type 'put' must have a response"sv };
// 2. Let r be operations requests associated request.
auto request = operation->request;
// 3. If rs urls scheme is not one of "http" and "https", throw a TypeError.
if (!request->url().scheme().is_one_of("http"sv, "https"sv))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Request must be a GET request with an HTTP(S) URL"sv };
// 4. If rs method is not `GET`, throw a TypeError.
if (request->method() != "GET"sv)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Request must be a GET request with an HTTP(S) URL"sv };
// 5. If operations options is not null, throw a TypeError.
// FIXME: Spec issue: No other part of the spec indicates that options may be created as null.
// 6. Set requestResponses to the result of running Query Cache with operations request.
request_responses = query_cache(operation->request, {}, {}, CloneCache::No);
// 7. For each requestResponse of requestResponses:
// 1. Remove the item whose value matches requestResponse from cache.
remove_items_from_cache(request_responses);
// 8. Append operations request/operations response to cache.
cache->elements().append(realm.heap().allocate<RequestResponse>(operation->request, *operation->response));
// FIXME: 9. If the cache write operation in the previous two steps failed due to exceeding the granted quota
// limit, throw a QuotaExceededError.
// 10. Append operations request/operations response to addedItems.
added_items->elements().append(realm.heap().allocate<RequestResponse>(operation->request, *operation->response));
}
// 7. Append operations request/operations response to resultList.
}
// 3. Return resultList.
return removed_any_items;
}();
// 5. And then, if an exception was thrown, then:
if (result.is_error()) {
// 1. Remove all the items from the relevant request response list.
// 2. For each requestResponse of backupCache:
// 1. Append requestResponse to the relevant request response list.
m_request_response_list = backup_cache;
// 3. Throw the exception.
return result.release_error();
// Note: When an exception is thrown, the implementation does undo (roll back) any changes made to the cache
// storage during the batch operation job.
}
return result.release_value();
}
void CacheBatchOperation::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(request);
visitor.visit(response);
}
}