Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: http caching #3562

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ module.exports.RedirectHandler = RedirectHandler
module.exports.interceptors = {
redirect: require('./lib/interceptor/redirect'),
retry: require('./lib/interceptor/retry'),
dump: require('./lib/interceptor/dump')
dump: require('./lib/interceptor/dump'),
cache: require('./lib/interceptor/cache')
}

module.exports.cacheStores = {
MemoryCacheStore: require('./lib/cache/memory-cache-store')
}

module.exports.buildConnector = buildConnector
Expand Down
133 changes: 133 additions & 0 deletions lib/cache/memory-cache-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use strict'

flakey5 marked this conversation as resolved.
Show resolved Hide resolved
/**
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
* @implements {CacheStore}
*/
class MemoryCacheStore {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts} opts
*/
#opts = {}
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
#entryCount = 0
/**
* @type {Map<string, import('../../types/cache-interceptor.d.ts').default.CacheStoreValue[]>}
*/
#data = new Map()

/**
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} opts
*/
constructor (opts) {
this.#opts = opts ?? {}

if (!this.#opts.maxEntries) {
this.#opts.maxEntries = Infinity
}

if (!this.#opts.maxEntrySize) {
this.#opts.maxEntrySize = Infinity
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
}
}

get entryCount () {
return this.#entryCount
}

get maxEntries () {
return this.#opts.maxEntries
}

get maxEntrySize () {
return this.#opts.maxEntrySize
}
ronag marked this conversation as resolved.
Show resolved Hide resolved
flakey5 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
* @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)
if (!values) {
return
}

let value
const now = Date.now()
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
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
}

let matches = true

if (current.vary) {
for (const key in current.vary) {
if (current.vary[key] !== req.headers[key]) {
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
matches = false
break
}
}
}

if (matches) {
value = current
break
}
}

return value
}

/**
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} value
*/
put (req, value) {
const existingValue = this.get(req)
if (existingValue) {
// We already cached something with the same key & vary headers, override it
Object.assign(existingValue, value)
} else {
// New response to cache
const key = this.#makeKey(req)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this.get(req) could accept a key value? then you would avoid calling makeKey twice when you first time cache the response?!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't work as the store decides how the key looks and sounds like a micro-optimization IMHO

if (!key) {
flakey5 marked this conversation as resolved.
Show resolved Hide resolved
return
}

let values = this.#data.get(key)
if (!values) {
values = []
this.#data.set(key, values)
}

this.#entryCount++
values.push(value)
}
}

/**
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
* @returns {string | undefined}
*/
#makeKey (req) {
// Can't cache if we don't have the origin
if (!req.origin) {
return undefined
}

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

module.exports = MemoryCacheStore
Loading
Loading