Skip to content

Commit

Permalink
Cache storage cleanup (nodejs#2082)
Browse files Browse the repository at this point in the history
* cache: add docs, expose, types, type tests

* cache: implement CacheStorage.prototype.match

* fix(cache): use correct webidl converter in CacheStorage.prototype.match
  • Loading branch information
KhafraDev authored and crysmags committed Feb 27, 2024
1 parent e035cc5 commit fec1d67
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 6 deletions.
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

0 comments on commit fec1d67

Please sign in to comment.