Skip to content

Commit

Permalink
cache revalidation handler
Browse files Browse the repository at this point in the history
Signed-off-by: flakey5 <[email protected]>
  • Loading branch information
flakey5 committed Sep 11, 2024
1 parent 9d1cb32 commit e07e3ec
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 79 deletions.
132 changes: 108 additions & 24 deletions lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const {
* store.
*
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-in-caches
*
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers
* @implements {DispatchHandlers}
*/
class CacheHandler extends DecoratorHandler {
/**
Expand Down Expand Up @@ -45,6 +48,16 @@ class CacheHandler extends DecoratorHandler {
this.#handler = handler
}

/**
* @see {DispatchHandlers.onHeaders}
*
* @param {number} statusCode
* @param {Buffer[]} rawHeaders
* @param {() => void} resume
* @param {string} statusMessage
* @param {string[] | undefined} headers
* @returns
*/
onHeaders (
statusCode,
rawHeaders,
Expand All @@ -54,10 +67,9 @@ class CacheHandler extends DecoratorHandler {
) {
const cacheControlHeader = headers['cache-control']
const contentLengthHeader = headers['content-length']
// TODO read cache control directives to see if we can cache requests with
// authorization headers
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-in-caches
if (!cacheControlHeader || !contentLengthHeader || headers['authorization']) {

if (!cacheControlHeader || !contentLengthHeader) {
// Don't have the headers we need, can't cache
return this.#handler.onHeaders(
statusCode,
rawHeaders,
Expand All @@ -66,26 +78,41 @@ class CacheHandler extends DecoratorHandler {
headers
)
}

const contentLength = Number(contentLengthHeader)
// TODO store etags
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
const maxEntrySize = this.#opts.store.maxEntrySize ?? Infinity

if (headers['authorization'] && !canCacheAuthorizationHeader(cacheControlDirectives)) {
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
return this.#handler.onHeaders(
statusCode,
rawHeaders,
resume,
statusMessage,
headers
)
}

const contentLength = Number(contentLengthHeader)
const maxEntrySize = this.#getMaxEntrySize()
if (
!isNaN(contentLength) &&
maxEntrySize > contentLength &&
cacheDirectivesAllowCaching(cacheControlDirectives, headers.vary) &&
[200, 307].includes(statusCode)
) {
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-in-caches
const varyDirectives = headers.vary
? parseVaryHeader(headers.vary)
: undefined

const ttl = determineTtl(headers, cacheControlDirectives) * 1000
if (ttl > 0) {
const strippedHeaders = stripNecessaryHeaders(rawHeaders, headers)
const currentSize = this.#getSizeOfBuffers(rawHeaders) +
(statusMessage?.length ?? 0) +
64

if (ttl > 0 && maxEntrySize > currentSize) {
const strippedHeaders = stripNecessaryHeaders(rawHeaders, headers)
const now = Date.now()

this.#value = {
complete: false,
data: {
Expand All @@ -97,9 +124,7 @@ class CacheHandler extends DecoratorHandler {
},
cachingDirectives: cacheControlDirectives,
vary: varyDirectives,
size: (rawHeaders?.reduce((xs, x) => xs + x.length, 0) ?? 0) +
(statusMessage?.length ?? 0) +
64,
size: currentSize,
cachedAt: now,
staleAt: now + ttl,
deleteAt: 0 // TODO
Expand All @@ -116,12 +141,17 @@ class CacheHandler extends DecoratorHandler {
)
}

/**
* @see {DispatchHandlers.onData}
*
* @param {Buffer} chunk
* @returns {boolean}
*/
onData (chunk) {
if (this.#value) {
this.#value.size += chunk.bodyLength

const maxEntrySize = this.#opts.store.maxEntrySize ?? Infinity
if (this.#value.size > maxEntrySize) {
if (this.#value.size > this.#getMaxEntrySize()) {
this.#value = null
} else {
this.#value.data.body.push(chunk)
Expand All @@ -131,24 +161,59 @@ class CacheHandler extends DecoratorHandler {
return this.#handler.onData(chunk)
}

/**
* @see {DispatchHandlers.onComplete}
*
* @param {string[] | null} rawTrailers
*/
onComplete (rawTrailers) {
if (this.#value) {
this.#value.complete = true
this.#value.data.rawTrailers = rawTrailers
this.#value.size += rawTrailers?.reduce((xs, x) => xs + x.length, 0) ?? 0
this.#value.size += this.#getSizeOfBuffers(rawTrailers)

this.#opts.store.put(this.#req, this.#value).catch(err => {
throw err
})
// If we're still under the max entry size, let's add it to the cache
if (this.#getMaxEntrySize() > this.#value.size) {
this.#opts.store.put(this.#req, this.#value).catch(err => {
throw err
})
}
}

return this.#handler.onComplete(rawTrailers)
}

/**
* @see {DispatchHandlers.onError}
*
* @param {Error} err
*/
onError (err) {
this.#value = undefined
this.#handler.onError(err)
}

/**
* @returns {number}
*/
#getMaxEntrySize () {
return this.#opts.store.maxEntrySize ?? Infinity
}

/**
*
* @param {string[] | Buffer[]} arr
* @returns {number}
*/
#getSizeOfBuffers (arr) {
let size = 0

for (const buffer of arr) {
size += buffer.length
}

return size
}
}

/**
Expand Down Expand Up @@ -202,17 +267,36 @@ function stripNecessaryHeaders (rawHeaders, parsedHeaders) {

if (headerName in HEADERS_TO_REMOVE) {
if (!strippedRawHeaders) {
strippedRawHeaders = rawHeaders.slice(0, n - 1)
strippedRawHeaders = rawHeaders.slice(0, i - 1)
} else {
strippedRawHeaders.push(rawHeaders[i])
}
} else if (strippedRawHeaders) {
strippedRawHeaders.push(rawHeaders[n])
}
}
strippedRawHeaders ??= rawHeaders

return strippedRawHeaders
? strippedRawHeaders.filter(() => true)
: rawHeaders
}

/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
*/
function canCacheAuthorizationHeader (cacheControlDirectives) {
if (
Array.isArray(cacheControlDirectives['no-cache']) &&
cacheControlDirectives['no-cache'].includes('authorization')
) {
return false
}

if (
Array.isArray(cacheControlDirectives['private']) &&
cacheControlDirectives['private'].includes('authorization')
) {
return false
}

return true
}

module.exports = CacheHandler
98 changes: 98 additions & 0 deletions lib/handler/cache-revalidation-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict'

const util = require('../core/util.js')
const DecoratorHandler = require('../handler/decorator-handler')

/**
* TODO docs
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
*
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers
* @implements {DispatchHandlers}
*/
class CacheRevalidationHandler extends DecoratorHandler {
#successful = false
/**
* @type {() => void}
*/
#successCallback = null
/**
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
*/
#handler = null

/**
* @param {() => void} successCallback
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
*/
constructor (successCallback, handler) {
super(handler)

this.#successCallback = successCallback
this.#handler = handler
}

/**
* @see {DispatchHandlers.onHeaders}
*
* @param {number} statusCode
* @param {Buffer[]} rawHeaders
* @param {() => void} resume
* @param {string} statusMessage
* @param {string[] | undefined} headers
* @returns {boolean}
*/
onHeaders (
statusCode,
rawHeaders,
resume,
statusMessage,
headers = util.parseHeaders(rawHeaders)
) {
if (statusCode === 304) {
// TODO update the cached value to be fresh again
this.#successCallback()
return true
}

return this.#handler.onHeaders(
statusCode,
rawHeaders,
resume,
statusMessage,
headers
)
}

/**
* @see {DispatchHandlers.onData}
*
* @param {Buffer} chunk
* @returns {boolean}
*/
onData (chunk) {
return this.#successful ? true : this.#handler.onData(chunk)
}

/**
* @see {DispatchHandlers.onComplete}
*
* @param {string[] | null} rawTrailers
*/
onComplete (rawTrailers) {
if (!this.#successful) {
this.#handler.onComplete(rawTrailers)
}
}

/**
* @see {DispatchHandlers.onError}
*
* @param {Error} err
*/
onError (err) {
this.#handler.onError(err)
}
}

module.exports = CacheRevalidationHandler
Loading

0 comments on commit e07e3ec

Please sign in to comment.