LibWeb: Implement Cache.match and Cache.matchAll

This commit is contained in:
Timothy Flynn 2026-04-02 16:00:29 -04:00 committed by Shannon Booth
parent a478e3a30d
commit e2d0ed4beb
7 changed files with 218 additions and 61 deletions

View file

@ -42,6 +42,160 @@ void Cache::visit_edges(Visitor& 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)
{

View file

@ -51,6 +51,8 @@ class Cache : public Bindings::PlatformObject {
GC_DECLARE_ALLOCATOR(Cache);
public:
GC::Ref<WebIDL::Promise> match(Fetch::RequestInfo, CacheQueryOptions);
GC::Ref<WebIDL::Promise> match_all(Optional<Fetch::RequestInfo>, CacheQueryOptions);
GC::Ref<WebIDL::Promise> add(Fetch::RequestInfo);
GC::Ref<WebIDL::Promise> add_all(ReadonlySpan<Fetch::RequestInfo>);
GC::Ref<WebIDL::Promise> put(Fetch::RequestInfo, GC::Ref<Fetch::Response>);

View file

@ -4,8 +4,8 @@
// https://w3c.github.io/ServiceWorker/#cache-interface
[SecureContext, Exposed=(Window,Worker)]
interface Cache {
// [NewObject] Promise<(Response or undefined)> match(RequestInfo request, optional CacheQueryOptions options = {});
// [NewObject] Promise<FrozenArray<Response>> matchAll(optional RequestInfo request, optional CacheQueryOptions options = {});
[NewObject] Promise<(Response or undefined)> match(RequestInfo request, optional CacheQueryOptions options = {});
[NewObject] Promise<FrozenArray<Response>> matchAll(optional RequestInfo request, optional CacheQueryOptions options = {});
[NewObject] Promise<undefined> add(RequestInfo request);
[NewObject] Promise<undefined> addAll(sequence<RequestInfo> requests);
[NewObject] Promise<undefined> put(RequestInfo request, Response response);

View file

@ -2,10 +2,10 @@ Harness status: OK
Found 22 tests
14 Pass
8 Fail
18 Pass
4 Fail
Pass Cache.add called with no arguments
Fail Cache.add called with relative URL specified as a string
Pass Cache.add called with relative URL specified as a string
Pass Cache.add called with non-HTTP/HTTPS URL
Pass Cache.add called with Request object
Pass Cache.add called with POST request
@ -19,9 +19,9 @@ Pass Cache.add with request that results in a status of 500
Pass Cache.addAll with no arguments
Pass Cache.addAll with a mix of valid and undefined arguments
Fail Cache.addAll with an empty array
Fail Cache.addAll with string URL arguments
Fail Cache.addAll with Request arguments
Fail Cache.addAll with a mix of succeeding and failing requests
Pass Cache.addAll with string URL arguments
Pass Cache.addAll with Request arguments
Pass Cache.addAll with a mix of succeeding and failing requests
Pass Cache.addAll called with the same Request object specified twice
Fail Cache.addAll should succeed when entries differ by vary header
Fail Cache.addAll should reject when entries are duplicate by vary header

View file

@ -2,29 +2,30 @@ Harness status: OK
Found 25 tests
25 Fail
Fail Cache.match with no matching entries
Fail Cache.match with URL
Fail Cache.match with Request
Fail Cache.match with multiple cache hits
Fail Cache.match with new Request
Fail Cache.match with HEAD
Fail Cache.match with ignoreSearch option (request with no search parameters)
Fail Cache.match with ignoreSearch option (request with search parameter)
Fail Cache.match supports ignoreMethod
Fail Cache.match supports ignoreVary
Fail Cache.match does not support cacheName option
Fail Cache.match with URL containing fragment
Fail Cache.match with string fragment "http" as query
Fail Cache.match with responses containing "Vary" header
Fail Cache.match with Request and Response objects with different URLs
Fail Cache.match invoked multiple times for the same Request/Response
Fail Cache.match blob should be sliceable
Fail Cache.match with POST Request
Fail Cache.match with a non-2xx Response
Fail Cache.match with a network error Response
Fail Cache produces large Responses that can be cloned and read correctly.
23 Pass
2 Fail
Pass Cache.match with no matching entries
Pass Cache.match with URL
Pass Cache.match with Request
Pass Cache.match with multiple cache hits
Pass Cache.match with new Request
Pass Cache.match with HEAD
Pass Cache.match with ignoreSearch option (request with no search parameters)
Pass Cache.match with ignoreSearch option (request with search parameter)
Pass Cache.match supports ignoreMethod
Pass Cache.match supports ignoreVary
Pass Cache.match does not support cacheName option
Pass Cache.match with URL containing fragment
Pass Cache.match with string fragment "http" as query
Pass Cache.match with responses containing "Vary" header
Pass Cache.match with Request and Response objects with different URLs
Pass Cache.match invoked multiple times for the same Request/Response
Pass Cache.match blob should be sliceable
Pass Cache.match with POST Request
Pass Cache.match with a non-2xx Response
Pass Cache.match with a network error Response
Pass Cache produces large Responses that can be cloned and read correctly.
Fail cors-exposed header should be stored correctly.
Fail MIME type should be set from content-header correctly.
Fail MIME type should reflect Content-Type headers of response.
Pass MIME type should be set from content-header correctly.
Pass MIME type should reflect Content-Type headers of response.
Fail Cache.match ignores vary headers on opaque response.

View file

@ -2,20 +2,20 @@ Harness status: OK
Found 16 tests
16 Fail
Fail Cache.matchAll with no matching entries
Fail Cache.matchAll with URL
Fail Cache.matchAll with Request
Fail Cache.matchAll with new Request
Fail Cache.matchAll with HEAD
Fail Cache.matchAll with ignoreSearch option (request with no search parameters)
Fail Cache.matchAll with ignoreSearch option (request with search parameters)
Fail Cache.matchAll supports ignoreMethod
Fail Cache.matchAll supports ignoreVary
Fail Cache.matchAll with URL containing fragment
Fail Cache.matchAll with string fragment "http" as query
Fail Cache.matchAll without parameters
Fail Cache.matchAll with explicitly undefined request
Fail Cache.matchAll with explicitly undefined request and empty options
Fail Cache.matchAll with responses containing "Vary" header
Fail Cache.matchAll with multiple vary pairs
16 Pass
Pass Cache.matchAll with no matching entries
Pass Cache.matchAll with URL
Pass Cache.matchAll with Request
Pass Cache.matchAll with new Request
Pass Cache.matchAll with HEAD
Pass Cache.matchAll with ignoreSearch option (request with no search parameters)
Pass Cache.matchAll with ignoreSearch option (request with search parameters)
Pass Cache.matchAll supports ignoreMethod
Pass Cache.matchAll supports ignoreVary
Pass Cache.matchAll with URL containing fragment
Pass Cache.matchAll with string fragment "http" as query
Pass Cache.matchAll without parameters
Pass Cache.matchAll with explicitly undefined request
Pass Cache.matchAll with explicitly undefined request and empty options
Pass Cache.matchAll with responses containing "Vary" header
Pass Cache.matchAll with multiple vary pairs

View file

@ -2,24 +2,24 @@ Harness status: OK
Found 27 tests
13 Pass
14 Fail
23 Pass
4 Fail
Pass Cache.put called with simple Request and Response
Fail Cache.put called with Request and Response from fetch()
Pass Cache.put called with Request and Response from fetch()
Pass Cache.put with Request without a body
Pass Cache.put with Response without a body
Fail Cache.put with a Response containing an empty URL
Fail Cache.put with an empty response body
Pass Cache.put with a Response containing an empty URL
Pass Cache.put with an empty response body
Pass Cache.put with synthetic 206 response
Fail Cache.put with HTTP 206 response
Fail Cache.put with opaque-filtered HTTP 206 response
Pass Cache.put with opaque-filtered HTTP 206 response
Fail Cache.put with HTTP 500 response
Fail Cache.put called twice with matching Requests and different Responses
Pass Cache.put called twice with matching Requests and different Responses
Fail Cache.put called multiple times with request URLs that differ only by a fragment
Fail Cache.put with a string request
Pass Cache.put with a string request
Pass Cache.put with an invalid response
Pass Cache.put with a non-HTTP/HTTPS request
Fail Cache.put with a relative URL
Pass Cache.put with a relative URL
Pass Cache.put with a non-GET request
Pass Cache.put with a null response
Pass Cache.put with a POST request
@ -28,6 +28,6 @@ Pass getReader() after Cache.put
Pass Cache.put with a VARY:* Response
Pass Cache.put with an embedded VARY:* Response
Fail Cache.put with a VARY:* opaque response should not reject
Fail Cache.put should store Response.redirect() correctly
Fail Cache.put called with simple Request and blob Response
Fail Cache.put called with simple Request and form data Response
Pass Cache.put should store Response.redirect() correctly
Pass Cache.put called with simple Request and blob Response
Pass Cache.put called with simple Request and form data Response