Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache storage #2076

Merged
merged 17 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
689 changes: 689 additions & 0 deletions lib/cache/cache.js

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions lib/cache/cachestorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use strict'

const { kConstruct } = require('./symbols')
const { Cache, getCacheRequestResponseList } = require('./cache')
const { webidl } = require('../fetch/webidl')
const { kEnumerableProperty } = require('../core/util')

class CacheStorage {
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map
* @type {Map<string, import('./cache').requestResponseList}
*/
#caches = new Map()

constructor () {
if (arguments[0] !== kConstruct) {
webidl.illegalConstructor()
}
}

async match (request, options = {}) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.match' })

request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options)
}

/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-has
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async has (cacheName) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' })

cacheName = webidl.converters.DOMString(cacheName)

// 2.1.1
// 2.2
return this.#caches.has(cacheName)
}

/**
* @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open
* @param {string} cacheName
* @returns {Promise<Cache>}
*/
async open (cacheName) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' })

cacheName = webidl.converters.DOMString(cacheName)

// 2.1
if (this.#caches.has(cacheName)) {
// await caches.open('v1') !== await caches.open('v1')

// 2.1.1
const cache = this.#caches.get(cacheName)
const list = getCacheRequestResponseList(cache)

// 2.1.1.1
return new Cache(kConstruct, list)
}

// 2.2
const cache = []

// 2.3
this.#caches.set(cacheName, cache)

// 2.4
return new Cache(kConstruct, cache)
}

/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-delete
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async delete (cacheName) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' })

cacheName = webidl.converters.DOMString(cacheName)

// 1.
// 2.
const cacheExists = this.#caches.has(cacheName)

// 2.1
if (!cacheExists) {
return false
}

// 2.3.1
this.#caches.delete(cacheName)

// 2.3.2
return true
}

/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
* @returns {string[]}
*/
async keys () {
webidl.brandCheck(this, CacheStorage)

// 2.1
const keys = this.#caches.keys()

// 2.2
return [...keys]
}
}

Object.defineProperties(CacheStorage.prototype, {
[Symbol.toStringTag]: {
value: 'CacheStorage',
configurable: true
},
match: kEnumerableProperty,
has: kEnumerableProperty,
open: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})

module.exports = {
CacheStorage
}
5 changes: 5 additions & 0 deletions lib/cache/symbols.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict'

module.exports = {
kConstruct: Symbol('constructable')
}
22 changes: 22 additions & 0 deletions lib/cache/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict'

const { URLSerializer } = require('../fetch/dataURL')

/**
* @see https://url.spec.whatwg.org/#concept-url-equals
* @param {URL} A
* @param {URL} B
* @param {boolean | undefined} excludeFragment
* @returns {boolean}
*/
function urlEquals (A, B, excludeFragment = false) {
const serializedA = URLSerializer(A, excludeFragment)

const serializedB = URLSerializer(B, excludeFragment)

return serializedA === serializedB
}

module.exports = {
urlEquals
}
3 changes: 2 additions & 1 deletion lib/fetch/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -569,5 +569,6 @@ module.exports = {
makeResponse,
makeAppropriateNetworkError,
filterResponse,
Response
Response,
cloneResponse
}
3 changes: 2 additions & 1 deletion lib/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1028,5 +1028,6 @@ module.exports = {
isomorphicDecode,
urlIsLocal,
urlHasHttpsScheme,
urlIsHttpHttpsScheme
urlIsHttpHttpsScheme,
readAllBytes
}
7 changes: 7 additions & 0 deletions lib/fetch/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ webidl.argumentLengthCheck = function ({ length }, min, ctx) {
}
}

webidl.illegalConstructor = function () {
throw webidl.errors.exception({
header: 'TypeError',
message: 'Illegal constructor'
})
}

// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
webidl.util.Type = function (V) {
switch (typeof V) {
Expand Down
15 changes: 15 additions & 0 deletions test/wpt/runner/worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
} from '../../../index.js'
import { CloseEvent } from '../../../lib/websocket/events.js'
import { WebSocket } from '../../../lib/websocket/websocket.js'
import { Cache } from '../../../lib/cache/cache.js'
import { CacheStorage } from '../../../lib/cache/cachestorage.js'
import { kConstruct } from '../../../lib/cache/symbols.js'

const { initScripts, meta, test, url, path } = workerData

Expand Down Expand Up @@ -74,6 +77,18 @@ Object.defineProperties(globalThis, {
...globalPropertyDescriptors,
// See https://github.com/nodejs/node/pull/45659
value: buffer.Blob
},
caches: {
...globalPropertyDescriptors,
value: new CacheStorage(kConstruct)
},
Cache: {
...globalPropertyDescriptors,
value: Cache
},
CacheStorage: {
...globalPropertyDescriptors,
value: CacheStorage
}
})

Expand Down
26 changes: 26 additions & 0 deletions test/wpt/start-cacheStorage.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { WPTRunner } from './runner/runner.mjs'
import { join } from 'path'
import { fileURLToPath } from 'url'
import { fork } from 'child_process'
import { on } from 'events'

const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'))

const child = fork(serverPath, [], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
})

child.on('exit', (code) => process.exit(code))

for await (const [message] of on(child, 'message')) {
if (message.server) {
const runner = new WPTRunner('service-workers/cache-storage', message.server)
runner.run()

runner.once('completion', () => {
if (child.connected) {
child.send('shutdown')
}
})
}
}
7 changes: 7 additions & 0 deletions test/wpt/status/service-workers/cache-storage.status.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"cache-storage": {
"cache-abort.https.any.js": {
"skip": true
}
}
}
6 changes: 6 additions & 0 deletions test/wpt/tests/service-workers/META.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
spec: https://w3c.github.io/ServiceWorker/
suggested_reviewers:
- asutherland
- mkruisselbrink
- mattto
- wanderview
3 changes: 3 additions & 0 deletions test/wpt/tests/service-workers/cache-storage/META.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
suggested_reviewers:
- inexorabletash
- wanderview
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// META: title=Cache Storage: Abort
// META: global=window,worker
// META: script=./resources/test-helpers.js
// META: script=/common/utils.js
// META: timeout=long

// We perform the same tests on put, add, addAll. Parameterise the tests to
// reduce repetition.
const methodsToTest = {
put: async (cache, request) => {
const response = await fetch(request);
return cache.put(request, response);
},
add: async (cache, request) => cache.add(request),
addAll: async (cache, request) => cache.addAll([request]),
};

for (const method in methodsToTest) {
const perform = methodsToTest[method];

cache_test(async (cache, test) => {
const controller = new AbortController();
const signal = controller.signal;
controller.abort();
const request = new Request('../resources/simple.txt', { signal });
return promise_rejects_dom(test, 'AbortError', perform(cache, request),
`${method} should reject`);
}, `${method}() on an already-aborted request should reject with AbortError`);

cache_test(async (cache, test) => {
const controller = new AbortController();
const signal = controller.signal;
const request = new Request('../resources/simple.txt', { signal });
const promise = perform(cache, request);
controller.abort();
return promise_rejects_dom(test, 'AbortError', promise,
`${method} should reject`);
}, `${method}() synchronously followed by abort should reject with ` +
`AbortError`);

cache_test(async (cache, test) => {
const controller = new AbortController();
const signal = controller.signal;
const stateKey = token();
const abortKey = token();
const request = new Request(
`../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`,
{ signal });

const promise = perform(cache, request);

// Wait for the server to start sending the response body.
let opened = false;
do {
// Normally only one fetch to 'stash-take' is needed, but the fetches
// will be served in reverse order sometimes
// (i.e., 'stash-take' gets served before 'infinite-slow-response').

const response =
await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`);
const body = await response.json();
if (body === 'open') opened = true;
} while (!opened);

// Sadly the above loop cannot guarantee that the browser has started
// processing the response body. This delay is needed to make the test
// failures non-flaky in Chrome version 66. My deepest apologies.
await new Promise(resolve => setTimeout(resolve, 250));

controller.abort();

await promise_rejects_dom(test, 'AbortError', promise,
`${method} should reject`);

// infinite-slow-response.py doesn't know when to stop.
return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`);
}, `${method}() followed by abort after headers received should reject ` +
`with AbortError`);
}

done();
Loading