Skip to content

Commit

Permalink
perf things, deleteByOrigin
Browse files Browse the repository at this point in the history
Signed-off-by: flakey5 <[email protected]>
  • Loading branch information
flakey5 committed Sep 19, 2024
1 parent 4546799 commit 5a215d2
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 72 deletions.
103 changes: 72 additions & 31 deletions lib/cache/memory-cache-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class MemoryCacheStore {

#entryCount = 0
/**
* @type {Map<string, import('../../types/cache-interceptor.d.ts').default.CacheStoreValue[]>}
* @type {Map<string, Map<string, import('../../types/cache-interceptor.d.ts').default.CacheStoreValue[]>>}
*/
#data = new Map()

Expand Down Expand Up @@ -65,12 +65,7 @@ class MemoryCacheStore {
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue | undefined}
*/
get (req) {
const key = this.#makeKey(req)
if (!key) {
return undefined
}

const values = this.#data.get(key)
const values = this.#getValues(req)
if (!values) {
return
}
Expand All @@ -80,15 +75,20 @@ class MemoryCacheStore {
for (let i = values.length - 1; i >= 0; i--) {
const current = values[i]
if (now >= current.deleteAt) {
// Should be deleted, so let's remove it
values.splice(i, 1)
this.#entryCount--
continue
// We've reached expired values, let's delete them
this.#entryCount -= values.length - i
values.length = i
break
}

let matches = true

if (current.vary) {
if (!req.headers) {
matches = false
break
}

for (const key in current.vary) {
if (current.vary[key] !== req.headers[key]) {
matches = false
Expand All @@ -103,7 +103,7 @@ class MemoryCacheStore {
}
}

return { ...value }
return value ? { ...value } : undefined
}

/**
Expand All @@ -113,38 +113,79 @@ class MemoryCacheStore {
put (req, value) {
const existingValue = this.get(req)
if (existingValue) {
// We already cached something with the same key & vary headers, override it
// We already cached something with the same key & vary headers, update it
Object.assign(existingValue, value)
} else {
// New response to cache
const key = this.#makeKey(req)
if (key === undefined) {
return
}
return
}

let values = this.#data.get(key)
if (!values) {
values = []
this.#data.set(key, values)
}
// New response to cache
const values = this.#getValues(req)

this.#entryCount++

if (!values) {
// We've never cached anything at this origin before
const pathValues = new Map()
pathValues.set(`${req.path}:${req.method}`, [value])

this.#entryCount++
this.#data.set(req.origin, pathValues)
return
}

if (
values.length === 0 ||
value.deleteAt < values[values.length - 1].deleteAt
) {
values.push(value)
}

if (value.deleteAt >= values[0].deleteAt) {
values.unshift(value)
return
}

let startIndex = 0
let endIndex = values.length
while (true) {
if (startIndex === endIndex) {
values.splice(startIndex, 0, value)
break
}

const middleIndex = (startIndex + endIndex) / 2
const middleValue = values[middleIndex]
if (value.deleteAt === middleIndex) {
values.splice(middleIndex, 0, value)
break
} else if (value.deleteAt > middleValue.deleteAt) {
endIndex = middleIndex
continue
} else {
startIndex = middleIndex
continue
}
}
}

/**
* @param {string} origin
*/
deleteByOrigin (origin) {
this.#data.delete(origin)
}

/**
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
* @returns {string | undefined}
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue[] | undefined}
*/
#makeKey (req) {
// Can't cache if we don't have the origin
if (!req.origin) {
#getValues (req) {
// https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
const cachedPaths = this.#data.get(req.origin)
if (!cachedPaths) {
return undefined
}

// https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
return `${req.origin}:${req.path}:${req.method}`
return cachedPaths.get(`${req.path}:${req.method}`)
}
}

Expand Down
50 changes: 31 additions & 19 deletions lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@

const util = require('../core/util')
const DecoratorHandler = require('../handler/decorator-handler')
const { parseCacheControlHeader, parseVaryHeader } = require('../util/cache')
const { 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}
* @type {import('../../types/cache-interceptor.d.ts').default.CacheOptions} | null
*/
#opts = null
/**
* @type {import('../../types/dispatcher.d.ts').default.RequestOptions}
* @type {import('../../types/dispatcher.d.ts').default.RequestOptions | null}
*/
#req = null
/**
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers | null}
*/
#handler = null
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue | undefined}
*/
#value = null
#value = undefined

/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
Expand Down Expand Up @@ -55,6 +55,30 @@ class CacheHandler extends DecoratorHandler {
statusMessage,
headers = util.parseHeaders(rawHeaders)
) {
if (
this.#req.method in UNSAFE_METHODS &&
statusCode >= 200 &&
statusCode <= 399
) {
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-respons
const result = this.#opts.store.deleteByOrigin(this.#req.origin)
if (
result &&
typeof result.catch === 'function' &&
typeof this.#handler.onError === 'function'
) {
result.catch(this.#handler.onError)
}

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

const cacheControlHeader = headers['cache-control']
const contentLengthHeader = headers['content-length']

Expand Down Expand Up @@ -243,22 +267,10 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
}

if (
!cacheControlDirectives.public &&
!cacheControlDirectives['s-maxage'] &&
!cacheControlDirectives['must-revalidate']
) {
// Response can't be used in a shared cache
return false
}

if (
// TODO double check these
!cacheControlDirectives.public ||
cacheControlDirectives.private === true ||
cacheControlDirectives['no-cache'] === true ||
cacheControlDirectives['no-store'] ||
cacheControlDirectives['no-transform'] ||
cacheControlDirectives['must-understand'] ||
cacheControlDirectives['proxy-revalidate']
cacheControlDirectives['no-store']
) {
return false
}
Expand Down
34 changes: 24 additions & 10 deletions lib/handler/cache-revalidation-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class CacheRevalidationHandler extends DecoratorHandler {
constructor (successCallback, handler) {
super(handler)

if (typeof successCallback !== 'function') {
throw new TypeError('successCallback must be a function')
}

this.#successCallback = successCallback
this.#handler = handler
}
Expand Down Expand Up @@ -62,13 +66,15 @@ class CacheRevalidationHandler extends DecoratorHandler {
return true
}

return this.#handler.onHeaders(
statusCode,
rawHeaders,
resume,
statusMessage,
headers
)
if (typeof this.#handler.onHeaders === 'function') {
return this.#handler.onHeaders(
statusCode,
rawHeaders,
resume,
statusMessage,
headers
)
}
}

/**
Expand All @@ -78,7 +84,13 @@ class CacheRevalidationHandler extends DecoratorHandler {
* @returns {boolean}
*/
onData (chunk) {
return this.#successful ? true : this.#handler.onData(chunk)
if (this.#successful) {
return true
}

if (typeof this.#handler.onData === 'function') {
this.#handler.onData(chunk)
}
}

/**
Expand All @@ -87,7 +99,7 @@ class CacheRevalidationHandler extends DecoratorHandler {
* @param {string[] | null} rawTrailers
*/
onComplete (rawTrailers) {
if (!this.#successful) {
if (!this.#successful && typeof this.#handler.onComplete === 'function') {
this.#handler.onComplete(rawTrailers)
}
}
Expand All @@ -98,7 +110,9 @@ class CacheRevalidationHandler extends DecoratorHandler {
* @param {Error} err
*/
onError (err) {
this.#handler.onError(err)
if (typeof this.#handler.onError === 'function') {
this.#handler.onError(err)
}
}
}

Expand Down
24 changes: 18 additions & 6 deletions lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const CacheHandler = require('../handler/cache-handler')
const MemoryCacheStore = require('../cache/memory-cache-store')
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
const { UNSAFE_METHODS } = require('../util/cache.js')

const AGE_HEADER = Buffer.from('age')

Expand Down Expand Up @@ -123,34 +124,45 @@ module.exports = globalOpts => {
if (globalOpts.store) {
for (const fn of ['get', 'put']) {
if (typeof globalOpts.store[fn] !== 'function') {
throw new Error(`CacheStore needs a \`${fn}()\` function`)
throw new TypeError(`CacheStore needs a \`${fn}()\` function`)
}
}

for (const getter of ['entryCount', 'maxEntries', 'maxEntrySize']) {
const actualType = typeof globalOpts.store[getter]
if (actualType !== 'number') {
throw new Error(`CacheStore needs a ${getter} property with type number, current type: ${actualType}`)
throw new TypeError(`CacheStore needs a ${getter} property with type number, current type: ${actualType}`)
}
}

for (const value of ['maxEntries', 'maxEntry']) {
if (globalOpts.store[value] <= 0) {
throw new Error(`CacheStore ${value} needs to be >= 1`)
throw new TypeError(`CacheStore ${value} needs to be >= 1`)
}
}
} else {
globalOpts.store = new MemoryCacheStore()
}

if (!globalOpts.methods) {
if (globalOpts.methods) {
if (!Array.isArray(globalOpts.methods)) {
throw new TypeError(`methods needs to be an array, got ${typeof globalOpts.methods}`)
}

if (globalOpts.methods.length === 0) {
throw new Error('methods must have at least one method in it')
}
} else {
globalOpts.methods = ['GET']
}

// Safe methods the user wants and unsafe methods
const methods = [...globalOpts.methods, ...UNSAFE_METHODS]

return dispatch => {
return (opts, handler) => {
if (!globalOpts.methods.includes(opts.method)) {
// Not a method we want to cache, skip
if (!opts.origin || !methods.includes(opts.method)) {
// Not a method we want to cache or we don't have the origin, skip
return dispatch(opts, handler)
}

Expand Down
Loading

0 comments on commit 5a215d2

Please sign in to comment.