/
redis.js
366 lines (321 loc) · 10.5 KB
/
redis.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
/*
* redis.js: Redis storage engine for nconf configuration(s)
*
* (C) 2011, Charlie Robbins
*
*/
var async = require('async'),
redis = require('redis'),
nconf = require('nconf'),
Memory = require('./memory').Memory;
//
// ### function Redis (options)
// #### @options {Object} Options for this instance
// Constructor function for the Redis nconf store which maintains
// a nested Redis key structure based on key delimiters `:`.
//
// e.g.
// my:nested:key, 'value'
// namespace:keys ==> ['my']
// namespace:nested:keys ==> ['key']
// namespace:nested:key ==> 'value'
//
var Redis = exports.Redis = function (options) {
options = options || {};
this.type = 'redis';
this.namespace = options.namespace || 'nconf';
this.host = options.host || 'localhost';
this.port = options.port || 6379;
this.ttl = options.ttl || 60 * 60 * 1000;
this.cache = new Memory();
this.redis = redis.createClient(options.port, options.host);
if (options.auth) {
this.redis.auth(options.auth);
}
// Suppress errors from the Redis client
this.redis.on('error', function (err) {
require('eyes').inspect(err);
});
};
//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Redis.prototype.get = function (key, callback) {
var self = this,
result = {},
now = Date.now(),
mtime = this.cache.mtimes[key],
fullKey = nconf.key(this.namespace, key);
// Set the callback if not provided for "fire and forget"
callback = callback || function () { };
//
// If the key exists in the cache and the ttl is less than
// the value set for this instance, return from the cache.
//
if (mtime && now - mtime < this.ttl) {
return callback(null, this.cache.get(key));
}
//
// Get the set of all children keys for the `key` supplied. If the value
// to be returned is an Object, this list will not be empty.
//
this.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
function addValue (source, next) {
self.get(nconf.key(key, source), function (err, value) {
if (err) {
return next(err);
}
result[source] = value;
next();
});
}
if (keys && keys.length > 0) {
//
// If the value to be retrieved is an Object, recursively attempt
// to get the value from redis. Here we use a recursive call to `this.get`
// to support nested Object keys.
//
async.forEach(keys, addValue, function (err) {
if (err) {
return callback(err);
}
self.cache.set(key, result);
callback(null, result);
})
}
else {
//
// If there are no keys, then the value to be retrieved is a literal
// and we can simply return the value from redis directly.
//
self.redis.get(fullKey, function (err, value) {
if (err) {
return callback(err);
}
result = JSON.parse(value);
if (result) {
self.cache.set(key, result);
}
callback(null, result);
});
}
});
};
//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Redis.prototype.set = function (key, value, callback) {
var self = this,
path = nconf.path(key);
// Set the callback if not provided for "fire and forget"
callback = callback || function () { };
function addKey (partial, next) {
var index = path.indexOf(partial),
base = [self.namespace].concat(path.slice(0, index)),
parent = nconf.key.apply(null, base.concat(['keys']));
self.redis.sadd(parent, partial, next);
};
//
// Iterate over the entire key path and add each key to the
// parent key-set if it doesn't exist already.
//
async.forEach(path, addKey, function (err) {
if (err) {
return callback(err);
}
var fullKey = nconf.key(self.namespace, key);
if (!Array.isArray(value) && typeof value === 'object') {
//
// If the value is an `Object` (and not an `Array`) then
// nest into the value and set the child keys appropriately.
// This is done for efficient lookup when setting Object keys.
// (i.e. If you set and Object then wish to later retrieve only a
// member of that Object, the entire Object need not be retrieved).
//
self._setObject(fullKey, value, callback);
}
else {
//
// If the value is a simple literal (or an `Array`) then JSON
// stringify it and put it into Redis.
//
value = JSON.stringify(value);
self.cache.set(key, value);
self.redis.set(fullKey, value, callback);
}
});
};
//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Redis.prototype.clear = function (key, callback) {
var self = this,
result = {},
path = [this.namespace].concat(nconf.path(key)),
last = path.pop(),
fullKey = nconf.key(this.namespace, key);
// Set the callback if not provided for "fire and forget"
callback = callback || function () { };
//
// Clear the key from the cache for this instance
//
this.cache.clear(key);
//
// Remove the `key` from the parent set of keys.
//
this.redis.srem(nconf.key.apply(null, path.concat(['keys'])), last, function (err) {
//
// Remove the value from redis by iterating over the set of keys (if any)
// and deleting each value. If no keys, then just delete the simple literal.
//
self.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
function removeValue (child, next) {
//
// Recursively call `self.clear` here to ensure we remove any
// nested Objects completely from this instance.
//
self.clear(nconf.key(key, child), next);
}
if (keys && keys.length > 0) {
//
// If there are child keys then iterate over them,
// removing each child along the way.
//
async.forEach(keys, removeValue, callback);
}
else {
//
// Otherwise if this is just a simple literal, then
// simply remove it from Redis directly.
//
self.redis.del(fullKey, callback);
}
});
});
};
//
// ### function save (value, callback)
// #### @value {Object} Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`.
//
Redis.prototype.save = function (value, callback) {
if (Array.isArray(value) || typeof value !== 'object') {
return callback(new Error('`value` to be saved must be an object.'));
}
var self = this,
keys = Object.keys(value);
// Set the callback if not provided for "fire and forget"
callback = callback || function () { };
//
// Clear all existing keys associated with this instance.
//
this.reset(function (err) {
if (err) {
return callback(err);
}
//
// Iterate over the keys in the new value, setting each of them.
//
async.forEach(keys, function (key, next) {
self.set(key, value[key], next);
}, callback);
});
};
//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
Redis.prototype.load = function (callback) {
var self = this,
result = {};
// Set the callback if not provided for "fire and forget"
callback = callback || function () { };
this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, keys) {
if (err) {
return callback(err);
}
function addValue (key, next) {
self.get(key, function (err, value) {
if (err) {
return next(err);
}
result[key] = value;
next();
});
}
async.forEach(keys, addValue, function (err) {
return err ? callback(err) : callback(null, result);
});
});
};
//
// ### function reset (callback)
// #### @callback {function} Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Redis.prototype.reset = function (callback) {
var self = this;
// Set the callback if not provided for "fire and forget"
callback = callback || function () { };
//
// Get the list of of top-level keys, then clear each of them
//
this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, existing) {
if (err) {
return callback(err);
}
async.forEach(existing, function (key, next) {
self.clear(key, next);
}, callback);
});
};
//
// ### @private function _setObject (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Internal helper function for setting all keys of a nested object.
//
Redis.prototype._setObject = function (key, value, callback) {
var self = this,
keys = Object.keys(value);
function addValue (child, next) {
//
// Add the child key to the parent key-set, then set the value.
// Recursively call `_setObject` in the event of nested Object(s).
//
self.redis.sadd(nconf.key(key, 'keys'), child, function (err) {
if (err) {
return next(err);
}
var fullKey = nconf.key(key, child),
childValue = value[child];
if (!Array.isArray(childValue) && typeof childValue === 'object') {
self._setObject(fullKey, childValue, next);
}
else {
childValue = JSON.stringify(childValue);
self.redis.set(fullKey, childValue, next);
}
});
}
//
// Iterate over the keys of the Object and set the appropriate values.
//
async.forEach(keys, addValue, function (err) {
return err ? callback(err) : callback();
});
};