Skip to content

Commit

Permalink
followup http-cache
Browse files Browse the repository at this point in the history
  • Loading branch information
Uzlopak committed Oct 14, 2024
1 parent 2d40ade commit c65844d
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 20 deletions.
48 changes: 29 additions & 19 deletions lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,63 @@

const util = require('../core/util')
const DecoratorHandler = require('../handler/decorator-handler')
const { parseCacheControlHeader, parseVaryHeader, UNSAFE_METHODS, assertCacheStoreType } = require('../util/cache')
const {
assertCacheStoreType,
parseCacheControlHeader,
parseVaryHeader,
UNSAFE_METHODS
} = require('../util/cache')

/**
* Writes a response to a CacheStore and then passes it on to the next handler
*/
class CacheHandler extends DecoratorHandler {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheOptions | null}
*/
#opts = null
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
*/
#store

/**
* @type {import('../../types/dispatcher.d.ts').default.RequestOptions | null}
* @type {import('../../types/dispatcher.d.ts').default.RequestOptions}
*/
#req = null
#requestOptions

/**
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers | null}
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
*/
#handler = null
#handler

/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStoreWriteable | undefined}
*/
#writeStream

/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} requestOptions
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
*/
constructor (opts, req, handler) {
constructor (opts, requestOptions, handler) {
super(handler)

if (typeof opts !== 'object') {
throw new TypeError(`expected opts to be an object, got type ${typeof opts}`)
}

assertCacheStoreType(opts.store)
const { store } = opts

assertCacheStoreType(store)

if (typeof req !== 'object') {
if (typeof requestOptions !== 'object') {
throw new TypeError(`expected req to be an object, got type ${typeof opts}`)
}

if (typeof handler !== 'object') {
throw new TypeError(`expected handler to be an object, got type ${typeof opts}`)
}

this.#opts = opts
this.#req = req
this.#store = store
this.#requestOptions = requestOptions
this.#handler = handler
}

Expand All @@ -75,14 +85,14 @@ class CacheHandler extends DecoratorHandler {
)

if (
UNSAFE_METHODS.includes(this.#req.method) &&
UNSAFE_METHODS.includes(this.#requestOptions.method) &&
statusCode >= 200 &&
statusCode <= 399
) {
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-respons
// Try/catch for if it's synchronous
try {
const result = this.#opts.store.deleteByOrigin(this.#req.origin)
const result = this.#store.deleteByOrigin(this.#requestOptions.origin)
if (
result &&
typeof result.catch === 'function' &&
Expand All @@ -103,7 +113,7 @@ class CacheHandler extends DecoratorHandler {
const cacheControlHeader = headers['cache-control']
const contentLengthHeader = headers['content-length']

if (!cacheControlHeader || !contentLengthHeader || this.#opts.store.isFull) {
if (!cacheControlHeader || !contentLengthHeader || this.#store.isFull) {
// Don't have the headers we need, can't cache
return downstreamOnHeaders()
}
Expand All @@ -122,7 +132,7 @@ class CacheHandler extends DecoratorHandler {
const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
if (staleAt) {
const varyDirectives = headers.vary
? parseVaryHeader(headers.vary, this.#req.headers)
? parseVaryHeader(headers.vary, this.#requestOptions.headers)
: undefined
const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)

Expand All @@ -132,7 +142,7 @@ class CacheHandler extends DecoratorHandler {
cacheControlDirectives
)

this.#writeStream = this.#opts.store.createWriteStream(this.#req, {
this.#writeStream = this.#store.createWriteStream(this.#requestOptions, {
statusCode,
statusMessage,
rawHeaders: strippedHeaders,
Expand Down
21 changes: 21 additions & 0 deletions lib/util/cache.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
'use strict'

const SAFE_METHODS = /** @type {const} */ ([
'GET', 'HEAD', 'OPTIONS', 'TRACE'
])

const UNSAFE_METHODS = /** @type {const} */ ([
'POST', 'PUT', 'PATCH', 'DELETE'
])
Expand Down Expand Up @@ -196,10 +200,27 @@ function assertCacheStoreType (store) {
throw new TypeError(`CacheStore needs a isFull getter with type boolean, current type: ${typeof store.isFull}`)
}
}
/**
* @param {unknown} methods
* @returns {asserts methods is import('../../types/cache-interceptor.d.ts').default.CacheMethods[]}
*/
function assertCacheMethods (methods) {
if (!Array.isArray(methods)) {
throw new TypeError(`expected type to be an array, got ${typeof methods}`)
}

for (const method of methods) {
if (!UNSAFE_METHODS.includes(method)) {
throw new TypeError(`CacheMethods needs to be one of ${UNSAFE_METHODS.join(', ')}, got ${method}`)
}
}
}

module.exports = {
SAFE_METHODS,
UNSAFE_METHODS,
parseCacheControlHeader,
parseVaryHeader,
assertCacheMethods,
assertCacheStoreType
}
4 changes: 3 additions & 1 deletion types/cache-interceptor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Dispatcher from './dispatcher'
export default CacheHandler

declare namespace CacheHandler {
export type CacheMethods = 'GET' | 'HEAD' | 'OPTIONS' | 'TRACE'

export interface CacheOptions {
store?: CacheStore

Expand All @@ -14,7 +16,7 @@ declare namespace CacheHandler {
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-respons
* @see https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1
*/
methods?: ('GET' | 'HEAD' | 'OPTIONS' | 'TRACE')[]
methods?: CacheMethods[]
}

/**
Expand Down

0 comments on commit c65844d

Please sign in to comment.