Spaces:
Runtime error
Runtime error
| const { kConstruct } = require('./symbols') | |
| const { urlEquals, fieldValues: getFieldValues } = require('./util') | |
| const { kEnumerableProperty, isDisturbed } = require('../core/util') | |
| const { kHeadersList } = require('../core/symbols') | |
| const { webidl } = require('../fetch/webidl') | |
| const { Response, cloneResponse } = require('../fetch/response') | |
| const { Request } = require('../fetch/request') | |
| const { kState, kHeaders, kGuard, kRealm } = require('../fetch/symbols') | |
| const { fetching } = require('../fetch/index') | |
| const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util') | |
| const assert = require('assert') | |
| const { getGlobalDispatcher } = require('../global') | |
| /** | |
| * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation | |
| * @typedef {Object} CacheBatchOperation | |
| * @property {'delete' | 'put'} type | |
| * @property {any} request | |
| * @property {any} response | |
| * @property {import('../../types/cache').CacheQueryOptions} options | |
| */ | |
| /** | |
| * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list | |
| * @typedef {[any, any][]} requestResponseList | |
| */ | |
| class Cache { | |
| /** | |
| * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list | |
| * @type {requestResponseList} | |
| */ | |
| #relevantRequestResponseList | |
| constructor () { | |
| if (arguments[0] !== kConstruct) { | |
| webidl.illegalConstructor() | |
| } | |
| this.#relevantRequestResponseList = arguments[1] | |
| } | |
| async match (request, options = {}) { | |
| webidl.brandCheck(this, Cache) | |
| webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' }) | |
| request = webidl.converters.RequestInfo(request) | |
| options = webidl.converters.CacheQueryOptions(options) | |
| const p = await this.matchAll(request, options) | |
| if (p.length === 0) { | |
| return | |
| } | |
| return p[0] | |
| } | |
| async matchAll (request = undefined, options = {}) { | |
| webidl.brandCheck(this, Cache) | |
| if (request !== undefined) request = webidl.converters.RequestInfo(request) | |
| options = webidl.converters.CacheQueryOptions(options) | |
| // 1. | |
| let r = null | |
| // 2. | |
| if (request !== undefined) { | |
| if (request instanceof Request) { | |
| // 2.1.1 | |
| r = request[kState] | |
| // 2.1.2 | |
| if (r.method !== 'GET' && !options.ignoreMethod) { | |
| return [] | |
| } | |
| } else if (typeof request === 'string') { | |
| // 2.2.1 | |
| r = new Request(request)[kState] | |
| } | |
| } | |
| // 5. | |
| // 5.1 | |
| const responses = [] | |
| // 5.2 | |
| if (request === undefined) { | |
| // 5.2.1 | |
| for (const requestResponse of this.#relevantRequestResponseList) { | |
| responses.push(requestResponse[1]) | |
| } | |
| } else { // 5.3 | |
| // 5.3.1 | |
| const requestResponses = this.#queryCache(r, options) | |
| // 5.3.2 | |
| for (const requestResponse of requestResponses) { | |
| responses.push(requestResponse[1]) | |
| } | |
| } | |
| // 5.4 | |
| // We don't implement CORs so we don't need to loop over the responses, yay! | |
| // 5.5.1 | |
| const responseList = [] | |
| // 5.5.2 | |
| for (const response of responses) { | |
| // 5.5.2.1 | |
| const responseObject = new Response(response.body?.source ?? null) | |
| const body = responseObject[kState].body | |
| responseObject[kState] = response | |
| responseObject[kState].body = body | |
| responseObject[kHeaders][kHeadersList] = response.headersList | |
| responseObject[kHeaders][kGuard] = 'immutable' | |
| responseList.push(responseObject) | |
| } | |
| // 6. | |
| return Object.freeze(responseList) | |
| } | |
| async add (request) { | |
| webidl.brandCheck(this, Cache) | |
| webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' }) | |
| request = webidl.converters.RequestInfo(request) | |
| // 1. | |
| const requests = [request] | |
| // 2. | |
| const responseArrayPromise = this.addAll(requests) | |
| // 3. | |
| return await responseArrayPromise | |
| } | |
| async addAll (requests) { | |
| webidl.brandCheck(this, Cache) | |
| webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' }) | |
| requests = webidl.converters['sequence<RequestInfo>'](requests) | |
| // 1. | |
| const responsePromises = [] | |
| // 2. | |
| const requestList = [] | |
| // 3. | |
| for (const request of requests) { | |
| if (typeof request === 'string') { | |
| continue | |
| } | |
| // 3.1 | |
| const r = request[kState] | |
| // 3.2 | |
| if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.addAll', | |
| message: 'Expected http/s scheme when method is not GET.' | |
| }) | |
| } | |
| } | |
| // 4. | |
| /** @type {ReturnType<typeof fetching>[]} */ | |
| const fetchControllers = [] | |
| // 5. | |
| for (const request of requests) { | |
| // 5.1 | |
| const r = new Request(request)[kState] | |
| // 5.2 | |
| if (!urlIsHttpHttpsScheme(r.url)) { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.addAll', | |
| message: 'Expected http/s scheme.' | |
| }) | |
| } | |
| // 5.4 | |
| r.initiator = 'fetch' | |
| r.destination = 'subresource' | |
| // 5.5 | |
| requestList.push(r) | |
| // 5.6 | |
| const responsePromise = createDeferredPromise() | |
| // 5.7 | |
| fetchControllers.push(fetching({ | |
| request: r, | |
| dispatcher: getGlobalDispatcher(), | |
| processResponse (response) { | |
| // 1. | |
| if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { | |
| responsePromise.reject(webidl.errors.exception({ | |
| header: 'Cache.addAll', | |
| message: 'Received an invalid status code or the request failed.' | |
| })) | |
| } else if (response.headersList.contains('vary')) { // 2. | |
| // 2.1 | |
| const fieldValues = getFieldValues(response.headersList.get('vary')) | |
| // 2.2 | |
| for (const fieldValue of fieldValues) { | |
| // 2.2.1 | |
| if (fieldValue === '*') { | |
| responsePromise.reject(webidl.errors.exception({ | |
| header: 'Cache.addAll', | |
| message: 'invalid vary field value' | |
| })) | |
| for (const controller of fetchControllers) { | |
| controller.abort() | |
| } | |
| return | |
| } | |
| } | |
| } | |
| }, | |
| processResponseEndOfBody (response) { | |
| // 1. | |
| if (response.aborted) { | |
| responsePromise.reject(new DOMException('aborted', 'AbortError')) | |
| return | |
| } | |
| // 2. | |
| responsePromise.resolve(response) | |
| } | |
| })) | |
| // 5.8 | |
| responsePromises.push(responsePromise.promise) | |
| } | |
| // 6. | |
| const p = Promise.all(responsePromises) | |
| // 7. | |
| const responses = await p | |
| // 7.1 | |
| const operations = [] | |
| // 7.2 | |
| let index = 0 | |
| // 7.3 | |
| for (const response of responses) { | |
| // 7.3.1 | |
| /** @type {CacheBatchOperation} */ | |
| const operation = { | |
| type: 'put', // 7.3.2 | |
| request: requestList[index], // 7.3.3 | |
| response // 7.3.4 | |
| } | |
| operations.push(operation) // 7.3.5 | |
| index++ // 7.3.6 | |
| } | |
| // 7.5 | |
| const cacheJobPromise = createDeferredPromise() | |
| // 7.6.1 | |
| let errorData = null | |
| // 7.6.2 | |
| try { | |
| this.#batchCacheOperations(operations) | |
| } catch (e) { | |
| errorData = e | |
| } | |
| // 7.6.3 | |
| queueMicrotask(() => { | |
| // 7.6.3.1 | |
| if (errorData === null) { | |
| cacheJobPromise.resolve(undefined) | |
| } else { | |
| // 7.6.3.2 | |
| cacheJobPromise.reject(errorData) | |
| } | |
| }) | |
| // 7.7 | |
| return cacheJobPromise.promise | |
| } | |
| async put (request, response) { | |
| webidl.brandCheck(this, Cache) | |
| webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' }) | |
| request = webidl.converters.RequestInfo(request) | |
| response = webidl.converters.Response(response) | |
| // 1. | |
| let innerRequest = null | |
| // 2. | |
| if (request instanceof Request) { | |
| innerRequest = request[kState] | |
| } else { // 3. | |
| innerRequest = new Request(request)[kState] | |
| } | |
| // 4. | |
| if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.put', | |
| message: 'Expected an http/s scheme when method is not GET' | |
| }) | |
| } | |
| // 5. | |
| const innerResponse = response[kState] | |
| // 6. | |
| if (innerResponse.status === 206) { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.put', | |
| message: 'Got 206 status' | |
| }) | |
| } | |
| // 7. | |
| if (innerResponse.headersList.contains('vary')) { | |
| // 7.1. | |
| const fieldValues = getFieldValues(innerResponse.headersList.get('vary')) | |
| // 7.2. | |
| for (const fieldValue of fieldValues) { | |
| // 7.2.1 | |
| if (fieldValue === '*') { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.put', | |
| message: 'Got * vary field value' | |
| }) | |
| } | |
| } | |
| } | |
| // 8. | |
| if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.put', | |
| message: 'Response body is locked or disturbed' | |
| }) | |
| } | |
| // 9. | |
| const clonedResponse = cloneResponse(innerResponse) | |
| // 10. | |
| const bodyReadPromise = createDeferredPromise() | |
| // 11. | |
| if (innerResponse.body != null) { | |
| // 11.1 | |
| const stream = innerResponse.body.stream | |
| // 11.2 | |
| const reader = stream.getReader() | |
| // 11.3 | |
| readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject) | |
| } else { | |
| bodyReadPromise.resolve(undefined) | |
| } | |
| // 12. | |
| /** @type {CacheBatchOperation[]} */ | |
| const operations = [] | |
| // 13. | |
| /** @type {CacheBatchOperation} */ | |
| const operation = { | |
| type: 'put', // 14. | |
| request: innerRequest, // 15. | |
| response: clonedResponse // 16. | |
| } | |
| // 17. | |
| operations.push(operation) | |
| // 19. | |
| const bytes = await bodyReadPromise.promise | |
| if (clonedResponse.body != null) { | |
| clonedResponse.body.source = bytes | |
| } | |
| // 19.1 | |
| const cacheJobPromise = createDeferredPromise() | |
| // 19.2.1 | |
| let errorData = null | |
| // 19.2.2 | |
| try { | |
| this.#batchCacheOperations(operations) | |
| } catch (e) { | |
| errorData = e | |
| } | |
| // 19.2.3 | |
| queueMicrotask(() => { | |
| // 19.2.3.1 | |
| if (errorData === null) { | |
| cacheJobPromise.resolve() | |
| } else { // 19.2.3.2 | |
| cacheJobPromise.reject(errorData) | |
| } | |
| }) | |
| return cacheJobPromise.promise | |
| } | |
| async delete (request, options = {}) { | |
| webidl.brandCheck(this, Cache) | |
| webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' }) | |
| request = webidl.converters.RequestInfo(request) | |
| options = webidl.converters.CacheQueryOptions(options) | |
| /** | |
| * @type {Request} | |
| */ | |
| let r = null | |
| if (request instanceof Request) { | |
| r = request[kState] | |
| if (r.method !== 'GET' && !options.ignoreMethod) { | |
| return false | |
| } | |
| } else { | |
| assert(typeof request === 'string') | |
| r = new Request(request)[kState] | |
| } | |
| /** @type {CacheBatchOperation[]} */ | |
| const operations = [] | |
| /** @type {CacheBatchOperation} */ | |
| const operation = { | |
| type: 'delete', | |
| request: r, | |
| options | |
| } | |
| operations.push(operation) | |
| const cacheJobPromise = createDeferredPromise() | |
| let errorData = null | |
| let requestResponses | |
| try { | |
| requestResponses = this.#batchCacheOperations(operations) | |
| } catch (e) { | |
| errorData = e | |
| } | |
| queueMicrotask(() => { | |
| if (errorData === null) { | |
| cacheJobPromise.resolve(!!requestResponses?.length) | |
| } else { | |
| cacheJobPromise.reject(errorData) | |
| } | |
| }) | |
| return cacheJobPromise.promise | |
| } | |
| /** | |
| * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys | |
| * @param {any} request | |
| * @param {import('../../types/cache').CacheQueryOptions} options | |
| * @returns {readonly Request[]} | |
| */ | |
| async keys (request = undefined, options = {}) { | |
| webidl.brandCheck(this, Cache) | |
| if (request !== undefined) request = webidl.converters.RequestInfo(request) | |
| options = webidl.converters.CacheQueryOptions(options) | |
| // 1. | |
| let r = null | |
| // 2. | |
| if (request !== undefined) { | |
| // 2.1 | |
| if (request instanceof Request) { | |
| // 2.1.1 | |
| r = request[kState] | |
| // 2.1.2 | |
| if (r.method !== 'GET' && !options.ignoreMethod) { | |
| return [] | |
| } | |
| } else if (typeof request === 'string') { // 2.2 | |
| r = new Request(request)[kState] | |
| } | |
| } | |
| // 4. | |
| const promise = createDeferredPromise() | |
| // 5. | |
| // 5.1 | |
| const requests = [] | |
| // 5.2 | |
| if (request === undefined) { | |
| // 5.2.1 | |
| for (const requestResponse of this.#relevantRequestResponseList) { | |
| // 5.2.1.1 | |
| requests.push(requestResponse[0]) | |
| } | |
| } else { // 5.3 | |
| // 5.3.1 | |
| const requestResponses = this.#queryCache(r, options) | |
| // 5.3.2 | |
| for (const requestResponse of requestResponses) { | |
| // 5.3.2.1 | |
| requests.push(requestResponse[0]) | |
| } | |
| } | |
| // 5.4 | |
| queueMicrotask(() => { | |
| // 5.4.1 | |
| const requestList = [] | |
| // 5.4.2 | |
| for (const request of requests) { | |
| const requestObject = new Request('https://a') | |
| requestObject[kState] = request | |
| requestObject[kHeaders][kHeadersList] = request.headersList | |
| requestObject[kHeaders][kGuard] = 'immutable' | |
| requestObject[kRealm] = request.client | |
| // 5.4.2.1 | |
| requestList.push(requestObject) | |
| } | |
| // 5.4.3 | |
| promise.resolve(Object.freeze(requestList)) | |
| }) | |
| return promise.promise | |
| } | |
| /** | |
| * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm | |
| * @param {CacheBatchOperation[]} operations | |
| * @returns {requestResponseList} | |
| */ | |
| #batchCacheOperations (operations) { | |
| // 1. | |
| const cache = this.#relevantRequestResponseList | |
| // 2. | |
| const backupCache = [...cache] | |
| // 3. | |
| const addedItems = [] | |
| // 4.1 | |
| const resultList = [] | |
| try { | |
| // 4.2 | |
| for (const operation of operations) { | |
| // 4.2.1 | |
| if (operation.type !== 'delete' && operation.type !== 'put') { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.#batchCacheOperations', | |
| message: 'operation type does not match "delete" or "put"' | |
| }) | |
| } | |
| // 4.2.2 | |
| if (operation.type === 'delete' && operation.response != null) { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.#batchCacheOperations', | |
| message: 'delete operation should not have an associated response' | |
| }) | |
| } | |
| // 4.2.3 | |
| if (this.#queryCache(operation.request, operation.options, addedItems).length) { | |
| throw new DOMException('???', 'InvalidStateError') | |
| } | |
| // 4.2.4 | |
| let requestResponses | |
| // 4.2.5 | |
| if (operation.type === 'delete') { | |
| // 4.2.5.1 | |
| requestResponses = this.#queryCache(operation.request, operation.options) | |
| // TODO: the spec is wrong, this is needed to pass WPTs | |
| if (requestResponses.length === 0) { | |
| return [] | |
| } | |
| // 4.2.5.2 | |
| for (const requestResponse of requestResponses) { | |
| const idx = cache.indexOf(requestResponse) | |
| assert(idx !== -1) | |
| // 4.2.5.2.1 | |
| cache.splice(idx, 1) | |
| } | |
| } else if (operation.type === 'put') { // 4.2.6 | |
| // 4.2.6.1 | |
| if (operation.response == null) { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.#batchCacheOperations', | |
| message: 'put operation should have an associated response' | |
| }) | |
| } | |
| // 4.2.6.2 | |
| const r = operation.request | |
| // 4.2.6.3 | |
| if (!urlIsHttpHttpsScheme(r.url)) { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.#batchCacheOperations', | |
| message: 'expected http or https scheme' | |
| }) | |
| } | |
| // 4.2.6.4 | |
| if (r.method !== 'GET') { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.#batchCacheOperations', | |
| message: 'not get method' | |
| }) | |
| } | |
| // 4.2.6.5 | |
| if (operation.options != null) { | |
| throw webidl.errors.exception({ | |
| header: 'Cache.#batchCacheOperations', | |
| message: 'options must not be defined' | |
| }) | |
| } | |
| // 4.2.6.6 | |
| requestResponses = this.#queryCache(operation.request) | |
| // 4.2.6.7 | |
| for (const requestResponse of requestResponses) { | |
| const idx = cache.indexOf(requestResponse) | |
| assert(idx !== -1) | |
| // 4.2.6.7.1 | |
| cache.splice(idx, 1) | |
| } | |
| // 4.2.6.8 | |
| cache.push([operation.request, operation.response]) | |
| // 4.2.6.10 | |
| addedItems.push([operation.request, operation.response]) | |
| } | |
| // 4.2.7 | |
| resultList.push([operation.request, operation.response]) | |
| } | |
| // 4.3 | |
| return resultList | |
| } catch (e) { // 5. | |
| // 5.1 | |
| this.#relevantRequestResponseList.length = 0 | |
| // 5.2 | |
| this.#relevantRequestResponseList = backupCache | |
| // 5.3 | |
| throw e | |
| } | |
| } | |
| /** | |
| * @see https://w3c.github.io/ServiceWorker/#query-cache | |
| * @param {any} requestQuery | |
| * @param {import('../../types/cache').CacheQueryOptions} options | |
| * @param {requestResponseList} targetStorage | |
| * @returns {requestResponseList} | |
| */ | |
| #queryCache (requestQuery, options, targetStorage) { | |
| /** @type {requestResponseList} */ | |
| const resultList = [] | |
| const storage = targetStorage ?? this.#relevantRequestResponseList | |
| for (const requestResponse of storage) { | |
| const [cachedRequest, cachedResponse] = requestResponse | |
| if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { | |
| resultList.push(requestResponse) | |
| } | |
| } | |
| return resultList | |
| } | |
| /** | |
| * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm | |
| * @param {any} requestQuery | |
| * @param {any} request | |
| * @param {any | null} response | |
| * @param {import('../../types/cache').CacheQueryOptions | undefined} options | |
| * @returns {boolean} | |
| */ | |
| #requestMatchesCachedItem (requestQuery, request, response = null, options) { | |
| // if (options?.ignoreMethod === false && request.method === 'GET') { | |
| // return false | |
| // } | |
| const queryURL = new URL(requestQuery.url) | |
| const cachedURL = new URL(request.url) | |
| if (options?.ignoreSearch) { | |
| cachedURL.search = '' | |
| queryURL.search = '' | |
| } | |
| if (!urlEquals(queryURL, cachedURL, true)) { | |
| return false | |
| } | |
| if ( | |
| response == null || | |
| options?.ignoreVary || | |
| !response.headersList.contains('vary') | |
| ) { | |
| return true | |
| } | |
| const fieldValues = getFieldValues(response.headersList.get('vary')) | |
| for (const fieldValue of fieldValues) { | |
| if (fieldValue === '*') { | |
| return false | |
| } | |
| const requestValue = request.headersList.get(fieldValue) | |
| const queryValue = requestQuery.headersList.get(fieldValue) | |
| // If one has the header and the other doesn't, or one has | |
| // a different value than the other, return false | |
| if (requestValue !== queryValue) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| } | |
| Object.defineProperties(Cache.prototype, { | |
| [Symbol.toStringTag]: { | |
| value: 'Cache', | |
| configurable: true | |
| }, | |
| match: kEnumerableProperty, | |
| matchAll: kEnumerableProperty, | |
| add: kEnumerableProperty, | |
| addAll: kEnumerableProperty, | |
| put: kEnumerableProperty, | |
| delete: kEnumerableProperty, | |
| keys: kEnumerableProperty | |
| }) | |
| const cacheQueryOptionConverters = [ | |
| { | |
| key: 'ignoreSearch', | |
| converter: webidl.converters.boolean, | |
| defaultValue: false | |
| }, | |
| { | |
| key: 'ignoreMethod', | |
| converter: webidl.converters.boolean, | |
| defaultValue: false | |
| }, | |
| { | |
| key: 'ignoreVary', | |
| converter: webidl.converters.boolean, | |
| defaultValue: false | |
| } | |
| ] | |
| webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters) | |
| webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ | |
| ...cacheQueryOptionConverters, | |
| { | |
| key: 'cacheName', | |
| converter: webidl.converters.DOMString | |
| } | |
| ]) | |
| webidl.converters.Response = webidl.interfaceConverter(Response) | |
| webidl.converters['sequence<RequestInfo>'] = webidl.sequenceConverter( | |
| webidl.converters.RequestInfo | |
| ) | |
| module.exports = { | |
| Cache | |
| } | |