Skip to content

Commit a09ec2e

Browse files
committed
API caching
1 parent d13df3d commit a09ec2e

File tree

5 files changed

+507
-51
lines changed

5 files changed

+507
-51
lines changed

lib/cache.js

+203-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
var inherits = require('util').inherits;
22
var events = require('events');
3+
var crypto = require('crypto');
34
var fs = require('fs');
45

6+
var DEFAULT_STORAGE = 'memory';
57
var DEFAULT_WRITE_PERMS = '0644';
68
var DEFAULT_INDEX_LIMIT = 1000;
79

@@ -14,16 +16,25 @@ var DEFAULT_INDEX_LIMIT = 1000;
1416
var Cache = function(clientId, options) {
1517
events.EventEmitter.call(this);
1618

19+
this.versions = null;
20+
this.indexes = null;
21+
this.memory = {};
22+
1723
options = options || {};
1824
if (typeof options === 'string') {
1925
options = { path: options };
2026
}
2127
this.params = {
2228
clientId: clientId,
23-
path: options.path,
29+
path: options.path ? String(options.path) : '',
30+
storage: options.storage || DEFAULT_STORAGE,
2431
writePerms: options.writePerms || DEFAULT_WRITE_PERMS,
2532
indexLimit: options.indexLimit || DEFAULT_INDEX_LIMIT
2633
};
34+
35+
if (this.params.storage !== 'memory') {
36+
throw new Error(this.params.storage + ' storage is not currently supported');
37+
}
2738
};
2839

2940
inherits(Cache, events.EventEmitter);
@@ -35,7 +46,32 @@ inherits(Cache, events.EventEmitter);
3546
* @param mixed data
3647
*/
3748
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+
}
3873

74+
return null;
3975
};
4076

4177
/**
@@ -46,7 +82,10 @@ Cache.prototype.get = function(url, data) {
4682
* @return string
4783
*/
4884
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');
5089
};
5190

5291
/**
@@ -55,7 +94,11 @@ Cache.prototype.getKey = function(url, data) {
5594
* @return string
5695
*/
5796
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('.');
59102
};
60103

61104
/**
@@ -64,7 +107,10 @@ Cache.prototype.getPath = function(url, data) {
64107
* @return array
65108
*/
66109
Cache.prototype.getVersions = function() {
67-
110+
if (!this.versions) {
111+
this.versions = this.getCache('versions') || {};
112+
}
113+
return this.versions;
68114
};
69115

70116
/**
@@ -73,7 +119,10 @@ Cache.prototype.getVersions = function() {
73119
* @return array
74120
*/
75121
Cache.prototype.getIndex = function() {
76-
122+
if (!this.indexes) {
123+
this.indexes = this.getCache('index') || {};
124+
}
125+
return this.indexes;
77126
};
78127

79128
/**
@@ -84,7 +133,37 @@ Cache.prototype.getIndex = function() {
84133
* @param mixed result
85134
*/
86135
Cache.prototype.put = function(url, data, result) {
136+
if (result.$data === undefined) {
137+
result.$data = null; // Allows for null response
138+
}
87139

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+
}
88167
};
89168

90169
/**
@@ -95,7 +174,21 @@ Cache.prototype.put = function(url, data, result) {
95174
* @param string size
96175
*/
97176
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+
}
98185

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);
99192
};
100193

101194
/**
@@ -106,7 +199,10 @@ Cache.prototype.putIndex = function(collection, key, size) {
106199
* @param mixed data
107200
*/
108201
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);
110206
};
111207

112208
/**
@@ -117,29 +213,71 @@ Cache.prototype.remove = function(url, data) {
117213
* @return bool
118214
*/
119215
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);
121225
};
122226

123227
/**
124228
* Update/write the cache version file
125229
*
126230
* @param string collection
127-
* @param string cacheKey
231+
* @param number version
128232
* @return number
129233
*/
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);
132242
};
133243

134244
/**
135245
* Clear all cache entries made invalid by result
136246
*
137-
* @param string url
138-
* @param mixed data
139247
* @param mixed result
140248
*/
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(/\.settings$/)) {
271+
invalid[verCollection] = true;
272+
}
273+
}
274+
}
275+
}
276+
}
142277

278+
if (Object.keys(invalid).length > 0) {
279+
this.clearIndexes(invalid);
280+
}
143281
};
144282

145283
/**
@@ -148,7 +286,36 @@ Cache.prototype.clear = function(url, data, result) {
148286
* @param array invalid
149287
*/
150288
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+
}
151316

317+
var indexPath = this.getPath('index');
318+
this.writeCache(indexPath, this.indexes);
152319
};
153320

154321
/**
@@ -157,7 +324,11 @@ Cache.prototype.clearIndexes = function(invalid) {
157324
* @return string
158325
*/
159326
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;
161332
};
162333

163334
/**
@@ -168,7 +339,13 @@ Cache.prototype.getCache = function() {
168339
* @return number
169340
*/
170341
Cache.prototype.writeCache = function(cachePath, content) {
342+
var cacheContent = JSON.stringify(content);
343+
var cacheSize = cacheContent.length;
171344

345+
// TODO: file system storage
346+
this.memory[cachePath] = cacheContent;
347+
348+
return cacheSize;
172349
};
173350

174351
/**
@@ -177,7 +354,7 @@ Cache.prototype.writeCache = function(cachePath, content) {
177354
* @param string cachePath
178355
*/
179356
Cache.prototype.clearCache = function(cachePath) {
180-
357+
delete this.memory[cachePath];
181358
};
182359

183360
/**
@@ -187,7 +364,17 @@ Cache.prototype.clearCache = function(cachePath) {
187364
* @return array
188365
*/
189366
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;
191378
};
192379

193380

0 commit comments

Comments
 (0)