Skip to content

Commit

Permalink
feat: support request cache control directives
Browse files Browse the repository at this point in the history
Signed-off-by: flakey5 <[email protected]>
  • Loading branch information
flakey5 committed Oct 15, 2024
1 parent b6952b4 commit 9a15ece
Showing 1 changed file with 99 additions and 3 deletions.
102 changes: 99 additions & 3 deletions lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,74 @@ 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 { UNSAFE_METHODS, assertCacheStoreType } = require('../util/cache.js')
const {
UNSAFE_METHODS,
assertCacheStoreType,
parseCacheControlHeader
} = require('../util/cache.js')

const AGE_HEADER = Buffer.from('age')

/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
*/
function sendGatewayTimeout (handler) {
const ac = new AbortController()
const signal = ac.signal

try {
if (typeof handler.onConnect === 'function') {
handler.onConnect(ac.abort)
signal.throwIfAborted()
}

if (typeof handler.onHeaders === 'function') {
handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
signal.throwIfAborted()
}

if (typeof handler.onComplete === 'function') {
handler.onComplete([])
}
} catch (err) {
if (typeof handler.onError === 'function') {
handler.onError(err)
}
}
}

/**
* @param {number} now
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} value
* @param {number} age
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
*/
function needsRevalidation (now, value, age, cacheControlDirectives) {
if (cacheControlDirectives?.['no-cache']) {
// Always revalidate requests with the no-cache parameter
return true
}

if (now > value.staleAt) {
// Response is stale
if (cacheControlDirectives?.['max-stale']) {
// https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
const gracePeriod = value.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
const gracePeriod = age + (cacheControlDirectives['min-fresh'] * 1000)
return (now - value.staleAt) > gracePeriod
}

return false
}

/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions | undefined} globalOpts
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
Expand Down Expand Up @@ -45,9 +109,25 @@ module.exports = globalOpts => {
return dispatch(opts, handler)
}

const requestCacheControl = opts.headers?.['cache-control']
? parseCacheControlHeader(opts.headers['cache-control'])
: undefined

if (requestCacheControl?.['no-store']) {
return dispatch(opts, handler)
}

const stream = globalOpts.store.createReadStream(opts)
if (!stream) {
// Request isn't cached

if (requestCacheControl?.['only-if-cached']) {
// We only want cached responses
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
sendGatewayTimeout(handler)
return true
}

return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
}

Expand Down Expand Up @@ -128,19 +208,35 @@ module.exports = globalOpts => {
const handleStream = (stream) => {
if (!stream) {
// Request isn't cached

if (requestCacheControl?.['only-if-cached']) {
// We only want cached responses
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
sendGatewayTimeout(handler)
return
}

return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
}

const { value } = stream

const now = Date.now()
const age = Math.round((now - value.cachedAt) / 1000)
if (requestCacheControl?.['max-age'] && age >= requestCacheControl['max-age']) {
// Response is considered expired for this specific request
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
dispatch(opts, handler)
return
}

// Dump body on error
if (util.isStream(opts.body)) {
opts.body?.on('error', () => {}).resume()
}

// Check if the response is stale
const now = Date.now()
if (now >= value.staleAt) {
if (needsRevalidation(now, value, age, requestCacheControl)) {
if (now >= value.deleteAt) {
// Safety check in case the store gave us a response that should've been
// deleted already
Expand Down

0 comments on commit 9a15ece

Please sign in to comment.