Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
1'use strict';
2
3/**
4 * @typedef {Object} HttpRequest
5 * @property {Record<string, string>} headers - Request headers
6 * @property {string} [method] - HTTP method
7 * @property {string} [url] - Request URL
8 */
9
10/**
11 * @typedef {Object} HttpResponse
12 * @property {Record<string, string>} headers - Response headers
13 * @property {number} [status] - HTTP status code
14 */
15
16/**
17 * Set of default cacheable status codes per RFC 7231 section 6.1.
18 * @type {Set<number>}
19 */
20const statusCodeCacheableByDefault = new Set([
21    200,
22    203,
23    204,
24    206,
25    300,
26    301,
27    308,
28    404,
29    405,
30    410,
31    414,
32    501,
33]);
34
35/**
36 * Set of HTTP status codes that the cache implementation understands.
37 * Note: This implementation does not understand partial responses (206).
38 * @type {Set<number>}
39 */
40const understoodStatuses = new Set([
41    200,
42    203,
43    204,
44    300,
45    301,
46    302,
47    303,
48    307,
49    308,
50    404,
51    405,
52    410,
53    414,
54    501,
55]);
56
57/**
58 * Set of HTTP error status codes.
59 * @type {Set<number>}
60 */
61const errorStatusCodes = new Set([
62    500,
63    502,
64    503,
65    504,
66]);
67
68/**
69 * Object representing hop-by-hop headers that should be removed.
70 * @type {Record<string, boolean>}
71 */
72const hopByHopHeaders = {
73    date: true, // included, because we add Age update Date
74    connection: true,
75    'keep-alive': true,
76    'proxy-authenticate': true,
77    'proxy-authorization': true,
78    te: true,
79    trailer: true,
80    'transfer-encoding': true,
81    upgrade: true,
82};
83
84/**
85 * Headers that are excluded from revalidation update.
86 * @type {Record<string, boolean>}
87 */
88const excludedFromRevalidationUpdate = {
89    // Since the old body is reused, it doesn't make sense to change properties of the body
90    'content-length': true,
91    'content-encoding': true,
92    'transfer-encoding': true,
93    'content-range': true,
94};
95
96/**
97 * Converts a string to a number or returns zero if the conversion fails.
98 * @param {string} s - The string to convert.
99 * @returns {number} The parsed number or 0.
100 */
101function toNumberOrZero(s) {
102    const n = parseInt(s, 10);
103    return isFinite(n) ? n : 0;
104}
105
106/**
107 * Determines if the given response is an error response.
108 * Implements RFC 5861 behavior.
109 * @param {HttpResponse|undefined} response - The HTTP response object.
110 * @returns {boolean} true if the response is an error or undefined, false otherwise.
111 */
112function isErrorResponse(response) {
113    // consider undefined response as faulty
114    if (!response) {
115        return true;
116    }
117    return errorStatusCodes.has(response.status);
118}
119
120/**
121 * Parses a Cache-Control header string into an object.
122 * @param {string} [header] - The Cache-Control header value.
123 * @returns {Record<string, string|boolean>} An object representing Cache-Control directives.
124 */
125function parseCacheControl(header) {
126    /** @type {Record<string, string|boolean>} */
127    const cc = {};
128    if (!header) return cc;
129
130    // TODO: When there is more than one value present for a given directive (e.g., two Expires header fields, multiple Cache-Control: max-age directives),
131    // the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale
132    const parts = header.trim().split(/,/);
133    for (const part of parts) {
134        const [k, v] = part.split(/=/, 2);
135        cc[k.trim()] = v === undefined ? true : v.trim().replace(/^"|"$/g, '');
136    }
137
138    return cc;
139}
140
141/**
142 * Formats a Cache-Control directives object into a header string.
143 * @param {Record<string, string|boolean>} cc - The Cache-Control directives.
144 * @returns {string|undefined} A formatted Cache-Control header string or undefined if empty.
145 */
146function formatCacheControl(cc) {
147    let parts = [];
148    for (const k in cc) {
149        const v = cc[k];
150        parts.push(v === true ? k : k + '=' + v);
151    }
152    if (!parts.length) {
153        return undefined;
154    }
155    return parts.join(', ');
156}
157
158module.exports = class CachePolicy {
159    /**
160     * Creates a new CachePolicy instance.
161     * @param {HttpRequest} req - Incoming client request.
162     * @param {HttpResponse} res - Received server response.
163     * @param {Object} [options={}] - Configuration options.
164     * @param {boolean} [options.shared=true] - Is the cache shared (a public proxy)? `false` for personal browser caches.
165     * @param {number} [options.cacheHeuristic=0.1] - Fallback heuristic (age fraction) for cache duration.
166     * @param {number} [options.immutableMinTimeToLive=86400000] - Minimum TTL for immutable responses in milliseconds.
167     * @param {boolean} [options.ignoreCargoCult=false] - Detect nonsense cache headers, and override them.
168     * @param {any} [options._fromObject] - Internal parameter for deserialization. Do not use.
169     */
170    constructor(
171        req,
172        res,
173        {
174            shared,
175            cacheHeuristic,
176            immutableMinTimeToLive,
177            ignoreCargoCult,
178            _fromObject,
179        } = {}
180    ) {
181        if (_fromObject) {
182            this._fromObject(_fromObject);
183            return;
184        }
185
186        if (!res || !res.headers) {
187            throw Error('Response headers missing');
188        }
189        this._assertRequestHasHeaders(req);
190
191        /** @type {number} Timestamp when the response was received */
192        this._responseTime = this.now();
193        /** @type {boolean} Indicates if the cache is shared */
194        this._isShared = shared !== false;
195        /** @type {boolean} Indicates if legacy cargo cult directives should be ignored */
196        this._ignoreCargoCult = !!ignoreCargoCult;
197        /** @type {number} Heuristic cache fraction */
198        this._cacheHeuristic =
199            undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
200        /** @type {number} Minimum TTL for immutable responses in ms */
201        this._immutableMinTtl =
202            undefined !== immutableMinTimeToLive
203                ? immutableMinTimeToLive
204                : 24 * 3600 * 1000;
205
206        /** @type {number} HTTP status code */
207        this._status = 'status' in res ? res.status : 200;
208        /** @type {Record<string, string>} Response headers */
209        this._resHeaders = res.headers;
210        /** @type {Record<string, string|boolean>} Parsed Cache-Control directives from response */
211        this._rescc = parseCacheControl(res.headers['cache-control']);
212        /** @type {string} HTTP method (e.g., GET, POST) */
213        this._method = 'method' in req ? req.method : 'GET';
214        /** @type {string} Request URL */
215        this._url = req.url;
216        /** @type {string} Host header from the request */
217        this._host = req.headers.host;
218        /** @type {boolean} Whether the request does not include an Authorization header */
219        this._noAuthorization = !req.headers.authorization;
220        /** @type {Record<string, string>|null} Request headers used for Vary matching */
221        this._reqHeaders = res.headers.vary ? req.headers : null; // Don't keep all request headers if they won't be used
222        /** @type {Record<string, string|boolean>} Parsed Cache-Control directives from request */
223        this._reqcc = parseCacheControl(req.headers['cache-control']);
224
225        // Assume that if someone uses legacy, non-standard uncecessary options they don't understand caching,
226        // so there's no point stricly adhering to the blindly copy&pasted directives.
227        if (
228            this._ignoreCargoCult &&
229            'pre-check' in this._rescc &&
230            'post-check' in this._rescc
231        ) {
232            delete this._rescc['pre-check'];
233            delete this._rescc['post-check'];
234            delete this._rescc['no-cache'];
235            delete this._rescc['no-store'];
236            delete this._rescc['must-revalidate'];
237            this._resHeaders = Object.assign({}, this._resHeaders, {
238                'cache-control': formatCacheControl(this._rescc),
239            });
240            delete this._resHeaders.expires;
241            delete this._resHeaders.pragma;
242        }
243
244        // When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive
245        // as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1).
246        if (
247            res.headers['cache-control'] == null &&
248            /no-cache/.test(res.headers.pragma)
249        ) {
250            this._rescc['no-cache'] = true;
251        }
252    }
253
254    /**
255     * You can monkey-patch it for testing.
256     * @returns {number} Current time in milliseconds.
257     */
258    now() {
259        return Date.now();
260    }
261
262    /**
263     * Determines if the response is storable in a cache.
264     * @returns {boolean} `false` if can never be cached.
265     */
266    storable() {
267        // The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it.
268        return !!(
269            !this._reqcc['no-store'] &&
270            // A cache MUST NOT store a response to any request, unless:
271            // The request method is understood by the cache and defined as being cacheable, and
272            ('GET' === this._method ||
273                'HEAD' === this._method ||
274                ('POST' === this._method && this._hasExplicitExpiration())) &&
275            // the response status code is understood by the cache, and
276            understoodStatuses.has(this._status) &&
277            // the "no-store" cache directive does not appear in request or response header fields, and
278            !this._rescc['no-store'] &&
279            // the "private" response directive does not appear in the response, if the cache is shared, and
280            (!this._isShared || !this._rescc.private) &&
281            // the Authorization header field does not appear in the request, if the cache is shared,
282            (!this._isShared ||
283                this._noAuthorization ||
284                this._allowsStoringAuthenticated()) &&
285            // the response either:
286            // contains an Expires header field, or
287            (this._resHeaders.expires ||
288                // contains a max-age response directive, or
289                // contains a s-maxage response directive and the cache is shared, or
290                // contains a public response directive.
291                this._rescc['max-age'] ||
292                (this._isShared && this._rescc['s-maxage']) ||
293                this._rescc.public ||
294                // has a status code that is defined as cacheable by default
295                statusCodeCacheableByDefault.has(this._status))
296        );
297    }
298
299    /**
300     * @returns {boolean} true if expiration is explicitly defined.
301     */
302    _hasExplicitExpiration() {
303        // 4.2.1 Calculating Freshness Lifetime
304        return !!(
305            (this._isShared && this._rescc['s-maxage']) ||
306            this._rescc['max-age'] ||
307            this._resHeaders.expires
308        );
309    }
310
311    /**
312     * @param {HttpRequest} req - a request
313     * @throws {Error} if the headers are missing.
314     */
315    _assertRequestHasHeaders(req) {
316        if (!req || !req.headers) {
317            throw Error('Request headers missing');
318        }
319    }
320
321    /**
322     * Checks if the request matches the cache and can be satisfied from the cache immediately,
323     * without having to make a request to the server.
324     *
325     * This doesn't support `stale-while-revalidate`. See `evaluateRequest()` for a more complete solution.
326     *
327     * @param {HttpRequest} req - The new incoming HTTP request.
328     * @returns {boolean} `true`` if the cached response used to construct this cache policy satisfies the request without revalidation.
329     */
330    satisfiesWithoutRevalidation(req) {
331        const result = this.evaluateRequest(req);
332        return !result.revalidation;
333    }
334
335    /**
336     * @param {{headers: Record<string, string>, synchronous: boolean}|undefined} revalidation - Revalidation information, if any.
337     * @returns {{response: {headers: Record<string, string>}, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object with a cached response headers and revalidation info.
338     */
339    _evaluateRequestHitResult(revalidation) {
340        return {
341            response: {
342                headers: this.responseHeaders(),
343            },
344            revalidation,
345        };
346    }
347
348    /**
349     * @param {HttpRequest} request - new incoming
350     * @param {boolean} synchronous - whether revalidation must be synchronous (not s-w-r).
351     * @returns {{headers: Record<string, string>, synchronous: boolean}} An object with revalidation headers and a synchronous flag.
352     */
353    _evaluateRequestRevalidation(request, synchronous) {
354        return {
355            synchronous,
356            headers: this.revalidationHeaders(request),
357        };
358    }
359
360    /**
361     * @param {HttpRequest} request - new incoming
362     * @returns {{response: undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}}} An object indicating no cached response and revalidation details.
363     */
364    _evaluateRequestMissResult(request) {
365        return {
366            response: undefined,
367            revalidation: this._evaluateRequestRevalidation(request, true),
368        };
369    }
370
371    /**
372     * Checks if the given request matches this cache entry, and how the cache can be used to satisfy it. Returns an object with:
373     *
374     * ```
375     * {
376     *     // If defined, you must send a request to the server.
377     *     revalidation: {
378     *         headers: {}, // HTTP headers to use when sending the revalidation response
379     *         // If true, you MUST wait for a response from the server before using the cache
380     *         // If false, this is stale-while-revalidate. The cache is stale, but you can use it while you update it asynchronously.
381     *         synchronous: bool,
382     *     },
383     *     // If defined, you can use this cached response.
384     *     response: {
385     *         headers: {}, // Updated cached HTTP headers you must use when responding to the client
386     *     },
387     * }
388     * ```
389     * @param {HttpRequest} req - new incoming HTTP request
390     * @returns {{response: {headers: Record<string, string>}|undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object containing keys:
391     *   - revalidation: { headers: Record<string, string>, synchronous: boolean } Set if you should send this to the origin server
392     *   - response: { headers: Record<string, string> } Set if you can respond to the client with these cached headers
393     */
394    evaluateRequest(req) {
395        this._assertRequestHasHeaders(req);
396
397        // In all circumstances, a cache MUST NOT ignore the must-revalidate directive
398        if (this._rescc['must-revalidate']) {
399            return this._evaluateRequestMissResult(req);
400        }
401
402        if (!this._requestMatches(req, false)) {
403            return this._evaluateRequestMissResult(req);
404        }
405
406        // When presented with a request, a cache MUST NOT reuse a stored response, unless:
407        // the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive,
408        // unless the stored response is successfully validated (Section 4.3), and
409        const requestCC = parseCacheControl(req.headers['cache-control']);
410
411        if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
412            return this._evaluateRequestMissResult(req);
413        }
414
415        if (requestCC['max-age'] && this.age() > toNumberOrZero(requestCC['max-age'])) {
416            return this._evaluateRequestMissResult(req);
417        }
418
419        if (requestCC['min-fresh'] && this.maxAge() - this.age() < toNumberOrZero(requestCC['min-fresh'])) {
420            return this._evaluateRequestMissResult(req);
421        }
422
423        // the stored response is either:
424        // fresh, or allowed to be served stale
425        if (this.stale()) {
426            // If a value is present, then the client is willing to accept a response that has
427            // exceeded its freshness lifetime by no more than the specified number of seconds
428            const allowsStaleWithoutRevalidation = 'max-stale' in requestCC &&
429                (true === requestCC['max-stale'] || requestCC['max-stale'] > this.age() - this.maxAge());
430
431            if (allowsStaleWithoutRevalidation) {
432                return this._evaluateRequestHitResult(undefined);
433            }
434
435            if (this.useStaleWhileRevalidate()) {
436                return this._evaluateRequestHitResult(this._evaluateRequestRevalidation(req, false));
437            }
438
439            return this._evaluateRequestMissResult(req);
440        }
441
442        return this._evaluateRequestHitResult(undefined);
443    }
444
445    /**
446     * @param {HttpRequest} req - check if this is for the same cache entry
447     * @param {boolean} allowHeadMethod - allow a HEAD method to match.
448     * @returns {boolean} `true` if the request matches.
449     */
450    _requestMatches(req, allowHeadMethod) {
451        // The presented effective request URI and that of the stored response match, and
452        return !!(
453            (!this._url || this._url === req.url) &&
454            this._host === req.headers.host &&
455            // the request method associated with the stored response allows it to be used for the presented request, and
456            (!req.method ||
457                this._method === req.method ||
458                (allowHeadMethod && 'HEAD' === req.method)) &&
459            // selecting header fields nominated by the stored response (if any) match those presented, and
460            this._varyMatches(req)
461        );
462    }
463
464    /**
465     * Determines whether storing authenticated responses is allowed.
466     * @returns {boolean} `true` if allowed.
467     */
468    _allowsStoringAuthenticated() {
469        // following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
470        return !!(
471            this._rescc['must-revalidate'] ||
472            this._rescc.public ||
473            this._rescc['s-maxage']
474        );
475    }
476
477    /**
478     * Checks whether the Vary header in the response matches the new request.
479     * @param {HttpRequest} req - incoming HTTP request
480     * @returns {boolean} `true` if the vary headers match.
481     */
482    _varyMatches(req) {
483        if (!this._resHeaders.vary) {
484            return true;
485        }
486
487        // A Vary header field-value of "*" always fails to match
488        if (this._resHeaders.vary === '*') {
489            return false;
490        }
491
492        const fields = this._resHeaders.vary
493            .trim()
494            .toLowerCase()
495            .split(/\s*,\s*/);
496        for (const name of fields) {
497            if (req.headers[name] !== this._reqHeaders[name]) return false;
498        }
499        return true;
500    }
501
502    /**
503     * Creates a copy of the given headers without any hop-by-hop headers.
504     * @param {Record<string, string>} inHeaders - old headers from the cached response
505     * @returns {Record<string, string>} A new headers object without hop-by-hop headers.
506     */
507    _copyWithoutHopByHopHeaders(inHeaders) {
508        /** @type {Record<string, string>} */
509        const headers = {};
510        for (const name in inHeaders) {
511            if (hopByHopHeaders[name]) continue;
512            headers[name] = inHeaders[name];
513        }
514        // 9.1.  Connection
515        if (inHeaders.connection) {
516            const tokens = inHeaders.connection.trim().split(/\s*,\s*/);
517            for (const name of tokens) {
518                delete headers[name];
519            }
520        }
521        if (headers.warning) {
522            const warnings = headers.warning.split(/,/).filter(warning => {
523                return !/^\s*1[0-9][0-9]/.test(warning);
524            });
525            if (!warnings.length) {
526                delete headers.warning;
527            } else {
528                headers.warning = warnings.join(',').trim();
529            }
530        }
531        return headers;
532    }
533
534    /**
535     * Returns the response headers adjusted for serving the cached response.
536     * Removes hop-by-hop headers and updates the Age and Date headers.
537     * @returns {Record<string, string>} The adjusted response headers.
538     */
539    responseHeaders() {
540        const headers = this._copyWithoutHopByHopHeaders(this._resHeaders);
541        const age = this.age();
542
543        // A cache SHOULD generate 113 warning if it heuristically chose a freshness
544        // lifetime greater than 24 hours and the response's age is greater than 24 hours.
545        if (
546            age > 3600 * 24 &&
547            !this._hasExplicitExpiration() &&
548            this.maxAge() > 3600 * 24
549        ) {
550            headers.warning =
551                (headers.warning ? `${headers.warning}, ` : '') +
552                '113 - "rfc7234 5.5.4"';
553        }
554        headers.age = `${Math.round(age)}`;
555        headers.date = new Date(this.now()).toUTCString();
556        return headers;
557    }
558
559    /**
560     * Returns the Date header value from the response or the current time if invalid.
561     * @returns {number} Timestamp (in milliseconds) representing the Date header or response time.
562     */
563    date() {
564        const serverDate = Date.parse(this._resHeaders.date);
565        if (isFinite(serverDate)) {
566            return serverDate;
567        }
568        return this._responseTime;
569    }
570
571    /**
572     * Value of the Age header, in seconds, updated for the current time.
573     * May be fractional.
574     * @returns {number} The age in seconds.
575     */
576    age() {
577        let age = this._ageValue();
578
579        const residentTime = (this.now() - this._responseTime) / 1000;
580        return age + residentTime;
581    }
582
583    /**
584     * @returns {number} The Age header value as a number.
585     */
586    _ageValue() {
587        return toNumberOrZero(this._resHeaders.age);
588    }
589
590    /**
591     * Possibly outdated value of applicable max-age (or heuristic equivalent) in seconds.
592     * This counts since response's `Date`.
593     *
594     * For an up-to-date value, see `timeToLive()`.
595     *
596     * Returns the maximum age (freshness lifetime) of the response in seconds.
597     * @returns {number} The max-age value in seconds.
598     */
599    maxAge() {
600        if (!this.storable() || this._rescc['no-cache']) {
601            return 0;
602        }
603
604        // Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default
605        // so this implementation requires explicit opt-in via public header
606        if (
607            this._isShared &&
608            (this._resHeaders['set-cookie'] &&
609                !this._rescc.public &&
610                !this._rescc.immutable)
611        ) {
612            return 0;
613        }
614
615        if (this._resHeaders.vary === '*') {
616            return 0;
617        }
618
619        if (this._isShared) {
620            if (this._rescc['proxy-revalidate']) {
621                return 0;
622            }
623            // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.
624            if (this._rescc['s-maxage']) {
625                return toNumberOrZero(this._rescc['s-maxage']);
626            }
627        }
628
629        // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.
630        if (this._rescc['max-age']) {
631            return toNumberOrZero(this._rescc['max-age']);
632        }
633
634        const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0;
635
636        const serverDate = this.date();
637        if (this._resHeaders.expires) {
638            const expires = Date.parse(this._resHeaders.expires);
639            // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").
640            if (Number.isNaN(expires) || expires < serverDate) {
641                return 0;
642            }
643            return Math.max(defaultMinTtl, (expires - serverDate) / 1000);
644        }
645
646        if (this._resHeaders['last-modified']) {
647            const lastModified = Date.parse(this._resHeaders['last-modified']);
648            if (isFinite(lastModified) && serverDate > lastModified) {
649                return Math.max(
650                    defaultMinTtl,
651                    ((serverDate - lastModified) / 1000) * this._cacheHeuristic
652                );
653            }
654        }
655
656        return defaultMinTtl;
657    }
658
659    /**
660     * Remaining time this cache entry may be useful for, in *milliseconds*.
661     * You can use this as an expiration time for your cache storage.
662     *
663     * Prefer this method over `maxAge()`, because it includes other factors like `age` and `stale-while-revalidate`.
664     * @returns {number} Time-to-live in milliseconds.
665     */
666    timeToLive() {
667        const age = this.maxAge() - this.age();
668        const staleIfErrorAge = age + toNumberOrZero(this._rescc['stale-if-error']);
669        const staleWhileRevalidateAge = age + toNumberOrZero(this._rescc['stale-while-revalidate']);
670        return Math.round(Math.max(0, age, staleIfErrorAge, staleWhileRevalidateAge) * 1000);
671    }
672
673    /**
674     * If true, this cache entry is past its expiration date.
675     * Note that stale cache may be useful sometimes, see `evaluateRequest()`.
676     * @returns {boolean} `false` doesn't mean it's fresh nor usable
677     */
678    stale() {
679        return this.maxAge() <= this.age();
680    }
681
682    /**
683     * @returns {boolean} `true` if `stale-if-error` condition allows use of a stale response.
684     */
685    _useStaleIfError() {
686        return this.maxAge() + toNumberOrZero(this._rescc['stale-if-error']) > this.age();
687    }
688
689    /** See `evaluateRequest()` for a more complete solution
690     * @returns {boolean} `true` if `stale-while-revalidate` is currently allowed.
691     */
692    useStaleWhileRevalidate() {
693        const swr = toNumberOrZero(this._rescc['stale-while-revalidate']);
694        return swr > 0 && this.maxAge() + swr > this.age();
695    }
696
697    /**
698     * Creates a `CachePolicy` instance from a serialized object.
699     * @param {Object} obj - The serialized object.
700     * @returns {CachePolicy} A new CachePolicy instance.
701     */
702    static fromObject(obj) {
703        return new this(undefined, undefined, { _fromObject: obj });
704    }
705
706    /**
707     * @param {any} obj - The serialized object.
708     * @throws {Error} If already initialized or if the object is invalid.
709     */
710    _fromObject(obj) {
711        if (this._responseTime) throw Error('Reinitialized');
712        if (!obj || obj.v !== 1) throw Error('Invalid serialization');
713
714        this._responseTime = obj.t;
715        this._isShared = obj.sh;
716        this._cacheHeuristic = obj.ch;
717        this._immutableMinTtl =
718            obj.imm !== undefined ? obj.imm : 24 * 3600 * 1000;
719        this._ignoreCargoCult = !!obj.icc;
720        this._status = obj.st;
721        this._resHeaders = obj.resh;
722        this._rescc = obj.rescc;
723        this._method = obj.m;
724        this._url = obj.u;
725        this._host = obj.h;
726        this._noAuthorization = obj.a;
727        this._reqHeaders = obj.reqh;
728        this._reqcc = obj.reqcc;
729    }
730
731    /**
732     * Serializes the `CachePolicy` instance into a JSON-serializable object.
733     * @returns {Object} The serialized object.
734     */
735    toObject() {
736        return {
737            v: 1,
738            t: this._responseTime,
739            sh: this._isShared,
740            ch: this._cacheHeuristic,
741            imm: this._immutableMinTtl,
742            icc: this._ignoreCargoCult,
743            st: this._status,
744            resh: this._resHeaders,
745            rescc: this._rescc,
746            m: this._method,
747            u: this._url,
748            h: this._host,
749            a: this._noAuthorization,
750            reqh: this._reqHeaders,
751            reqcc: this._reqcc,
752        };
753    }
754
755    /**
756     * Headers for sending to the origin server to revalidate stale response.
757     * Allows server to return 304 to allow reuse of the previous response.
758     *
759     * Hop by hop headers are always stripped.
760     * Revalidation headers may be added or removed, depending on request.
761     * @param {HttpRequest} incomingReq - The incoming HTTP request.
762     * @returns {Record<string, string>} The headers for the revalidation request.
763     */
764    revalidationHeaders(incomingReq) {
765        this._assertRequestHasHeaders(incomingReq);
766        const headers = this._copyWithoutHopByHopHeaders(incomingReq.headers);
767
768        // This implementation does not understand range requests
769        delete headers['if-range'];
770
771        if (!this._requestMatches(incomingReq, true) || !this.storable()) {
772            // revalidation allowed via HEAD
773            // not for the same resource, or wasn't allowed to be cached anyway
774            delete headers['if-none-match'];
775            delete headers['if-modified-since'];
776            return headers;
777        }
778
779        /* MUST send that entity-tag in any cache validation request (using If-Match or If-None-Match) if an entity-tag has been provided by the origin server. */
780        if (this._resHeaders.etag) {
781            headers['if-none-match'] = headers['if-none-match']
782                ? `${headers['if-none-match']}, ${this._resHeaders.etag}`
783                : this._resHeaders.etag;
784        }
785
786        // Clients MAY issue simple (non-subrange) GET requests with either weak validators or strong validators. Clients MUST NOT use weak validators in other forms of request.
787        const forbidsWeakValidators =
788            headers['accept-ranges'] ||
789            headers['if-match'] ||
790            headers['if-unmodified-since'] ||
791            (this._method && this._method != 'GET');
792
793        /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server.
794        Note: This implementation does not understand partial responses (206) */
795        if (forbidsWeakValidators) {
796            delete headers['if-modified-since'];
797
798            if (headers['if-none-match']) {
799                const etags = headers['if-none-match']
800                    .split(/,/)
801                    .filter(etag => {
802                        return !/^\s*W\//.test(etag);
803                    });
804                if (!etags.length) {
805                    delete headers['if-none-match'];
806                } else {
807                    headers['if-none-match'] = etags.join(',').trim();
808                }
809            }
810        } else if (
811            this._resHeaders['last-modified'] &&
812            !headers['if-modified-since']
813        ) {
814            headers['if-modified-since'] = this._resHeaders['last-modified'];
815        }
816
817        return headers;
818    }
819
820    /**
821     * Creates new CachePolicy with information combined from the previews response,
822     * and the new revalidation response.
823     *
824     * Returns {policy, modified} where modified is a boolean indicating
825     * whether the response body has been modified, and old cached body can't be used.
826     *
827     * @param {HttpRequest} request - The latest HTTP request asking for the cached entry.
828     * @param {HttpResponse} response - The latest revalidation HTTP response from the origin server.
829     * @returns {{policy: CachePolicy, modified: boolean, matches: boolean}} The updated policy and modification status.
830     * @throws {Error} If the response headers are missing.
831     */
832    revalidatedPolicy(request, response) {
833        this._assertRequestHasHeaders(request);
834
835        if (this._useStaleIfError() && isErrorResponse(response)) {
836          return {
837              policy: this,
838              modified: false,
839              matches: true,
840          };
841        }
842
843        if (!response || !response.headers) {
844            throw Error('Response headers missing');
845        }
846
847        // These aren't going to be supported exactly, since one CachePolicy object
848        // doesn't know about all the other cached objects.
849        let matches = false;
850        if (response.status !== undefined && response.status != 304) {
851            matches = false;
852        } else if (
853            response.headers.etag &&
854            !/^\s*W\//.test(response.headers.etag)
855        ) {
856            // "All of the stored responses with the same strong validator are selected.
857            // If none of the stored responses contain the same strong validator,
858            // then the cache MUST NOT use the new response to update any stored responses."
859            matches =
860                this._resHeaders.etag &&
861                this._resHeaders.etag.replace(/^\s*W\//, '') ===
862                    response.headers.etag;
863        } else if (this._resHeaders.etag && response.headers.etag) {
864            // "If the new response contains a weak validator and that validator corresponds
865            // to one of the cache's stored responses,
866            // then the most recent of those matching stored responses is selected for update."
867            matches =
868                this._resHeaders.etag.replace(/^\s*W\//, '') ===
869                response.headers.etag.replace(/^\s*W\//, '');
870        } else if (this._resHeaders['last-modified']) {
871            matches =
872                this._resHeaders['last-modified'] ===
873                response.headers['last-modified'];
874        } else {
875            // If the new response does not include any form of validator (such as in the case where
876            // a client generates an If-Modified-Since request from a source other than the Last-Modified
877            // response header field), and there is only one stored response, and that stored response also
878            // lacks a validator, then that stored response is selected for update.
879            if (
880                !this._resHeaders.etag &&
881                !this._resHeaders['last-modified'] &&
882                !response.headers.etag &&
883                !response.headers['last-modified']
884            ) {
885                matches = true;
886            }
887        }
888
889        const optionsCopy = {
890            shared: this._isShared,
891            cacheHeuristic: this._cacheHeuristic,
892            immutableMinTimeToLive: this._immutableMinTtl,
893            ignoreCargoCult: this._ignoreCargoCult,
894        };
895
896        if (!matches) {
897            return {
898                policy: new this.constructor(request, response, optionsCopy),
899                // Client receiving 304 without body, even if it's invalid/mismatched has no option
900                // but to reuse a cached body. We don't have a good way to tell clients to do
901                // error recovery in such case.
902                modified: response.status != 304,
903                matches: false,
904            };
905        }
906
907        // use other header fields provided in the 304 (Not Modified) response to replace all instances
908        // of the corresponding header fields in the stored response.
909        const headers = {};
910        for (const k in this._resHeaders) {
911            headers[k] =
912                k in response.headers && !excludedFromRevalidationUpdate[k]
913                    ? response.headers[k]
914                    : this._resHeaders[k];
915        }
916
917        const newResponse = Object.assign({}, response, {
918            status: this._status,
919            method: this._method,
920            headers,
921        });
922        return {
923            policy: new this.constructor(request, newResponse, optionsCopy),
924            modified: false,
925            matches: true,
926        };
927    }
928};
929 
codekingpro/portable-devtools · Team Ai