1
1
var inherits = require ( 'util' ) . inherits ;
2
2
var events = require ( 'events' ) ;
3
+ var crypto = require ( 'crypto' ) ;
3
4
var fs = require ( 'fs' ) ;
4
5
6
+ var DEFAULT_STORAGE = 'memory' ;
5
7
var DEFAULT_WRITE_PERMS = '0644' ;
6
8
var DEFAULT_INDEX_LIMIT = 1000 ;
7
9
@@ -14,16 +16,25 @@ var DEFAULT_INDEX_LIMIT = 1000;
14
16
var Cache = function ( clientId , options ) {
15
17
events . EventEmitter . call ( this ) ;
16
18
19
+ this . versions = null ;
20
+ this . indexes = null ;
21
+ this . memory = { } ;
22
+
17
23
options = options || { } ;
18
24
if ( typeof options === 'string' ) {
19
25
options = { path : options } ;
20
26
}
21
27
this . params = {
22
28
clientId : clientId ,
23
- path : options . path ,
29
+ path : options . path ? String ( options . path ) : '' ,
30
+ storage : options . storage || DEFAULT_STORAGE ,
24
31
writePerms : options . writePerms || DEFAULT_WRITE_PERMS ,
25
32
indexLimit : options . indexLimit || DEFAULT_INDEX_LIMIT
26
33
} ;
34
+
35
+ if ( this . params . storage !== 'memory' ) {
36
+ throw new Error ( this . params . storage + ' storage is not currently supported' ) ;
37
+ }
27
38
} ;
28
39
29
40
inherits ( Cache , events . EventEmitter ) ;
@@ -35,7 +46,32 @@ inherits(Cache, events.EventEmitter);
35
46
* @param mixed data
36
47
*/
37
48
Cache . prototype . get = function ( url , data ) {
49
+ data = data || null ;
50
+
51
+ var cacheKey = this . getKey ( url , data ) ;
52
+ var result = this . getCache ( cacheKey , 'result' ) ;
53
+
54
+ if ( result ) {
55
+ // Ensure cache_key exists in index
56
+ this . getIndex ( ) ;
57
+ if ( result . $collection !== undefined ) {
58
+ var collection = result . $collection ;
59
+ if ( this . indexes [ collection ] && this . indexes [ collection ] [ cacheKey ] ) {
60
+ return result ;
61
+ }
62
+ }
63
+
64
+ // Not found in proper index, then clear?
65
+ var resultCollections = this . resultCollections ( result ) ;
66
+ for ( var i = 0 ; i < resultCollections . length ; i ++ ) {
67
+ var collection = resultCollections [ i ] ;
68
+ var where = { } ;
69
+ where [ collection ] = cacheKey ;
70
+ this . clearIndexes ( where ) ;
71
+ }
72
+ }
38
73
74
+ return null ;
39
75
} ;
40
76
41
77
/**
@@ -46,7 +82,10 @@ Cache.prototype.get = function(url, data) {
46
82
* @return string
47
83
*/
48
84
Cache . prototype . getKey = function ( url , data ) {
49
-
85
+ data = data || null ;
86
+ var saneUrl = String ( url ) . trim ( ) . replace ( / ^ \/ | \/ $ / g, '' ) ;
87
+ var keyData = JSON . stringify ( [ saneUrl , data ] ) ;
88
+ return crypto . createHash ( 'md5' ) . update ( keyData ) . digest ( 'hex' ) ;
50
89
} ;
51
90
52
91
/**
@@ -55,7 +94,11 @@ Cache.prototype.getKey = function(url, data) {
55
94
* @return string
56
95
*/
57
96
Cache . prototype . getPath = function ( url , data ) {
58
-
97
+ return this . params . path . replace ( / \/ $ / , '' ) +
98
+ '/client.' +
99
+ this . params . clientId +
100
+ '.' +
101
+ Array . prototype . slice . call ( arguments ) . join ( '.' ) ;
59
102
} ;
60
103
61
104
/**
@@ -64,7 +107,10 @@ Cache.prototype.getPath = function(url, data) {
64
107
* @return array
65
108
*/
66
109
Cache . prototype . getVersions = function ( ) {
67
-
110
+ if ( ! this . versions ) {
111
+ this . versions = this . getCache ( 'versions' ) || { } ;
112
+ }
113
+ return this . versions ;
68
114
} ;
69
115
70
116
/**
@@ -73,7 +119,10 @@ Cache.prototype.getVersions = function() {
73
119
* @return array
74
120
*/
75
121
Cache . prototype . getIndex = function ( ) {
76
-
122
+ if ( ! this . indexes ) {
123
+ this . indexes = this . getCache ( 'index' ) || { } ;
124
+ }
125
+ return this . indexes ;
77
126
} ;
78
127
79
128
/**
@@ -84,7 +133,37 @@ Cache.prototype.getIndex = function() {
84
133
* @param mixed result
85
134
*/
86
135
Cache . prototype . put = function ( url , data , result ) {
136
+ if ( result . $data === undefined ) {
137
+ result . $data = null ; // Allows for null response
138
+ }
87
139
140
+ this . getVersions ( ) ;
141
+
142
+ var cacheContent = Object . assign ( { } , result ) ;
143
+ cacheContent . $cached = true ;
144
+
145
+ var cacheKey = this . getKey ( url , data ) ;
146
+ var cachePath = this . getPath ( cacheKey , 'result' ) ;
147
+
148
+ var size = this . writeCache ( cachePath , cacheContent ) ;
149
+
150
+ if ( size > 0 ) {
151
+ if ( result . $cached !== undefined ) {
152
+ var cached = result . $cached ;
153
+ var resultCollections = this . resultCollections ( result ) ;
154
+ for ( var i = 0 ; i < resultCollections . length ; i ++ ) {
155
+ var collection = resultCollections [ i ] ;
156
+ // Collection may not be cacheable
157
+ if ( cached [ collection ] === undefined && this . versions [ collection ] === undefined ) {
158
+ continue ;
159
+ }
160
+ this . putIndex ( collection , cacheKey , size ) ;
161
+ if ( cached [ collection ] !== undefined ) {
162
+ this . putVersion ( collection , cached [ collection ] ) ;
163
+ }
164
+ }
165
+ }
166
+ }
88
167
} ;
89
168
90
169
/**
@@ -95,7 +174,21 @@ Cache.prototype.put = function(url, data, result) {
95
174
* @param string size
96
175
*/
97
176
Cache . prototype . putIndex = function ( collection , key , size ) {
177
+ this . getIndex ( ) ;
178
+
179
+ // Limit size of index per client/collection
180
+ if ( this . indexes [ collection ] !== undefined ) {
181
+ if ( Object . keys ( this . indexes [ collection ] ) . length >= this . params . indexLimit ) {
182
+ this . truncateIndex ( collection ) ;
183
+ }
184
+ }
98
185
186
+ this . indexes [ collection ] = this . indexes [ collection ] || { } ;
187
+ this . indexes [ collection ] [ key ] = size ;
188
+
189
+ var indexPath = this . getPath ( 'index' ) ;
190
+
191
+ return this . writeCache ( indexPath , this . indexes ) ;
99
192
} ;
100
193
101
194
/**
@@ -106,7 +199,10 @@ Cache.prototype.putIndex = function(collection, key, size) {
106
199
* @param mixed data
107
200
*/
108
201
Cache . prototype . remove = function ( url , data ) {
109
-
202
+ data = data || null ;
203
+ var cacheKey = this . getKey ( url , data ) ;
204
+ var cachePath = this . getPath ( cacheKey , 'result' ) ;
205
+ this . clearCache ( cachePath ) ;
110
206
} ;
111
207
112
208
/**
@@ -117,29 +213,71 @@ Cache.prototype.remove = function(url, data) {
117
213
* @return bool
118
214
*/
119
215
Cache . prototype . truncateIndex = function ( collection ) {
120
-
216
+ this . getIndex ( ) ;
217
+ if ( this . indexes [ collection ] === undefined ) {
218
+ return ;
219
+ }
220
+ var keys = Object . keys ( this . indexes [ collection ] ) ;
221
+ var lastKey = keys [ keys . length - 1 ] ;
222
+ var invalid = { } ;
223
+ invalid [ collection ] = lastKey ;
224
+ this . clearIndexes ( invalid ) ;
121
225
} ;
122
226
123
227
/**
124
228
* Update/write the cache version file
125
229
*
126
230
* @param string collection
127
- * @param string cacheKey
231
+ * @param number version
128
232
* @return number
129
233
*/
130
- Cache . prototype . putVersion = function ( collection , cacheKey ) {
131
-
234
+ Cache . prototype . putVersion = function ( collection , version ) {
235
+ if ( ! version ) {
236
+ return ;
237
+ }
238
+ this . getVersions ( ) ;
239
+ this . versions [ collection ] = version ;
240
+ var versionPath = this . getPath ( 'versions' ) ;
241
+ this . writeCache ( versionPath , this . versions ) ;
132
242
} ;
133
243
134
244
/**
135
245
* Clear all cache entries made invalid by result
136
246
*
137
- * @param string url
138
- * @param mixed data
139
247
* @param mixed result
140
248
*/
141
- Cache . prototype . clear = function ( url , data , result ) {
249
+ Cache . prototype . clear = function ( result ) {
250
+ if ( result . $cached === undefined ) {
251
+ return ;
252
+ }
253
+
254
+ this . getVersions ( ) ;
255
+
256
+ var invalid = { } ;
257
+ var cachedCollections = Object . keys ( result . $cached ) ;
258
+ for ( var i = 0 ; i < cachedCollections . length ; i ++ ) {
259
+ var collection = cachedCollections [ i ] ;
260
+ var version = result . $cached [ collection ] ;
261
+ if ( this . versions [ collection ] === undefined || version !== this . versions [ collection ] ) {
262
+ this . putVersion ( collection , version ) ;
263
+ invalid [ collection ] = true ;
264
+ // Hack to make admin.settings affect other api.settings
265
+ // TODO: figure out how to do this on the server side
266
+ if ( collection === 'admin.settings' ) {
267
+ var versionCollections = Object . keys ( this . versions ) ;
268
+ for ( var j = 0 ; j < versionCollections . length ; j ++ ) {
269
+ var verCollection = versionCollections [ j ] ;
270
+ if ( String ( verCollection ) . match ( / \. s e t t i n g s $ / ) ) {
271
+ invalid [ verCollection ] = true ;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
142
277
278
+ if ( Object . keys ( invalid ) . length > 0 ) {
279
+ this . clearIndexes ( invalid ) ;
280
+ }
143
281
} ;
144
282
145
283
/**
@@ -148,7 +286,36 @@ Cache.prototype.clear = function(url, data, result) {
148
286
* @param array invalid
149
287
*/
150
288
Cache . prototype . clearIndexes = function ( invalid ) {
289
+ if ( ! invalid || Object . keys ( invalid ) . length === 0 ) {
290
+ return ;
291
+ }
292
+
293
+ this . getIndex ( ) ;
294
+ var invalidCollections = Object . keys ( invalid ) ;
295
+ for ( var i = 0 ; i < invalidCollections . length ; i ++ ) {
296
+ var collection = invalidCollections [ i ] ;
297
+ if ( this . indexes [ collection ] !== undefined ) {
298
+ if ( invalid [ collection ] === true ) {
299
+ // Clear all indexes per collection
300
+ var cacheKeys = Object . keys ( this . indexes [ collection ] ) ;
301
+ for ( var j = 0 ; j < cacheKeys . length ; j ++ ) {
302
+ var key = cacheKeys [ j ] ;
303
+ var cachePath = this . getPath ( key , 'result' ) ;
304
+ this . clearCache ( cachePath ) ;
305
+ delete this . indexes [ collection ] [ key ] ;
306
+ }
307
+ } else if ( invalid [ collection ] && this . indexes [ collection ] [ invalid [ collection ] ] !== undefined ) {
308
+ // Clear a single index element by key
309
+ var key = invalid [ collection ] ;
310
+ var cachePath = this . getPath ( key , 'result' ) ;
311
+ this . clearCache ( cachePath ) ;
312
+ delete this . indexes [ collection ] [ key ] ;
313
+ }
314
+ }
315
+ }
151
316
317
+ var indexPath = this . getPath ( 'index' ) ;
318
+ this . writeCache ( indexPath , this . indexes ) ;
152
319
} ;
153
320
154
321
/**
@@ -157,7 +324,11 @@ Cache.prototype.clearIndexes = function(invalid) {
157
324
* @return string
158
325
*/
159
326
Cache . prototype . getCache = function ( ) {
160
-
327
+ var cachePath = this . getPath . apply ( this , arguments ) ;
328
+ if ( this . memory [ cachePath ] !== undefined ) {
329
+ return JSON . parse ( this . memory [ cachePath ] ) ;
330
+ }
331
+ return null ;
161
332
} ;
162
333
163
334
/**
@@ -168,7 +339,13 @@ Cache.prototype.getCache = function() {
168
339
* @return number
169
340
*/
170
341
Cache . prototype . writeCache = function ( cachePath , content ) {
342
+ var cacheContent = JSON . stringify ( content ) ;
343
+ var cacheSize = cacheContent . length ;
171
344
345
+ // TODO: file system storage
346
+ this . memory [ cachePath ] = cacheContent ;
347
+
348
+ return cacheSize ;
172
349
} ;
173
350
174
351
/**
@@ -177,7 +354,7 @@ Cache.prototype.writeCache = function(cachePath, content) {
177
354
* @param string cachePath
178
355
*/
179
356
Cache . prototype . clearCache = function ( cachePath ) {
180
-
357
+ delete this . memory [ cachePath ] ;
181
358
} ;
182
359
183
360
/**
@@ -187,7 +364,17 @@ Cache.prototype.clearCache = function(cachePath) {
187
364
* @return array
188
365
*/
189
366
Cache . prototype . resultCollections = function ( result ) {
190
-
367
+ var collections = result . $collection !== undefined ? [ result . $collection ] : [ ] ;
368
+ // Combine $collection and $expanded headers
369
+ if ( result . $expanded !== undefined ) {
370
+ for ( var i = 0 ; i < result . $expanded . length ; i ++ ) {
371
+ var expCollection = result . $expanded [ i ] ;
372
+ if ( collections . indexOf ( expCollection ) === - 1 ) {
373
+ collections . push ( expCollection ) ;
374
+ }
375
+ }
376
+ }
377
+ return collections ;
191
378
} ;
192
379
193
380
0 commit comments