3
3
const { Writable } = require ( 'node:stream' )
4
4
5
5
/**
6
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey } CacheKey
6
7
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore } CacheStore
8
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CachedResponse } CachedResponse
9
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult } GetResult
7
10
* @implements {CacheStore}
8
- *
9
- * @typedef {{
10
- * locked: boolean
11
- * opts: import('../../types/cache-interceptor.d.ts').default.CachedResponse
12
- * body?: Buffer[]
13
- * }} MemoryStoreValue
14
11
*/
15
12
class MemoryCacheStore {
16
13
#maxCount = Infinity
@@ -20,7 +17,7 @@ class MemoryCacheStore {
20
17
#entryCount = 0
21
18
22
19
/**
23
- * @type {Map<string, Map<string, MemoryStoreValue []>> }
20
+ * @type {Map<string, Map<string, GetResult []>> }
24
21
*/
25
22
#data = new Map ( )
26
23
@@ -66,22 +63,8 @@ class MemoryCacheStore {
66
63
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined }
67
64
*/
68
65
get ( key ) {
69
- if ( typeof key !== 'object' ) {
70
- throw new TypeError ( `expected key to be object, got ${ typeof key } ` )
71
- }
72
-
73
- const values = this . #getValuesForRequest( key , false )
74
- if ( ! values ) {
75
- return undefined
76
- }
77
-
78
- const value = this . #findValue( key , values )
79
-
80
- if ( ! value || value . locked ) {
81
- return undefined
82
- }
83
-
84
- return { ...value . opts , body : value . body }
66
+ const values = this . #getValuesForRequest( key )
67
+ return findValue ( key , values )
85
68
}
86
69
87
70
/**
@@ -98,86 +81,52 @@ class MemoryCacheStore {
98
81
}
99
82
100
83
if ( this . isFull ) {
101
- return undefined
84
+ this . #prune ( )
102
85
}
103
86
104
- const values = this . #getValuesForRequest( key , true )
105
-
106
- let value = this . #findValue( key , values )
107
- if ( ! value ) {
108
- // The value doesn't already exist, meaning we haven't cached this
109
- // response before. Let's assign it a value and insert it into our data
110
- // property.
87
+ if ( this . isFull ) {
88
+ return undefined
89
+ }
111
90
112
- if ( this . isFull ) {
113
- // Or not, we don't have space to add another response
114
- return undefined
115
- }
91
+ const values = this . #getValuesForRequest( key )
116
92
117
- if ( this . #entryCount++ > this . #maxEntries) {
118
- this . #prune( )
119
- }
93
+ let value = findValue ( key , values )
120
94
121
- value = { locked : true , opts }
95
+ if ( ! value ) {
96
+ value = { ...opts , body : null }
122
97
values . push ( value )
123
- } else {
124
- // Check if there's already another request writing to the value or
125
- // a request reading from it
126
- if ( value . locked ) {
127
- return undefined
128
- }
129
-
130
- // Empty it so we can overwrite it
131
- value . body = [ ]
132
98
}
133
99
134
100
let currentSize = 0
135
- /**
136
- * @type {Buffer[] | null }
137
- */
138
- let body = [ ]
101
+
102
+ const body = [ ]
139
103
const maxEntrySize = this . #maxEntrySize
140
104
141
- const writable = new Writable ( {
105
+ return new Writable ( {
142
106
write ( chunk , encoding , callback ) {
143
- if ( key . method === 'HEAD' ) {
144
- throw new Error ( 'HEAD request shouldn\'t have a body' )
145
- }
146
-
147
- if ( ! body ) {
148
- return callback ( )
149
- }
150
-
151
107
if ( typeof chunk === 'string' ) {
152
108
chunk = Buffer . from ( chunk , encoding )
153
109
}
154
110
155
111
currentSize += chunk . byteLength
156
112
157
113
if ( currentSize >= maxEntrySize ) {
158
- body = null
159
- this . end ( )
160
- return callback ( )
114
+ this . destroy ( )
115
+ } else {
116
+ body . push ( chunk )
161
117
}
162
118
163
- body . push ( chunk )
164
119
callback ( )
165
120
} ,
166
121
final ( callback ) {
167
- value . locked = false
168
- if ( body !== null ) {
169
- value . body = body
170
- }
171
-
122
+ Object . assign ( value , opts , { body } )
172
123
callback ( )
173
124
}
174
125
} )
175
-
176
- return writable
177
126
}
178
127
179
128
/**
180
- * @param {import('../../types/cache-interceptor.d.ts').default. CacheKey } key
129
+ * @param {CacheKey } key
181
130
*/
182
131
delete ( key ) {
183
132
this . #data. delete ( `${ key . origin } :${ key . path } ` )
@@ -186,53 +135,36 @@ class MemoryCacheStore {
186
135
/**
187
136
* Gets all of the requests of the same origin, path, and method. Does not
188
137
* take the `vary` property into account.
189
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey } key
190
- * @param {boolean } [makeIfDoesntExist=false]
191
- * @returns {MemoryStoreValue[] | undefined }
138
+ * @param {CacheKey } key
139
+ * @returns {GetResult[] }
192
140
*/
193
- #getValuesForRequest ( key , makeIfDoesntExist ) {
141
+ #getValuesForRequest ( key ) {
142
+ if ( typeof key !== 'object' ) {
143
+ throw new TypeError ( `expected key to be object, got ${ typeof key } ` )
144
+ }
145
+
194
146
// https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
195
147
const topLevelKey = `${ key . origin } :${ key . path } `
196
148
let cachedPaths = this . #data. get ( topLevelKey )
197
149
if ( ! cachedPaths ) {
198
- if ( ! makeIfDoesntExist ) {
199
- return undefined
200
- }
201
-
202
150
cachedPaths = new Map ( )
203
151
this . #data. set ( topLevelKey , cachedPaths )
204
152
}
205
153
206
154
let value = cachedPaths . get ( key . method )
207
- if ( ! value && makeIfDoesntExist ) {
155
+ if ( ! value ) {
208
156
value = [ ]
209
157
cachedPaths . set ( key . method , value )
210
158
}
211
159
212
160
return value
213
161
}
214
162
215
- /**
216
- * Given a list of values of a certain request, this decides the best value
217
- * to respond with.
218
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey } req
219
- * @param {MemoryStoreValue[] } values
220
- * @returns {(MemoryStoreValue) | undefined }
221
- */
222
- #findValue ( req , values ) {
223
- const now = Date . now ( )
224
- return values . find ( ( { opts : { deleteAt, vary } , body } ) => (
225
- body != null &&
226
- deleteAt > now &&
227
- ( ! vary || Object . keys ( vary ) . every ( key => vary [ key ] === req . headers ?. [ key ] ) )
228
- ) )
229
- }
230
-
231
163
#prune ( ) {
232
164
const now = Date . now ( )
233
165
for ( const [ key , cachedPaths ] of this . #data) {
234
166
for ( const [ method , prev ] of cachedPaths ) {
235
- const next = prev . filter ( ( { opts , body } ) => body == null || opts . deleteAt > now )
167
+ const next = prev . filter ( ( { deleteAt } ) => deleteAt > now )
236
168
if ( next . length === 0 ) {
237
169
cachedPaths . delete ( method )
238
170
if ( cachedPaths . size === 0 ) {
@@ -247,4 +179,24 @@ class MemoryCacheStore {
247
179
}
248
180
}
249
181
182
+ /**
183
+ * Given a list of values of a certain request, this decides the best value
184
+ * to respond with.
185
+ * @param {CacheKey } key
186
+ * @param {GetResult[] | undefined } values
187
+ * @returns {(GetResult) | undefined }
188
+ */
189
+ function findValue ( key , values ) {
190
+ if ( typeof key !== 'object' ) {
191
+ throw new TypeError ( `expected key to be object, got ${ typeof key } ` )
192
+ }
193
+
194
+ const now = Date . now ( )
195
+ return values ?. find ( ( { deleteAt, vary, body } ) => (
196
+ body != null &&
197
+ deleteAt > now &&
198
+ ( ! vary || Object . keys ( vary ) . every ( headerName => vary [ headerName ] === key . headers ?. [ headerName ] ) )
199
+ ) )
200
+ }
201
+
250
202
module . exports = MemoryCacheStore
0 commit comments