Skip to content

Commit

Permalink
feat: http caching
Browse files Browse the repository at this point in the history
Implements bare-bones http caching as per rfc9111

Closes #3231
Closes #2760
Closes #2256
Closes #1146

Co-authored-by: Carlos Fuentes <me@metcoder.dev>

Co-authored-by: Robert Nagy <ronagy@icloud.com>

Co-authored-by: Isak Törnros <isak.tornros@hotmail.com>

Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com>
  • Loading branch information
flakey5 committed Sep 14, 2024
1 parent b66fb4b commit f69a8b2
Show file tree
Hide file tree
Showing 12 changed files with 1,434 additions and 1 deletion.
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
101 changes: 101 additions & 0 deletions lib/cache/memory-cache-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict'

/**
* @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 = {}
/**
* @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.maxEntrySize) {
this.#opts.maxEntrySize = Infinity
}
}

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

/**
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
* @returns {Promise<import('../../types/cache-interceptor.d.ts').default.CacheStoreValue | undefined>}
*/
get (req) {
const key = this.#makeKey(req)

const values = this.#data.get(key)
if (!values) {
return
}

let value
const now = Date.now()
for (let i = values.length - 1; i >= 0; i--) {
const current = values[i]
if (now >= current.deleteAt) {
// Delete the expired ones
values.splice(i, 1)
continue
}

let matches = true

if (current.vary) {
for (const key in current.vary) {
if (current.vary[key] !== req.headers[key]) {
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 key = this.#makeKey(req)

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

values.push(value)
}

/**
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
* @returns {string}
*/
#makeKey (req) {
// TODO origin is undefined
// https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
return `${req.origin}:${req.path}:${req.method}`
}
}

module.exports = MemoryCacheStore
Loading

0 comments on commit f69a8b2

Please sign in to comment.