source: node_modules/undici/lib/cache/cache.js@ d24f17c

main
Last change on this file since d24f17c was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

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