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 cleanup #2082

Merged
merged 3 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/api/CacheStorage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# CacheStorage

Undici exposes a W3C spec-compliant implementation of [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) and [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache).

## Opening a Cache

Undici exports a top-level CacheStorage instance. You can open a new Cache, or duplicate a Cache with an existing name, by using `CacheStorage.prototype.open`. If you open a Cache with the same name as an already-existing Cache, its list of cached Responses will be shared between both instances.

```mjs
import { caches } from 'undici'

const cache_1 = await caches.open('v1')
const cache_2 = await caches.open('v1')

// Although .open() creates a new instance,
assert(cache_1 !== cache_2)
// The same Response is matched in both.
assert.deepStrictEqual(await cache_1.match('/req'), await cache_2.match('/req'))
```

## Deleting a Cache

If a Cache is deleted, the cached Responses/Requests can still be used.

```mjs
const response = await cache_1.match('/req')
await caches.delete('v1')

await response.text() // the Response's body
```
2 changes: 2 additions & 0 deletions docs/api/Fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Documentation and examples can be found on [MDN](https://developer.mozilla.org/e

This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/File)

In Node versions v18.13.0 and above and v19.2.0 and above, undici will default to using Node's [File](https://nodejs.org/api/buffer.html#class-file) class. In versions where it's not available, it will default to the undici one.

## FormData

This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
Expand Down
1 change: 1 addition & 0 deletions docsify/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* [Diagnostics Channel Support](/docs/api/DiagnosticsChannel.md "Diagnostics Channel Support")
* [WebSocket](/docs/api/WebSocket.md "Undici API - WebSocket")
* [MIME Type Parsing](/docs/api/ContentType.md "Undici API - MIME Type Parsing")
* [CacheStorage](/docs/api/CacheStorage.md "Undici API - CacheStorage")
* Best Practices
* [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy")
* [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate")
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './types/formdata'
export * from './types/diagnostics-channel'
export * from './types/websocket'
export * from './types/content-type'
export * from './types/cache'
export { Interceptable } from './types/mock-interceptor'

export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler }
Expand Down Expand Up @@ -52,4 +53,5 @@ declare namespace Undici {
var MockAgent: typeof import('./types/mock-agent').default;
var mockErrors: typeof import('./types/mock-errors').default;
var fetch: typeof import('./types/fetch').fetch;
var caches: typeof import('./types/cache').caches;
}
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) {

module.exports.setGlobalOrigin = setGlobalOrigin
module.exports.getGlobalOrigin = getGlobalOrigin

const { CacheStorage } = require('./lib/cache/cachestorage')
const { kConstruct } = require('./lib/cache/symbols')

// Cache & CacheStorage are tightly coupled with fetch. Even if it may run
// in an older version of Node, it doesn't have any use without fetch.
module.exports.caches = new CacheStorage(kConstruct)
}

if (util.nodeMajor >= 16) {
Expand Down
12 changes: 11 additions & 1 deletion lib/cache/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ Object.defineProperties(Cache.prototype, {
keys: kEnumerableProperty
})

webidl.converters.CacheQueryOptions = webidl.dictionaryConverter([
const cacheQueryOptionConverters = [
{
key: 'ignoreSearch',
converter: webidl.converters.boolean,
Expand All @@ -821,6 +821,16 @@ webidl.converters.CacheQueryOptions = webidl.dictionaryConverter([
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)
Expand Down
26 changes: 25 additions & 1 deletion lib/cache/cachestorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,31 @@ class CacheStorage {
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.match' })

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

// 1.
if (options.cacheName != null) {
// 1.1.1.1
if (this.#caches.has(options.cacheName)) {
// 1.1.1.1.1
const cacheList = this.#caches.get(options.cacheName)
const cache = new Cache(kConstruct, cacheList)

return await cache.match(request, options)
}
} else { // 2.
// 2.2
for (const cacheList of this.#caches.values()) {
const cache = new Cache(kConstruct, cacheList)

// 2.2.1.2
const response = await cache.match(request, options)

if (response !== undefined) {
return response
}
}
}
}

/**
Expand Down
39 changes: 39 additions & 0 deletions test/types/cache-storage.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expectAssignable } from 'tsd'
import {
caches,
CacheStorage,
Cache,
CacheQueryOptions,
MultiCacheQueryOptions,
RequestInfo,
Request,
Response
} from '../..'

declare const response: Response
declare const request: Request
declare const options: RequestInfo
declare const cache: Cache

expectAssignable<CacheStorage>(caches)
expectAssignable<MultiCacheQueryOptions>({})
expectAssignable<MultiCacheQueryOptions>({ cacheName: 'v1' })
expectAssignable<MultiCacheQueryOptions>({ ignoreMethod: false, ignoreSearch: true })

expectAssignable<CacheQueryOptions>({})
expectAssignable<CacheQueryOptions>({ ignoreVary: false, ignoreMethod: true, ignoreSearch: true })

expectAssignable<Promise<Cache>>(caches.open('v1'))
expectAssignable<Promise<Response | undefined>>(caches.match(options))
expectAssignable<Promise<Response | undefined>>(caches.match(request))
expectAssignable<Promise<boolean>>(caches.has('v1'))
expectAssignable<Promise<boolean>>(caches.delete('v1'))
expectAssignable<Promise<string[]>>(caches.keys())

expectAssignable<Promise<Response | undefined>>(cache.match(options))
expectAssignable<Promise<readonly Response[]>>(cache.matchAll('v1'))
expectAssignable<Promise<boolean>>(cache.delete('v1'))
expectAssignable<Promise<readonly Request[]>>(cache.keys())
expectAssignable<Promise<undefined>>(cache.add(options))
expectAssignable<Promise<undefined>>(cache.addAll([options]))
expectAssignable<Promise<undefined>>(cache.put(options, response))
4 changes: 0 additions & 4 deletions test/wpt/status/service-workers/cache-storage.status.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"skip": true,
"note": "navigator is not defined"
},
"cache-storage-match.https.any.js": {
"skip": true,
"note": "CacheStorage.prototype.match isnt implemented yet"
},
"cache-put.https.any.js": {
"note": "probably can be fixed",
"fail": [
Expand Down
7 changes: 7 additions & 0 deletions types/cache.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export interface CacheStorage {
keys (): Promise<string[]>
}

declare const CacheStorage: {
prototype: CacheStorage
new(): CacheStorage
}

export interface Cache {
match (request: RequestInfo, options?: CacheQueryOptions): Promise<Response | undefined>,
matchAll (request?: RequestInfo, options?: CacheQueryOptions): Promise<readonly Response[]>,
Expand All @@ -27,3 +32,5 @@ export interface CacheQueryOptions {
export interface MultiCacheQueryOptions extends CacheQueryOptions {
cacheName?: string
}

export declare const caches: CacheStorage