Skip to content

Commit f69a8b2

Browse files
committed
feat: http caching
Implements bare-bones http caching as per rfc9111 Closes #3231 Closes #2760 Closes #2256 Closes #1146 Co-authored-by: Carlos Fuentes <[email protected]> Co-authored-by: Robert Nagy <[email protected]> Co-authored-by: Isak Törnros <[email protected]> Signed-off-by: flakey5 <[email protected]>
1 parent b66fb4b commit f69a8b2

File tree

12 files changed

+1434
-1
lines changed

12 files changed

+1434
-1
lines changed

index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ module.exports.RedirectHandler = RedirectHandler
3939
module.exports.interceptors = {
4040
redirect: require('./lib/interceptor/redirect'),
4141
retry: require('./lib/interceptor/retry'),
42-
dump: require('./lib/interceptor/dump')
42+
dump: require('./lib/interceptor/dump'),
43+
cache: require('./lib/interceptor/cache')
44+
}
45+
46+
module.exports.cacheStores = {
47+
MemoryCacheStore: require('./lib/cache/memory-cache-store')
4348
}
4449

4550
module.exports.buildConnector = buildConnector

lib/cache/memory-cache-store.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use strict'
2+
3+
/**
4+
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
5+
* @implements {CacheStore}
6+
*/
7+
class MemoryCacheStore {
8+
/**
9+
* @type {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts} opts
10+
*/
11+
#opts = {}
12+
/**
13+
* @type {Map<string, import('../../types/cache-interceptor.d.ts').default.CacheStoreValue[]>}
14+
*/
15+
#data = new Map()
16+
17+
/**
18+
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} opts
19+
*/
20+
constructor (opts) {
21+
this.#opts = opts ?? {}
22+
23+
if (!this.#opts.maxEntrySize) {
24+
this.#opts.maxEntrySize = Infinity
25+
}
26+
}
27+
28+
get maxEntrySize () {
29+
return this.#opts.maxEntrySize
30+
}
31+
32+
/**
33+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
34+
* @returns {Promise<import('../../types/cache-interceptor.d.ts').default.CacheStoreValue | undefined>}
35+
*/
36+
get (req) {
37+
const key = this.#makeKey(req)
38+
39+
const values = this.#data.get(key)
40+
if (!values) {
41+
return
42+
}
43+
44+
let value
45+
const now = Date.now()
46+
for (let i = values.length - 1; i >= 0; i--) {
47+
const current = values[i]
48+
if (now >= current.deleteAt) {
49+
// Delete the expired ones
50+
values.splice(i, 1)
51+
continue
52+
}
53+
54+
let matches = true
55+
56+
if (current.vary) {
57+
for (const key in current.vary) {
58+
if (current.vary[key] !== req.headers[key]) {
59+
matches = false
60+
break
61+
}
62+
}
63+
}
64+
65+
if (matches) {
66+
value = current
67+
break
68+
}
69+
}
70+
71+
return value
72+
}
73+
74+
/**
75+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
76+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} value
77+
*/
78+
put (req, value) {
79+
const key = this.#makeKey(req)
80+
81+
let values = this.#data.get(key)
82+
if (!values) {
83+
values = []
84+
this.#data.set(key, values)
85+
}
86+
87+
values.push(value)
88+
}
89+
90+
/**
91+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
92+
* @returns {string}
93+
*/
94+
#makeKey (req) {
95+
// TODO origin is undefined
96+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
97+
return `${req.origin}:${req.path}:${req.method}`
98+
}
99+
}
100+
101+
module.exports = MemoryCacheStore

0 commit comments

Comments
 (0)