cache.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. 'use strict'
  2. const { kConstruct } = require('./symbols')
  3. const { urlEquals, getFieldValues } = require('./util')
  4. const { kEnumerableProperty, isDisturbed } = require('../../core/util')
  5. const { webidl } = require('../fetch/webidl')
  6. const { Response, cloneResponse, fromInnerResponse } = require('../fetch/response')
  7. const { Request, fromInnerRequest } = require('../fetch/request')
  8. const { kState } = require('../fetch/symbols')
  9. const { fetching } = require('../fetch/index')
  10. const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util')
  11. const assert = require('node:assert')
  12. /**
  13. * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
  14. * @typedef {Object} CacheBatchOperation
  15. * @property {'delete' | 'put'} type
  16. * @property {any} request
  17. * @property {any} response
  18. * @property {import('../../types/cache').CacheQueryOptions} options
  19. */
  20. /**
  21. * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list
  22. * @typedef {[any, any][]} requestResponseList
  23. */
  24. class Cache {
  25. /**
  26. * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list
  27. * @type {requestResponseList}
  28. */
  29. #relevantRequestResponseList
  30. constructor () {
  31. if (arguments[0] !== kConstruct) {
  32. webidl.illegalConstructor()
  33. }
  34. webidl.util.markAsUncloneable(this)
  35. this.#relevantRequestResponseList = arguments[1]
  36. }
  37. async match (request, options = {}) {
  38. webidl.brandCheck(this, Cache)
  39. const prefix = 'Cache.match'
  40. webidl.argumentLengthCheck(arguments, 1, prefix)
  41. request = webidl.converters.RequestInfo(request, prefix, 'request')
  42. options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
  43. const p = this.#internalMatchAll(request, options, 1)
  44. if (p.length === 0) {
  45. return
  46. }
  47. return p[0]
  48. }
  49. async matchAll (request = undefined, options = {}) {
  50. webidl.brandCheck(this, Cache)
  51. const prefix = 'Cache.matchAll'
  52. if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request')
  53. options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
  54. return this.#internalMatchAll(request, options)
  55. }
  56. async add (request) {
  57. webidl.brandCheck(this, Cache)
  58. const prefix = 'Cache.add'
  59. webidl.argumentLengthCheck(arguments, 1, prefix)
  60. request = webidl.converters.RequestInfo(request, prefix, 'request')
  61. // 1.
  62. const requests = [request]
  63. // 2.
  64. const responseArrayPromise = this.addAll(requests)
  65. // 3.
  66. return await responseArrayPromise
  67. }
  68. async addAll (requests) {
  69. webidl.brandCheck(this, Cache)
  70. const prefix = 'Cache.addAll'
  71. webidl.argumentLengthCheck(arguments, 1, prefix)
  72. // 1.
  73. const responsePromises = []
  74. // 2.
  75. const requestList = []
  76. // 3.
  77. for (let request of requests) {
  78. if (request === undefined) {
  79. throw webidl.errors.conversionFailed({
  80. prefix,
  81. argument: 'Argument 1',
  82. types: ['undefined is not allowed']
  83. })
  84. }
  85. request = webidl.converters.RequestInfo(request)
  86. if (typeof request === 'string') {
  87. continue
  88. }
  89. // 3.1
  90. const r = request[kState]
  91. // 3.2
  92. if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') {
  93. throw webidl.errors.exception({
  94. header: prefix,
  95. message: 'Expected http/s scheme when method is not GET.'
  96. })
  97. }
  98. }
  99. // 4.
  100. /** @type {ReturnType<typeof fetching>[]} */
  101. const fetchControllers = []
  102. // 5.
  103. for (const request of requests) {
  104. // 5.1
  105. const r = new Request(request)[kState]
  106. // 5.2
  107. if (!urlIsHttpHttpsScheme(r.url)) {
  108. throw webidl.errors.exception({
  109. header: prefix,
  110. message: 'Expected http/s scheme.'
  111. })
  112. }
  113. // 5.4
  114. r.initiator = 'fetch'
  115. r.destination = 'subresource'
  116. // 5.5
  117. requestList.push(r)
  118. // 5.6
  119. const responsePromise = createDeferredPromise()
  120. // 5.7
  121. fetchControllers.push(fetching({
  122. request: r,
  123. processResponse (response) {
  124. // 1.
  125. if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) {
  126. responsePromise.reject(webidl.errors.exception({
  127. header: 'Cache.addAll',
  128. message: 'Received an invalid status code or the request failed.'
  129. }))
  130. } else if (response.headersList.contains('vary')) { // 2.
  131. // 2.1
  132. const fieldValues = getFieldValues(response.headersList.get('vary'))
  133. // 2.2
  134. for (const fieldValue of fieldValues) {
  135. // 2.2.1
  136. if (fieldValue === '*') {
  137. responsePromise.reject(webidl.errors.exception({
  138. header: 'Cache.addAll',
  139. message: 'invalid vary field value'
  140. }))
  141. for (const controller of fetchControllers) {
  142. controller.abort()
  143. }
  144. return
  145. }
  146. }
  147. }
  148. },
  149. processResponseEndOfBody (response) {
  150. // 1.
  151. if (response.aborted) {
  152. responsePromise.reject(new DOMException('aborted', 'AbortError'))
  153. return
  154. }
  155. // 2.
  156. responsePromise.resolve(response)
  157. }
  158. }))
  159. // 5.8
  160. responsePromises.push(responsePromise.promise)
  161. }
  162. // 6.
  163. const p = Promise.all(responsePromises)
  164. // 7.
  165. const responses = await p
  166. // 7.1
  167. const operations = []
  168. // 7.2
  169. let index = 0
  170. // 7.3
  171. for (const response of responses) {
  172. // 7.3.1
  173. /** @type {CacheBatchOperation} */
  174. const operation = {
  175. type: 'put', // 7.3.2
  176. request: requestList[index], // 7.3.3
  177. response // 7.3.4
  178. }
  179. operations.push(operation) // 7.3.5
  180. index++ // 7.3.6
  181. }
  182. // 7.5
  183. const cacheJobPromise = createDeferredPromise()
  184. // 7.6.1
  185. let errorData = null
  186. // 7.6.2
  187. try {
  188. this.#batchCacheOperations(operations)
  189. } catch (e) {
  190. errorData = e
  191. }
  192. // 7.6.3
  193. queueMicrotask(() => {
  194. // 7.6.3.1
  195. if (errorData === null) {
  196. cacheJobPromise.resolve(undefined)
  197. } else {
  198. // 7.6.3.2
  199. cacheJobPromise.reject(errorData)
  200. }
  201. })
  202. // 7.7
  203. return cacheJobPromise.promise
  204. }
  205. async put (request, response) {
  206. webidl.brandCheck(this, Cache)
  207. const prefix = 'Cache.put'
  208. webidl.argumentLengthCheck(arguments, 2, prefix)
  209. request = webidl.converters.RequestInfo(request, prefix, 'request')
  210. response = webidl.converters.Response(response, prefix, 'response')
  211. // 1.
  212. let innerRequest = null
  213. // 2.
  214. if (request instanceof Request) {
  215. innerRequest = request[kState]
  216. } else { // 3.
  217. innerRequest = new Request(request)[kState]
  218. }
  219. // 4.
  220. if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') {
  221. throw webidl.errors.exception({
  222. header: prefix,
  223. message: 'Expected an http/s scheme when method is not GET'
  224. })
  225. }
  226. // 5.
  227. const innerResponse = response[kState]
  228. // 6.
  229. if (innerResponse.status === 206) {
  230. throw webidl.errors.exception({
  231. header: prefix,
  232. message: 'Got 206 status'
  233. })
  234. }
  235. // 7.
  236. if (innerResponse.headersList.contains('vary')) {
  237. // 7.1.
  238. const fieldValues = getFieldValues(innerResponse.headersList.get('vary'))
  239. // 7.2.
  240. for (const fieldValue of fieldValues) {
  241. // 7.2.1
  242. if (fieldValue === '*') {
  243. throw webidl.errors.exception({
  244. header: prefix,
  245. message: 'Got * vary field value'
  246. })
  247. }
  248. }
  249. }
  250. // 8.
  251. if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) {
  252. throw webidl.errors.exception({
  253. header: prefix,
  254. message: 'Response body is locked or disturbed'
  255. })
  256. }
  257. // 9.
  258. const clonedResponse = cloneResponse(innerResponse)
  259. // 10.
  260. const bodyReadPromise = createDeferredPromise()
  261. // 11.
  262. if (innerResponse.body != null) {
  263. // 11.1
  264. const stream = innerResponse.body.stream
  265. // 11.2
  266. const reader = stream.getReader()
  267. // 11.3
  268. readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject)
  269. } else {
  270. bodyReadPromise.resolve(undefined)
  271. }
  272. // 12.
  273. /** @type {CacheBatchOperation[]} */
  274. const operations = []
  275. // 13.
  276. /** @type {CacheBatchOperation} */
  277. const operation = {
  278. type: 'put', // 14.
  279. request: innerRequest, // 15.
  280. response: clonedResponse // 16.
  281. }
  282. // 17.
  283. operations.push(operation)
  284. // 19.
  285. const bytes = await bodyReadPromise.promise
  286. if (clonedResponse.body != null) {
  287. clonedResponse.body.source = bytes
  288. }
  289. // 19.1
  290. const cacheJobPromise = createDeferredPromise()
  291. // 19.2.1
  292. let errorData = null
  293. // 19.2.2
  294. try {
  295. this.#batchCacheOperations(operations)
  296. } catch (e) {
  297. errorData = e
  298. }
  299. // 19.2.3
  300. queueMicrotask(() => {
  301. // 19.2.3.1
  302. if (errorData === null) {
  303. cacheJobPromise.resolve()
  304. } else { // 19.2.3.2
  305. cacheJobPromise.reject(errorData)
  306. }
  307. })
  308. return cacheJobPromise.promise
  309. }
  310. async delete (request, options = {}) {
  311. webidl.brandCheck(this, Cache)
  312. const prefix = 'Cache.delete'
  313. webidl.argumentLengthCheck(arguments, 1, prefix)
  314. request = webidl.converters.RequestInfo(request, prefix, 'request')
  315. options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
  316. /**
  317. * @type {Request}
  318. */
  319. let r = null
  320. if (request instanceof Request) {
  321. r = request[kState]
  322. if (r.method !== 'GET' && !options.ignoreMethod) {
  323. return false
  324. }
  325. } else {
  326. assert(typeof request === 'string')
  327. r = new Request(request)[kState]
  328. }
  329. /** @type {CacheBatchOperation[]} */
  330. const operations = []
  331. /** @type {CacheBatchOperation} */
  332. const operation = {
  333. type: 'delete',
  334. request: r,
  335. options
  336. }
  337. operations.push(operation)
  338. const cacheJobPromise = createDeferredPromise()
  339. let errorData = null
  340. let requestResponses
  341. try {
  342. requestResponses = this.#batchCacheOperations(operations)
  343. } catch (e) {
  344. errorData = e
  345. }
  346. queueMicrotask(() => {
  347. if (errorData === null) {
  348. cacheJobPromise.resolve(!!requestResponses?.length)
  349. } else {
  350. cacheJobPromise.reject(errorData)
  351. }
  352. })
  353. return cacheJobPromise.promise
  354. }
  355. /**
  356. * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
  357. * @param {any} request
  358. * @param {import('../../types/cache').CacheQueryOptions} options
  359. * @returns {Promise<readonly Request[]>}
  360. */
  361. async keys (request = undefined, options = {}) {
  362. webidl.brandCheck(this, Cache)
  363. const prefix = 'Cache.keys'
  364. if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request')
  365. options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
  366. // 1.
  367. let r = null
  368. // 2.
  369. if (request !== undefined) {
  370. // 2.1
  371. if (request instanceof Request) {
  372. // 2.1.1
  373. r = request[kState]
  374. // 2.1.2
  375. if (r.method !== 'GET' && !options.ignoreMethod) {
  376. return []
  377. }
  378. } else if (typeof request === 'string') { // 2.2
  379. r = new Request(request)[kState]
  380. }
  381. }
  382. // 4.
  383. const promise = createDeferredPromise()
  384. // 5.
  385. // 5.1
  386. const requests = []
  387. // 5.2
  388. if (request === undefined) {
  389. // 5.2.1
  390. for (const requestResponse of this.#relevantRequestResponseList) {
  391. // 5.2.1.1
  392. requests.push(requestResponse[0])
  393. }
  394. } else { // 5.3
  395. // 5.3.1
  396. const requestResponses = this.#queryCache(r, options)
  397. // 5.3.2
  398. for (const requestResponse of requestResponses) {
  399. // 5.3.2.1
  400. requests.push(requestResponse[0])
  401. }
  402. }
  403. // 5.4
  404. queueMicrotask(() => {
  405. // 5.4.1
  406. const requestList = []
  407. // 5.4.2
  408. for (const request of requests) {
  409. const requestObject = fromInnerRequest(
  410. request,
  411. new AbortController().signal,
  412. 'immutable'
  413. )
  414. // 5.4.2.1
  415. requestList.push(requestObject)
  416. }
  417. // 5.4.3
  418. promise.resolve(Object.freeze(requestList))
  419. })
  420. return promise.promise
  421. }
  422. /**
  423. * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
  424. * @param {CacheBatchOperation[]} operations
  425. * @returns {requestResponseList}
  426. */
  427. #batchCacheOperations (operations) {
  428. // 1.
  429. const cache = this.#relevantRequestResponseList
  430. // 2.
  431. const backupCache = [...cache]
  432. // 3.
  433. const addedItems = []
  434. // 4.1
  435. const resultList = []
  436. try {
  437. // 4.2
  438. for (const operation of operations) {
  439. // 4.2.1
  440. if (operation.type !== 'delete' && operation.type !== 'put') {
  441. throw webidl.errors.exception({
  442. header: 'Cache.#batchCacheOperations',
  443. message: 'operation type does not match "delete" or "put"'
  444. })
  445. }
  446. // 4.2.2
  447. if (operation.type === 'delete' && operation.response != null) {
  448. throw webidl.errors.exception({
  449. header: 'Cache.#batchCacheOperations',
  450. message: 'delete operation should not have an associated response'
  451. })
  452. }
  453. // 4.2.3
  454. if (this.#queryCache(operation.request, operation.options, addedItems).length) {
  455. throw new DOMException('???', 'InvalidStateError')
  456. }
  457. // 4.2.4
  458. let requestResponses
  459. // 4.2.5
  460. if (operation.type === 'delete') {
  461. // 4.2.5.1
  462. requestResponses = this.#queryCache(operation.request, operation.options)
  463. // TODO: the spec is wrong, this is needed to pass WPTs
  464. if (requestResponses.length === 0) {
  465. return []
  466. }
  467. // 4.2.5.2
  468. for (const requestResponse of requestResponses) {
  469. const idx = cache.indexOf(requestResponse)
  470. assert(idx !== -1)
  471. // 4.2.5.2.1
  472. cache.splice(idx, 1)
  473. }
  474. } else if (operation.type === 'put') { // 4.2.6
  475. // 4.2.6.1
  476. if (operation.response == null) {
  477. throw webidl.errors.exception({
  478. header: 'Cache.#batchCacheOperations',
  479. message: 'put operation should have an associated response'
  480. })
  481. }
  482. // 4.2.6.2
  483. const r = operation.request
  484. // 4.2.6.3
  485. if (!urlIsHttpHttpsScheme(r.url)) {
  486. throw webidl.errors.exception({
  487. header: 'Cache.#batchCacheOperations',
  488. message: 'expected http or https scheme'
  489. })
  490. }
  491. // 4.2.6.4
  492. if (r.method !== 'GET') {
  493. throw webidl.errors.exception({
  494. header: 'Cache.#batchCacheOperations',
  495. message: 'not get method'
  496. })
  497. }
  498. // 4.2.6.5
  499. if (operation.options != null) {
  500. throw webidl.errors.exception({
  501. header: 'Cache.#batchCacheOperations',
  502. message: 'options must not be defined'
  503. })
  504. }
  505. // 4.2.6.6
  506. requestResponses = this.#queryCache(operation.request)
  507. // 4.2.6.7
  508. for (const requestResponse of requestResponses) {
  509. const idx = cache.indexOf(requestResponse)
  510. assert(idx !== -1)
  511. // 4.2.6.7.1
  512. cache.splice(idx, 1)
  513. }
  514. // 4.2.6.8
  515. cache.push([operation.request, operation.response])
  516. // 4.2.6.10
  517. addedItems.push([operation.request, operation.response])
  518. }
  519. // 4.2.7
  520. resultList.push([operation.request, operation.response])
  521. }
  522. // 4.3
  523. return resultList
  524. } catch (e) { // 5.
  525. // 5.1
  526. this.#relevantRequestResponseList.length = 0
  527. // 5.2
  528. this.#relevantRequestResponseList = backupCache
  529. // 5.3
  530. throw e
  531. }
  532. }
  533. /**
  534. * @see https://w3c.github.io/ServiceWorker/#query-cache
  535. * @param {any} requestQuery
  536. * @param {import('../../types/cache').CacheQueryOptions} options
  537. * @param {requestResponseList} targetStorage
  538. * @returns {requestResponseList}
  539. */
  540. #queryCache (requestQuery, options, targetStorage) {
  541. /** @type {requestResponseList} */
  542. const resultList = []
  543. const storage = targetStorage ?? this.#relevantRequestResponseList
  544. for (const requestResponse of storage) {
  545. const [cachedRequest, cachedResponse] = requestResponse
  546. if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) {
  547. resultList.push(requestResponse)
  548. }
  549. }
  550. return resultList
  551. }
  552. /**
  553. * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
  554. * @param {any} requestQuery
  555. * @param {any} request
  556. * @param {any | null} response
  557. * @param {import('../../types/cache').CacheQueryOptions | undefined} options
  558. * @returns {boolean}
  559. */
  560. #requestMatchesCachedItem (requestQuery, request, response = null, options) {
  561. // if (options?.ignoreMethod === false && request.method === 'GET') {
  562. // return false
  563. // }
  564. const queryURL = new URL(requestQuery.url)
  565. const cachedURL = new URL(request.url)
  566. if (options?.ignoreSearch) {
  567. cachedURL.search = ''
  568. queryURL.search = ''
  569. }
  570. if (!urlEquals(queryURL, cachedURL, true)) {
  571. return false
  572. }
  573. if (
  574. response == null ||
  575. options?.ignoreVary ||
  576. !response.headersList.contains('vary')
  577. ) {
  578. return true
  579. }
  580. const fieldValues = getFieldValues(response.headersList.get('vary'))
  581. for (const fieldValue of fieldValues) {
  582. if (fieldValue === '*') {
  583. return false
  584. }
  585. const requestValue = request.headersList.get(fieldValue)
  586. const queryValue = requestQuery.headersList.get(fieldValue)
  587. // If one has the header and the other doesn't, or one has
  588. // a different value than the other, return false
  589. if (requestValue !== queryValue) {
  590. return false
  591. }
  592. }
  593. return true
  594. }
  595. #internalMatchAll (request, options, maxResponses = Infinity) {
  596. // 1.
  597. let r = null
  598. // 2.
  599. if (request !== undefined) {
  600. if (request instanceof Request) {
  601. // 2.1.1
  602. r = request[kState]
  603. // 2.1.2
  604. if (r.method !== 'GET' && !options.ignoreMethod) {
  605. return []
  606. }
  607. } else if (typeof request === 'string') {
  608. // 2.2.1
  609. r = new Request(request)[kState]
  610. }
  611. }
  612. // 5.
  613. // 5.1
  614. const responses = []
  615. // 5.2
  616. if (request === undefined) {
  617. // 5.2.1
  618. for (const requestResponse of this.#relevantRequestResponseList) {
  619. responses.push(requestResponse[1])
  620. }
  621. } else { // 5.3
  622. // 5.3.1
  623. const requestResponses = this.#queryCache(r, options)
  624. // 5.3.2
  625. for (const requestResponse of requestResponses) {
  626. responses.push(requestResponse[1])
  627. }
  628. }
  629. // 5.4
  630. // We don't implement CORs so we don't need to loop over the responses, yay!
  631. // 5.5.1
  632. const responseList = []
  633. // 5.5.2
  634. for (const response of responses) {
  635. // 5.5.2.1
  636. const responseObject = fromInnerResponse(response, 'immutable')
  637. responseList.push(responseObject.clone())
  638. if (responseList.length >= maxResponses) {
  639. break
  640. }
  641. }
  642. // 6.
  643. return Object.freeze(responseList)
  644. }
  645. }
  646. Object.defineProperties(Cache.prototype, {
  647. [Symbol.toStringTag]: {
  648. value: 'Cache',
  649. configurable: true
  650. },
  651. match: kEnumerableProperty,
  652. matchAll: kEnumerableProperty,
  653. add: kEnumerableProperty,
  654. addAll: kEnumerableProperty,
  655. put: kEnumerableProperty,
  656. delete: kEnumerableProperty,
  657. keys: kEnumerableProperty
  658. })
  659. const cacheQueryOptionConverters = [
  660. {
  661. key: 'ignoreSearch',
  662. converter: webidl.converters.boolean,
  663. defaultValue: () => false
  664. },
  665. {
  666. key: 'ignoreMethod',
  667. converter: webidl.converters.boolean,
  668. defaultValue: () => false
  669. },
  670. {
  671. key: 'ignoreVary',
  672. converter: webidl.converters.boolean,
  673. defaultValue: () => false
  674. }
  675. ]
  676. webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters)
  677. webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([
  678. ...cacheQueryOptionConverters,
  679. {
  680. key: 'cacheName',
  681. converter: webidl.converters.DOMString
  682. }
  683. ])
  684. webidl.converters.Response = webidl.interfaceConverter(Response)
  685. webidl.converters['sequence<RequestInfo>'] = webidl.sequenceConverter(
  686. webidl.converters.RequestInfo
  687. )
  688. module.exports = {
  689. Cache
  690. }