33const { Writable } = require ( 'node:stream' )
44
55/**
6+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey } CacheKey
7+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue } CacheValue
68 * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore } CacheStore
9+ * @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult } GetResult
10+ */
11+
12+ /**
713 * @implements {CacheStore}
8- *
9- * @typedef {{
10- * locked: boolean
11- * opts: import('../../types/cache-interceptor.d.ts').default.CachedResponse
12- * body?: Buffer[]
13- * }} MemoryStoreValue
1414 */
1515class MemoryCacheStore {
1616 #maxCount = Infinity
17-
17+ #maxSize = Infinity
1818 #maxEntrySize = Infinity
1919
20- #entryCount = 0
21-
22- /**
23- * @type {Map<string, Map<string, MemoryStoreValue[]>> }
24- */
25- #data = new Map ( )
20+ #size = 0
21+ #count = 0
22+ #entries = new Map ( )
2623
2724 /**
2825 * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined } [opts]
@@ -44,6 +41,17 @@ class MemoryCacheStore {
4441 this . #maxCount = opts . maxCount
4542 }
4643
44+ if ( opts . maxSize !== undefined ) {
45+ if (
46+ typeof opts . maxSize !== 'number' ||
47+ ! Number . isInteger ( opts . maxSize ) ||
48+ opts . maxSize < 0
49+ ) {
50+ throw new TypeError ( 'MemoryCacheStore options.maxSize must be a non-negative integer' )
51+ }
52+ this . #maxSize = opts . maxSize
53+ }
54+
4755 if ( opts . maxEntrySize !== undefined ) {
4856 if (
4957 typeof opts . maxEntrySize !== 'number' ||
@@ -57,269 +65,115 @@ class MemoryCacheStore {
5765 }
5866 }
5967
60- get isFull ( ) {
61- return this . #entryCount >= this . #maxCount
62- }
63-
6468 /**
65- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey } key
69+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey } req
6670 * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined }
6771 */
6872 get ( key ) {
6973 if ( typeof key !== 'object' ) {
7074 throw new TypeError ( `expected key to be object, got ${ typeof key } ` )
7175 }
7276
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- }
77+ const topLevelKey = `${ key . origin } :${ key . path } `
8378
84- return { ...value . opts , body : value . body }
79+ const now = Date . now ( )
80+ const entry = this . #entries. get ( topLevelKey ) ?. find ( ( entry ) => (
81+ entry . deleteAt > now &&
82+ entry . method === key . method &&
83+ ( entry . vary == null || Object . keys ( entry . vary ) . every ( headerName => entry . vary [ headerName ] === key . headers ?. [ headerName ] ) )
84+ ) )
85+
86+ return entry == null
87+ ? undefined
88+ : {
89+ statusMessage : entry . statusMessage ,
90+ statusCode : entry . statusCode ,
91+ rawHeaders : entry . rawHeaders ,
92+ body : entry . body ,
93+ cachedAt : entry . cachedAt ,
94+ staleAt : entry . staleAt ,
95+ deleteAt : entry . deleteAt
96+ }
8597 }
8698
8799 /**
88100 * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey } key
89- * @param {import('../../types/cache-interceptor.d.ts').default.CachedResponse } opts
101+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue } val
90102 * @returns {Writable | undefined }
91103 */
92- createWriteStream ( key , opts ) {
104+ createWriteStream ( key , val ) {
93105 if ( typeof key !== 'object' ) {
94106 throw new TypeError ( `expected key to be object, got ${ typeof key } ` )
95107 }
96- if ( typeof opts !== 'object' ) {
97- throw new TypeError ( `expected value to be object, got ${ typeof opts } ` )
98- }
99-
100- if ( this . isFull ) {
101- return undefined
108+ if ( typeof val !== 'object' ) {
109+ throw new TypeError ( `expected value to be object, got ${ typeof val } ` )
102110 }
103111
104- const values = this . #getValuesForRequest( key , true )
105-
106- /**
107- * @type {(MemoryStoreValue & { index: number }) | undefined }
108- */
109- let value = this . #findValue( key , values )
110- let valueIndex = value ?. index
111- if ( ! value ) {
112- // The value doesn't already exist, meaning we haven't cached this
113- // response before. Let's assign it a value and insert it into our data
114- // property.
115-
116- if ( this . isFull ) {
117- // Or not, we don't have space to add another response
118- return undefined
119- }
120-
121- this . #entryCount++
122-
123- value = {
124- locked : true ,
125- opts
126- }
127-
128- // We want to sort our responses in decending order by their deleteAt
129- // timestamps so that deleting expired responses is faster
130- if (
131- values . length === 0 ||
132- opts . deleteAt < values [ values . length - 1 ] . deleteAt
133- ) {
134- // Our value is either the only response for this path or our deleteAt
135- // time is sooner than all the other responses
136- values . push ( value )
137- valueIndex = values . length - 1
138- } else if ( opts . deleteAt >= values [ 0 ] . deleteAt ) {
139- // Our deleteAt is later than everyone elses
140- values . unshift ( value )
141- valueIndex = 0
142- } else {
143- // We're neither in the front or the end, let's just binary search to
144- // find our stop we need to be in
145- let startIndex = 0
146- let endIndex = values . length
147- while ( true ) {
148- if ( startIndex === endIndex ) {
149- values . splice ( startIndex , 0 , value )
150- break
151- }
152-
153- const middleIndex = Math . floor ( ( startIndex + endIndex ) / 2 )
154- const middleValue = values [ middleIndex ]
155- if ( opts . deleteAt === middleIndex ) {
156- values . splice ( middleIndex , 0 , value )
157- valueIndex = middleIndex
158- break
159- } else if ( opts . deleteAt > middleValue . opts . deleteAt ) {
160- endIndex = middleIndex
161- continue
162- } else {
163- startIndex = middleIndex
164- continue
165- }
166- }
167- }
168- } else {
169- // Check if there's already another request writing to the value or
170- // a request reading from it
171- if ( value . locked ) {
172- return undefined
173- }
174-
175- // Empty it so we can overwrite it
176- value . body = [ ]
177- }
112+ const topLevelKey = `${ key . origin } :${ key . path } `
178113
179- let currentSize = 0
180- /**
181- * @type {Buffer[] | null }
182- */
183- let body = key . method !== 'HEAD' ? [ ] : null
184- const maxEntrySize = this . #maxEntrySize
114+ const store = this
115+ const entry = { ...key , ...val , body : [ ] , size : 0 }
185116
186- const writable = new Writable ( {
117+ return new Writable ( {
187118 write ( chunk , encoding , callback ) {
188- if ( key . method === 'HEAD' ) {
189- throw new Error ( 'HEAD request shouldn\'t have a body' )
190- }
191-
192- if ( ! body ) {
193- return callback ( )
194- }
195-
196119 if ( typeof chunk === 'string' ) {
197120 chunk = Buffer . from ( chunk , encoding )
198121 }
199122
200- currentSize += chunk . byteLength
123+ entry . size += chunk . byteLength
201124
202- if ( currentSize >= maxEntrySize ) {
203- body = null
204- this . end ( )
205- shiftAtIndex ( values , valueIndex )
206- return callback ( )
125+ if ( entry . size >= store . #maxEntrySize) {
126+ this . destroy ( )
127+ } else {
128+ entry . body . push ( chunk )
207129 }
208130
209- body . push ( chunk )
210- callback ( )
131+ callback ( null )
211132 } ,
212133 final ( callback ) {
213- value . locked = false
214- if ( body !== null ) {
215- value . body = body
134+ let entries = store . #entries. get ( topLevelKey )
135+ if ( ! entries ) {
136+ entries = [ ]
137+ store . #entries. set ( topLevelKey , entries )
138+ }
139+ entries . push ( entry )
140+
141+ store . #size += entry . size
142+ store . #count += 1
143+
144+ if ( store . #size > store . #maxSize || store . #count > store . #maxCount) {
145+ for ( const [ key , entries ] of store . #entries) {
146+ for ( const entry of entries . splice ( 0 , entries . length / 2 ) ) {
147+ store . #size -= entry . size
148+ store . #count -= 1
149+ }
150+ if ( entries . length === 0 ) {
151+ store . #entries. delete ( key )
152+ }
153+ }
216154 }
217155
218- callback ( )
156+ callback ( null )
219157 }
220158 } )
221-
222- return writable
223159 }
224160
225161 /**
226- * @param {import('../../types/cache-interceptor.d.ts').default. CacheKey } key
162+ * @param {CacheKey } key
227163 */
228164 delete ( key ) {
229- this . #data. delete ( `${ key . origin } :${ key . path } ` )
230- }
231-
232- /**
233- * Gets all of the requests of the same origin, path, and method. Does not
234- * take the `vary` property into account.
235- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey } key
236- * @param {boolean } [makeIfDoesntExist=false]
237- * @returns {MemoryStoreValue[] | undefined }
238- */
239- #getValuesForRequest ( key , makeIfDoesntExist ) {
240- // https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
241- const topLevelKey = `${ key . origin } :${ key . path } `
242- let cachedPaths = this . #data. get ( topLevelKey )
243- if ( ! cachedPaths ) {
244- if ( ! makeIfDoesntExist ) {
245- return undefined
246- }
247-
248- cachedPaths = new Map ( )
249- this . #data. set ( topLevelKey , cachedPaths )
250- }
251-
252- let value = cachedPaths . get ( key . method )
253- if ( ! value && makeIfDoesntExist ) {
254- value = [ ]
255- cachedPaths . set ( key . method , value )
165+ if ( typeof key !== 'object' ) {
166+ throw new TypeError ( `expected key to be object, got ${ typeof key } ` )
256167 }
257168
258- return value
259- }
260-
261- /**
262- * Given a list of values of a certain request, this decides the best value
263- * to respond with.
264- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey } req
265- * @param {MemoryStoreValue[] } values
266- * @returns {(MemoryStoreValue & { index: number }) | undefined }
267- */
268- #findValue ( req , values ) {
269- /**
270- * @type {MemoryStoreValue | undefined }
271- */
272- let value
273- const now = Date . now ( )
274- for ( let i = values . length - 1 ; i >= 0 ; i -- ) {
275- const current = values [ i ]
276- const currentCacheValue = current . opts
277- if ( now >= currentCacheValue . deleteAt ) {
278- // We've reached expired values, let's delete them
279- this . #entryCount -= values . length - i
280- values . length = i
281- break
282- }
283-
284- let matches = true
285-
286- if ( currentCacheValue . vary ) {
287- if ( ! req . headers ) {
288- matches = false
289- break
290- }
291-
292- for ( const key in currentCacheValue . vary ) {
293- if ( currentCacheValue . vary [ key ] !== req . headers [ key ] ) {
294- matches = false
295- break
296- }
297- }
298- }
169+ const topLevelKey = `${ key . origin } :${ key . path } `
299170
300- if ( matches ) {
301- value = {
302- ...current ,
303- index : i
304- }
305- break
306- }
171+ for ( const entry of this . #entries. get ( topLevelKey ) ?? [ ] ) {
172+ this . #size -= entry . size
173+ this . #count -= 1
307174 }
308-
309- return value
310- }
311- }
312-
313- /**
314- * @param {any[] } array Array to modify
315- * @param {number } idx Index to delete
316- */
317- function shiftAtIndex ( array , idx ) {
318- for ( let i = idx + 1 ; idx < array . length ; i ++ ) {
319- array [ i - 1 ] = array [ i ]
175+ this . #entries. delete ( topLevelKey )
320176 }
321-
322- array . length --
323177}
324178
325179module . exports = MemoryCacheStore
0 commit comments