aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/undici/lib/interceptor/cache.js
diff options
context:
space:
mode:
Diffstat (limited to 'vanilla/node_modules/undici/lib/interceptor/cache.js')
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/cache.js495
1 files changed, 0 insertions, 495 deletions
diff --git a/vanilla/node_modules/undici/lib/interceptor/cache.js b/vanilla/node_modules/undici/lib/interceptor/cache.js
deleted file mode 100644
index 81d7cb1..0000000
--- a/vanilla/node_modules/undici/lib/interceptor/cache.js
+++ /dev/null
@@ -1,495 +0,0 @@
-'use strict'
-
-const assert = require('node:assert')
-const { Readable } = require('node:stream')
-const util = require('../core/util')
-const CacheHandler = require('../handler/cache-handler')
-const MemoryCacheStore = require('../cache/memory-cache-store')
-const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
-const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js')
-const { AbortError } = require('../core/errors.js')
-
-/**
- * @param {(string | RegExp)[] | undefined} origins
- * @param {string} name
- */
-function assertCacheOrigins (origins, name) {
- if (origins === undefined) return
- if (!Array.isArray(origins)) {
- throw new TypeError(`expected ${name} to be an array or undefined, got ${typeof origins}`)
- }
- for (let i = 0; i < origins.length; i++) {
- const origin = origins[i]
- if (typeof origin !== 'string' && !(origin instanceof RegExp)) {
- throw new TypeError(`expected ${name}[${i}] to be a string or RegExp, got ${typeof origin}`)
- }
- }
-}
-
-const nop = () => {}
-
-/**
- * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
- */
-
-/**
- * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
- * @returns {boolean}
- */
-function needsRevalidation (result, cacheControlDirectives, { headers = {} }) {
- // Always revalidate requests with the no-cache request directive.
- if (cacheControlDirectives?.['no-cache']) {
- return true
- }
-
- // Always revalidate requests with unqualified no-cache response directive.
- if (result.cacheControlDirectives?.['no-cache'] && !Array.isArray(result.cacheControlDirectives['no-cache'])) {
- return true
- }
-
- // Always revalidate requests with conditional headers.
- if (headers['if-modified-since'] || headers['if-none-match']) {
- return true
- }
-
- return false
-}
-
-/**
- * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
- * @returns {boolean}
- */
-function isStale (result, cacheControlDirectives) {
- const now = Date.now()
- if (now > result.staleAt) {
- // Response is stale
- if (cacheControlDirectives?.['max-stale']) {
- // There's a threshold where we can serve stale responses, let's see if
- // we're in it
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
- const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000)
- return now > gracePeriod
- }
-
- return true
- }
-
- if (cacheControlDirectives?.['min-fresh']) {
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
-
- // At this point, staleAt is always > now
- const timeLeftTillStale = result.staleAt - now
- const threshold = cacheControlDirectives['min-fresh'] * 1000
-
- return timeLeftTillStale <= threshold
- }
-
- return false
-}
-
-/**
- * Check if we're within the stale-while-revalidate window for a stale response
- * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
- * @returns {boolean}
- */
-function withinStaleWhileRevalidateWindow (result) {
- const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
- if (!staleWhileRevalidate) {
- return false
- }
-
- const now = Date.now()
- const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000)
- return now <= staleWhileRevalidateExpiry
-}
-
-/**
- * @param {DispatchFn} dispatch
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
- */
-function handleUncachedResponse (
- dispatch,
- globalOpts,
- cacheKey,
- handler,
- opts,
- reqCacheControl
-) {
- if (reqCacheControl?.['only-if-cached']) {
- let aborted = false
- try {
- if (typeof handler.onConnect === 'function') {
- handler.onConnect(() => {
- aborted = true
- })
-
- if (aborted) {
- return
- }
- }
-
- if (typeof handler.onHeaders === 'function') {
- handler.onHeaders(504, [], nop, 'Gateway Timeout')
- if (aborted) {
- return
- }
- }
-
- if (typeof handler.onComplete === 'function') {
- handler.onComplete([])
- }
- } catch (err) {
- if (typeof handler.onError === 'function') {
- handler.onError(err)
- }
- }
-
- return true
- }
-
- return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
-}
-
-/**
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
- * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
- * @param {number} age
- * @param {any} context
- * @param {boolean} isStale
- */
-function sendCachedValue (handler, opts, result, age, context, isStale) {
- // TODO (perf): Readable.from path can be optimized...
- const stream = util.isStream(result.body)
- ? result.body
- : Readable.from(result.body ?? [])
-
- assert(!stream.destroyed, 'stream should not be destroyed')
- assert(!stream.readableDidRead, 'stream should not be readableDidRead')
-
- const controller = {
- resume () {
- stream.resume()
- },
- pause () {
- stream.pause()
- },
- get paused () {
- return stream.isPaused()
- },
- get aborted () {
- return stream.destroyed
- },
- get reason () {
- return stream.errored
- },
- abort (reason) {
- stream.destroy(reason ?? new AbortError())
- }
- }
-
- stream
- .on('error', function (err) {
- if (!this.readableEnded) {
- if (typeof handler.onResponseError === 'function') {
- handler.onResponseError(controller, err)
- } else {
- throw err
- }
- }
- })
- .on('close', function () {
- if (!this.errored) {
- handler.onResponseEnd?.(controller, {})
- }
- })
-
- handler.onRequestStart?.(controller, context)
-
- if (stream.destroyed) {
- return
- }
-
- // Add the age header
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
- const headers = { ...result.headers, age: String(age) }
-
- if (isStale) {
- // Add warning header
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning
- headers.warning = '110 - "response is stale"'
- }
-
- handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
-
- if (opts.method === 'HEAD') {
- stream.destroy()
- } else {
- stream.on('data', function (chunk) {
- handler.onResponseData?.(controller, chunk)
- })
- }
-}
-
-/**
- * @param {DispatchFn} dispatch
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
- * @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result
- */
-function handleResult (
- dispatch,
- globalOpts,
- cacheKey,
- handler,
- opts,
- reqCacheControl,
- result
-) {
- if (!result) {
- return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
- }
-
- const now = Date.now()
- if (now > result.deleteAt) {
- // Response is expired, cache store shouldn't have given this to us
- return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
- }
-
- const age = Math.round((now - result.cachedAt) / 1000)
- if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
- // Response is considered expired for this specific request
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
- return dispatch(opts, handler)
- }
-
- const stale = isStale(result, reqCacheControl)
- const revalidate = needsRevalidation(result, reqCacheControl, opts)
-
- // Check if the response is stale
- if (stale || revalidate) {
- if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
- // If body is a stream we can't revalidate...
- // TODO (fix): This could be less strict...
- return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
- }
-
- // RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
- // and revalidate in background, unless immediate revalidation is necessary
- if (!revalidate && withinStaleWhileRevalidateWindow(result)) {
- // Serve stale response immediately
- sendCachedValue(handler, opts, result, age, null, true)
-
- // Start background revalidation (fire-and-forget)
- queueMicrotask(() => {
- const headers = {
- ...opts.headers,
- 'if-modified-since': new Date(result.cachedAt).toUTCString()
- }
-
- if (result.etag) {
- headers['if-none-match'] = result.etag
- }
-
- if (result.vary) {
- for (const key in result.vary) {
- if (result.vary[key] != null) {
- headers[key] = result.vary[key]
- }
- }
- }
-
- // Background revalidation - update cache if we get new data
- dispatch(
- {
- ...opts,
- headers
- },
- new CacheHandler(globalOpts, cacheKey, {
- // Silent handler that just updates the cache
- onRequestStart () {},
- onRequestUpgrade () {},
- onResponseStart () {},
- onResponseData () {},
- onResponseEnd () {},
- onResponseError () {}
- })
- )
- })
-
- return true
- }
-
- let withinStaleIfErrorThreshold = false
- const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
- if (staleIfErrorExpiry) {
- withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
- }
-
- const headers = {
- ...opts.headers,
- 'if-modified-since': new Date(result.cachedAt).toUTCString()
- }
-
- if (result.etag) {
- headers['if-none-match'] = result.etag
- }
-
- if (result.vary) {
- for (const key in result.vary) {
- if (result.vary[key] != null) {
- headers[key] = result.vary[key]
- }
- }
- }
-
- // We need to revalidate the response
- return dispatch(
- {
- ...opts,
- headers
- },
- new CacheRevalidationHandler(
- (success, context) => {
- if (success) {
- // TODO: successful revalidation should be considered fresh (not give stale warning).
- sendCachedValue(handler, opts, result, age, context, stale)
- } else if (util.isStream(result.body)) {
- result.body.on('error', nop).destroy()
- }
- },
- new CacheHandler(globalOpts, cacheKey, handler),
- withinStaleIfErrorThreshold
- )
- )
- }
-
- // Dump request body.
- if (util.isStream(opts.body)) {
- opts.body.on('error', nop).destroy()
- }
-
- sendCachedValue(handler, opts, result, age, null, false)
-}
-
-/**
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
- * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
- */
-module.exports = (opts = {}) => {
- const {
- store = new MemoryCacheStore(),
- methods = ['GET'],
- cacheByDefault = undefined,
- type = 'shared',
- origins = undefined
- } = opts
-
- if (typeof opts !== 'object' || opts === null) {
- throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
- }
-
- assertCacheStore(store, 'opts.store')
- assertCacheMethods(methods, 'opts.methods')
- assertCacheOrigins(origins, 'opts.origins')
-
- if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
- throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
- }
-
- if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
- throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`)
- }
-
- const globalOpts = {
- store,
- methods,
- cacheByDefault,
- type
- }
-
- const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
-
- return dispatch => {
- return (opts, handler) => {
- if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
- // Not a method we want to cache or we don't have the origin, skip
- return dispatch(opts, handler)
- }
-
- // Check if origin is in whitelist
- if (origins !== undefined) {
- const requestOrigin = opts.origin.toString().toLowerCase()
- let isAllowed = false
-
- for (let i = 0; i < origins.length; i++) {
- const allowed = origins[i]
- if (typeof allowed === 'string') {
- if (allowed.toLowerCase() === requestOrigin) {
- isAllowed = true
- break
- }
- } else if (allowed.test(requestOrigin)) {
- isAllowed = true
- break
- }
- }
-
- if (!isAllowed) {
- return dispatch(opts, handler)
- }
- }
-
- opts = {
- ...opts,
- headers: normalizeHeaders(opts)
- }
-
- const reqCacheControl = opts.headers?.['cache-control']
- ? parseCacheControlHeader(opts.headers['cache-control'])
- : undefined
-
- if (reqCacheControl?.['no-store']) {
- return dispatch(opts, handler)
- }
-
- /**
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
- */
- const cacheKey = makeCacheKey(opts)
- const result = store.get(cacheKey)
-
- if (result && typeof result.then === 'function') {
- return result
- .then(result => handleResult(dispatch,
- globalOpts,
- cacheKey,
- handler,
- opts,
- reqCacheControl,
- result
- ))
- } else {
- return handleResult(
- dispatch,
- globalOpts,
- cacheKey,
- handler,
- opts,
- reqCacheControl,
- result
- )
- }
- }
- }
-}