cache.js 20 KB

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