1
1
import assert from 'node:assert'
2
2
import { DecoratorHandler , parseHeaders , parseCacheControl } from '../utils.js'
3
3
import { DatabaseSync } from 'node:sqlite' // --experimental-sqlite
4
+ import * as BJSON from 'buffer-json'
4
5
5
6
class CacheHandler extends DecoratorHandler {
6
7
#handler
7
8
#store
8
9
#key
9
10
#opts
10
- #value = null
11
+ #value
11
12
12
13
constructor ( { key, handler, store, opts = [ ] } ) {
13
14
super ( handler )
@@ -25,13 +26,11 @@ class CacheHandler extends DecoratorHandler {
25
26
}
26
27
27
28
onHeaders ( statusCode , rawHeaders , resume , statusMessage , headers = parseHeaders ( rawHeaders ) ) {
28
- if ( statusCode !== 307 ) {
29
+ if ( statusCode !== 307 || statusCode !== 200 ) {
29
30
return this . #handler. onHeaders ( statusCode , rawHeaders , resume , statusMessage , headers )
30
31
}
31
32
32
- // TODO (fix): Support vary header.
33
33
const cacheControl = parseCacheControl ( headers [ 'cache-control' ] )
34
-
35
34
const contentLength = headers [ 'content-length' ] ? Number ( headers [ 'content-length' ] ) : Infinity
36
35
const maxEntrySize = this . #store. maxEntrySize ?? Infinity
37
36
@@ -66,7 +65,7 @@ class CacheHandler extends DecoratorHandler {
66
65
( rawHeaders ?. reduce ( ( xs , x ) => xs + x . length , 0 ) ?? 0 ) +
67
66
( statusMessage ?. length ?? 0 ) +
68
67
64 ,
69
- expires : Date . now ( ) + ttl , // in ms!
68
+ expires : Date . now ( ) + ttl ,
70
69
}
71
70
}
72
71
}
@@ -89,20 +88,14 @@ class CacheHandler extends DecoratorHandler {
89
88
90
89
onComplete ( rawTrailers ) {
91
90
if ( this . #value) {
91
+ const reqHeaders = this . #opts
92
92
const resHeaders = parseHeaders ( this . #value. data . rawHeaders )
93
93
94
94
// Early return if Vary = *, uncacheable.
95
95
if ( resHeaders . vary === '*' ) {
96
96
return this . #handler. onComplete ( rawTrailers )
97
97
}
98
98
99
- const reqHeaders = this . #opts
100
-
101
- // If Range header present, assume that the response varies based on Range.
102
- if ( reqHeaders . headers ?. range ) {
103
- resHeaders . vary += ', Range'
104
- }
105
-
106
99
this . #value. data . rawTrailers = rawTrailers
107
100
this . #value. size = this . #value. size
108
101
? this . #value. size + rawTrailers ?. reduce ( ( xs , x ) => xs + x . length , 0 )
@@ -112,6 +105,7 @@ class CacheHandler extends DecoratorHandler {
112
105
113
106
this . #store. set ( this . #key, this . #value)
114
107
}
108
+
115
109
return this . #handler. onComplete ( rawTrailers )
116
110
}
117
111
}
@@ -120,20 +114,27 @@ function formatVaryData(resHeaders, reqHeaders) {
120
114
return resHeaders . vary
121
115
?. split ( ',' )
122
116
. map ( ( key ) => key . trim ( ) . toLowerCase ( ) )
123
- . map ( ( key ) => [ key , reqHeaders [ key ] ?? reqHeaders . headers [ key ] ] )
124
- . filter ( ( [ _key , val ] ) => val )
117
+ . map ( ( key ) => [ key , reqHeaders [ key ] ?? '' ] )
118
+ . filter ( ( [ , val ] ) => val )
125
119
}
126
120
127
121
export class CacheStore {
128
- constructor ( ) {
129
- this . database = null
130
- this . init ( )
131
- }
122
+ #database
123
+
124
+ #insertquery
125
+ #getQuery
126
+ #purgeQuery
127
+
128
+ #size = 0
129
+ #maxSize = 128e9
130
+
131
+ constructor ( location = ':memory:' , opts ) {
132
+ // TODO (fix): Validate args...
132
133
133
- init ( ) {
134
- this . database = new DatabaseSync ( 'file:memdb1?mode=memory&cache=shared' )
134
+ this . #maxSize = opts . maxSize ?? this . #maxSize
135
+ this . # database = new DatabaseSync ( location )
135
136
136
- this . database . exec ( `
137
+ this . # database. exec ( `
137
138
CREATE TABLE IF NOT EXISTS cacheInterceptor(
138
139
key TEXT,
139
140
data TEXT,
@@ -142,91 +143,51 @@ export class CacheStore {
142
143
expires INTEGER
143
144
) STRICT
144
145
` )
145
- }
146
-
147
- set ( key , entry ) {
148
- if ( ! this . database ) {
149
- throw new Error ( 'Database not initialized' )
150
- }
151
146
152
- entry . data = JSON . stringify ( entry . data )
153
- entry . vary = JSON . stringify ( entry . vary )
154
-
155
- const insert = this . database . prepare (
147
+ this . #insertquery = this . #database. prepare (
156
148
'INSERT INTO cacheInterceptor (key, data, vary, size, expires) VALUES (?, ?, ?, ?, ?)' ,
157
149
)
158
150
159
- insert . run ( key , entry . data , entry . vary , entry . size , entry . expires )
151
+ this . #getQuery = this . #database. prepare (
152
+ 'SELECT * FROM cacheInterceptor WHERE key = ? AND expires > ? ' ,
153
+ )
154
+
155
+ this . #purgeQuery = this . #database. prepare ( 'DELETE FROM cacheInterceptor WHERE expires < ?' )
160
156
161
- this . purge ( )
157
+ this . #maybePurge ( )
162
158
}
163
159
164
- get ( key ) {
165
- if ( ! this . database ) {
166
- throw new Error ( 'Database not initialized' )
167
- }
168
- this . purge ( )
169
- const query = this . database . prepare (
170
- 'SELECT * FROM cacheInterceptor WHERE key = ? AND expires > ? ' ,
171
- )
172
- const rows = query . all ( key , Date . now ( ) )
173
- rows . map ( ( i ) => {
174
- i . data = JSON . parse ( i . data )
175
- i . vary = JSON . parse ( i . vary )
176
- i . data = {
177
- ...i . data ,
178
- // JSON.parse doesn't convert a Buffer object back to a Buffer object once it has been stringified.
179
- body : this . #convertToBuffer( i . data . body ) ,
180
- rawHeaders : this . #convertToBuffer( i . data . rawHeaders ) ,
181
- rawTrailers : this . #convertToBuffer( i . data . rawTrailers ) ,
182
- }
183
- return i
184
- } )
160
+ set ( key , { data, vary, size, expires } ) {
161
+ this . #insertquery. run ( key , JSON . stringify ( data ) , BJSON . stringify ( vary ) , size , expires )
185
162
186
- return rows
163
+ this . #size += size
164
+ this . #maybePurge( )
187
165
}
188
166
189
- purge ( ) {
190
- const query = this . database . prepare ( 'DELETE FROM cacheInterceptor WHERE expires < ?' )
191
- query . run ( Date . now ( ) )
167
+ get ( key ) {
168
+ return this . #getQuery. all ( key , Date . now ( ) ) . map ( ( { data, vary, size, expires } ) => ( {
169
+ data : BJSON . parse ( data ) ,
170
+ vary : JSON . parse ( vary ) ,
171
+ size : parseInt ( size ) , // TODO (fix): Is parseInt necessary?
172
+ expores : parseInt ( expires ) , // TODO (fix): Is parseInt necessary?
173
+ } ) )
192
174
}
193
175
194
- deleteAll ( ) {
195
- const query = this . database . prepare ( 'DELETE FROM cacheInterceptor' )
196
- query . run ( )
176
+ close ( ) {
177
+ this . #database. close ( )
197
178
}
198
179
199
- #convertToBuffer( bufferArray ) {
200
- if ( Array . isArray ( bufferArray ) && bufferArray . length > 0 ) {
201
- return bufferArray . map ( ( ba ) => {
202
- return typeof ba === 'object' ? Buffer . from ( ba . data ) : ba
203
- } )
180
+ #maybePurge( ) {
181
+ if ( this . #size == null || this . #size > this . #maxSize) {
182
+ this . #purgeQuery. run ( Date . now ( ) )
183
+ this . #size = this . #database. exec ( 'SELECT SUM(size) FROM cacheInterceptor' ) [ 0 ] . values [ 0 ] [ 0 ]
204
184
}
205
- return [ ]
206
185
}
207
186
}
208
187
209
- /*
210
- Sort entries by number of vary headers in descending order, because
211
- we need to compare the most complex response to the request first.
212
- A cached response with an empty ´vary´ field will otherwise win every time.
213
- */
214
- function sortEntriesByVary ( entries ) {
215
- entries . sort ( ( a , b ) => {
216
- const lengthA = a . vary ? a . vary . length : 0
217
- const lengthB = b . vary ? b . vary . length : 0
218
- return lengthB - lengthA
219
- } )
220
- }
221
-
222
188
function findEntryByHeaders ( entries , reqHeaders ) {
223
- sortEntriesByVary ( entries )
224
-
225
189
return entries ?. find (
226
- ( entry ) =>
227
- entry . vary ?. every ( ( [ key , val ] ) => {
228
- return reqHeaders ?. headers [ key ] === val
229
- } ) ?? true ,
190
+ ( entry ) => entry . vary ?. every ( ( [ key , val ] ) => reqHeaders ?. headers [ key ] === val ) ?? true ,
230
191
)
231
192
}
232
193
@@ -276,7 +237,7 @@ export default (opts) => (dispatch) => (opts, handler) => {
276
237
277
238
const key = `${ opts . method } :${ opts . path } `
278
239
279
- const entries = ( store . get ( key ) ?? opts . method === 'HEAD' ) ? store . get ( `GET:${ opts . path } ` ) : null
240
+ const entries = store . get ( key ) ?? ( opts . method === 'HEAD' ? store . get ( `GET:${ opts . path } ` ) : null )
280
241
281
242
const entry = findEntryByHeaders ( entries , opts )
282
243
0 commit comments