/* Copyright 2018 Google LLC Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at https://opensource.org/licenses/MIT. */ import { assert } from "../../utils/assert.js"; import { getFriendlyURL } from "../../utils/getFriendlyURL.js"; import { logger } from "../../utils/logger.js"; import { SerwistError } from "../../utils/SerwistError.js"; export interface CacheableResponseOptions { /** * One or more HTTP status codes that a response can have to be considered cacheable. */ statuses?: number[]; /** * A mapping of header names and expected values that a response can have and be * considered cacheable. If multiple headers are provided, only one needs to be present. */ headers?: HeadersInit; } /** * Allows you to set up rules determining what status codes and/or headers need * to be present in order for a [response](https://developer.mozilla.org/en-US/docs/Web/API/Response) * to be considered cacheable. */ export class CacheableResponse { private readonly _statuses?: CacheableResponseOptions["statuses"]; private readonly _headers?: Headers; /** * To construct a new `CacheableResponse` instance you must provide at least * one of the `config` properties. * * If both `statuses` and `headers` are specified, then both conditions must * be met for the response to be considered cacheable. * * @param config */ constructor(config: CacheableResponseOptions = {}) { if (process.env.NODE_ENV !== "production") { if (!(config.statuses || config.headers)) { throw new SerwistError("statuses-or-headers-required", { moduleName: "serwist", className: "CacheableResponse", funcName: "constructor", }); } if (config.statuses) { assert!.isArray(config.statuses, { moduleName: "serwist", className: "CacheableResponse", funcName: "constructor", paramName: "config.statuses", }); } if (config.headers) { assert!.isType(config.headers, "object", { moduleName: "serwist", className: "CacheableResponse", funcName: "constructor", paramName: "config.headers", }); } } this._statuses = config.statuses; if (config.headers) { this._headers = new Headers(config.headers); } } /** * Checks a response to see whether it's cacheable or not. * * @param response The response whose cacheability is being * checked. * @returns `true` if the response is cacheable, and `false` * otherwise. */ isResponseCacheable(response: Response): boolean { if (process.env.NODE_ENV !== "production") { assert!.isInstance(response, Response, { moduleName: "serwist", className: "CacheableResponse", funcName: "isResponseCacheable", paramName: "response", }); } let cacheable = true; if (this._statuses) { cacheable = this._statuses.includes(response.status); } if (this._headers && cacheable) { for (const [headerName, headerValue] of this._headers.entries()) { if (response.headers.get(headerName) !== headerValue) { cacheable = false; break; } } } if (process.env.NODE_ENV !== "production") { if (!cacheable) { logger.groupCollapsed( `The request for '${getFriendlyURL(response.url)}' returned a response that does not meet the criteria for being cached.`, ); logger.groupCollapsed("View cacheability criteria here."); logger.log(`Cacheable statuses: ${JSON.stringify(this._statuses)}`); logger.log(`Cacheable headers: ${JSON.stringify(this._headers, null, 2)}`); logger.groupEnd(); const logFriendlyHeaders: { [key: string]: string } = {}; response.headers.forEach((value, key) => { logFriendlyHeaders[key] = value; }); logger.groupCollapsed("View response status and headers here."); logger.log(`Response status: ${response.status}`); logger.log(`Response headers: ${JSON.stringify(logFriendlyHeaders, null, 2)}`); logger.groupEnd(); logger.groupCollapsed("View full response details here."); logger.log(response.headers); logger.log(response); logger.groupEnd(); logger.groupEnd(); } } return cacheable; } }