petty-cache 3.5.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -201
- package/README.md +58 -15
- package/eslint.config.js +1 -15
- package/index.js +475 -245
- package/package.json +32 -34
package/index.js
CHANGED
|
@@ -3,6 +3,11 @@ const lock = require('lock').Lock();
|
|
|
3
3
|
const memoryCache = require('memory-cache');
|
|
4
4
|
const redis = require('redis');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Creates a new PettyCache instance backed by Redis.
|
|
8
|
+
* Accepts the same arguments as redis.createClient(), or an existing RedisClient instance.
|
|
9
|
+
* @param {...*} args - Either a RedisClient instance, or arguments forwarded to redis.createClient().
|
|
10
|
+
*/
|
|
6
11
|
function PettyCache() {
|
|
7
12
|
const intervals = {};
|
|
8
13
|
let redisClient;
|
|
@@ -16,18 +21,23 @@ function PettyCache() {
|
|
|
16
21
|
//eslint-disable-next-line no-console
|
|
17
22
|
redisClient.on('error', err => console.warn(`Warning: Redis reported a client error: ${err}`));
|
|
18
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Fetches multiple keys from Redis.
|
|
26
|
+
* @param {string[]} keys
|
|
27
|
+
* @param {Function} callback - callback(err, values) where values maps each key to {exists, value}.
|
|
28
|
+
*/
|
|
19
29
|
function bulkGetFromRedis(keys, callback) {
|
|
20
30
|
// Try to get values from Redis
|
|
21
|
-
redisClient.mget(keys,
|
|
31
|
+
redisClient.mget(keys, (err, data) => {
|
|
22
32
|
if (err) {
|
|
23
33
|
return callback(err);
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
const values = {};
|
|
27
37
|
|
|
28
|
-
for (
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
for (let i = 0; i < keys.length; i++) {
|
|
39
|
+
const key = keys[i];
|
|
40
|
+
const value = data[i];
|
|
31
41
|
|
|
32
42
|
if (value === null) {
|
|
33
43
|
values[key] = { exists: false };
|
|
@@ -41,6 +51,11 @@ function PettyCache() {
|
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Fetches a single key from the in-process memory cache.
|
|
56
|
+
* @param {string} key
|
|
57
|
+
* @returns {{exists: boolean, value: *}}
|
|
58
|
+
*/
|
|
44
59
|
function getFromMemoryCache(key) {
|
|
45
60
|
// Try to get value from memory cache
|
|
46
61
|
const value = memoryCache.get(key);
|
|
@@ -59,9 +74,14 @@ function PettyCache() {
|
|
|
59
74
|
return { exists: false };
|
|
60
75
|
}
|
|
61
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Fetches a single key from Redis.
|
|
79
|
+
* @param {string} key
|
|
80
|
+
* @param {Function} callback - callback(err, {exists, value}).
|
|
81
|
+
*/
|
|
62
82
|
function getFromRedis(key, callback) {
|
|
63
83
|
// Try to get value from Redis
|
|
64
|
-
redisClient.get(key,
|
|
84
|
+
redisClient.get(key, (err, data) => {
|
|
65
85
|
if (err) {
|
|
66
86
|
return callback(err);
|
|
67
87
|
}
|
|
@@ -75,6 +95,12 @@ function PettyCache() {
|
|
|
75
95
|
});
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Resolves TTL options into a {min, max} object in milliseconds. Defaults to 30–60 seconds.
|
|
100
|
+
* @param {Object} options
|
|
101
|
+
* @param {number|Object} [options.ttl] - Fixed ms value, or an object with min/max properties.
|
|
102
|
+
* @returns {{min: number, max: number}}
|
|
103
|
+
*/
|
|
78
104
|
function getTtl(options) {
|
|
79
105
|
// Default TTL is 30-60 seconds
|
|
80
106
|
const ttl = {
|
|
@@ -101,163 +127,223 @@ function PettyCache() {
|
|
|
101
127
|
}
|
|
102
128
|
|
|
103
129
|
/**
|
|
104
|
-
*
|
|
130
|
+
* Returns data from cache for each key if available; otherwise executes func for the missing keys
|
|
131
|
+
* and stores the results in cache before returning. Supports both callback and promise styles.
|
|
132
|
+
* @param {Array} keys - An array of cache keys.
|
|
133
|
+
* @param {Function} func - Function called with missing keys and a callback: (keys, callback).
|
|
134
|
+
* @param {Object} [options] - Optional settings.
|
|
135
|
+
* @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties.
|
|
136
|
+
* @param {Function} [callback] - Optional callback(err, values). If omitted, returns a Promise.
|
|
137
|
+
* @returns {Promise|undefined} Resolves with an object mapping each key to its cached value.
|
|
105
138
|
*/
|
|
106
|
-
this.bulkFetch =
|
|
107
|
-
|
|
108
|
-
if (!callback) {
|
|
139
|
+
this.bulkFetch = (keys, func, options = {}, callback) => {
|
|
140
|
+
if (typeof options === 'function') {
|
|
109
141
|
callback = options;
|
|
110
142
|
options = {};
|
|
111
143
|
}
|
|
112
144
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
145
|
+
const executor = () => {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
// If there aren't any keys, return
|
|
148
|
+
if (!keys.length) {
|
|
149
|
+
return resolve({});
|
|
150
|
+
}
|
|
117
151
|
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
const _keys = Array.from(new Set(keys));
|
|
153
|
+
const values = {};
|
|
120
154
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
// Try to get values from memory cache
|
|
156
|
+
for (let i = _keys.length - 1; i >= 0; i--) {
|
|
157
|
+
const key = _keys[i];
|
|
158
|
+
const result = getFromMemoryCache(key);
|
|
125
159
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
160
|
+
if (result.exists) {
|
|
161
|
+
values[key] = result.value;
|
|
162
|
+
_keys.splice(i, 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
166
|
+
// If there aren't any keys left, return
|
|
167
|
+
if (!_keys.length) {
|
|
168
|
+
return resolve(values);
|
|
169
|
+
}
|
|
136
170
|
|
|
137
|
-
|
|
171
|
+
// Try to get values from Redis
|
|
172
|
+
bulkGetFromRedis(_keys, (err, results) => {
|
|
173
|
+
if (err) {
|
|
174
|
+
return reject(err);
|
|
175
|
+
}
|
|
138
176
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return callback(err);
|
|
143
|
-
}
|
|
177
|
+
for (let i = _keys.length - 1; i >= 0; i--) {
|
|
178
|
+
const key = _keys[i];
|
|
179
|
+
const result = results[key];
|
|
144
180
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
if (result.exists) {
|
|
182
|
+
_keys.splice(i, 1);
|
|
183
|
+
values[key] = result.value;
|
|
148
184
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
185
|
+
// Store value in memory cache with a short expiration
|
|
186
|
+
memoryCache.put(key, result.value, random(2000, 5000));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
152
189
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
190
|
+
// If there aren't any keys left, return
|
|
191
|
+
if (!_keys.length) {
|
|
192
|
+
return resolve(values);
|
|
193
|
+
}
|
|
157
194
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
195
|
+
// Execute the specified function for remaining keys
|
|
196
|
+
func(_keys, (err, data) => {
|
|
197
|
+
if (err) {
|
|
198
|
+
return reject(err);
|
|
199
|
+
}
|
|
162
200
|
|
|
163
|
-
|
|
164
|
-
func(_keys, function(err, data) {
|
|
165
|
-
if (err) {
|
|
166
|
-
return callback(err);
|
|
167
|
-
}
|
|
201
|
+
Object.keys(data).forEach(key => values[key] = data[key]);
|
|
168
202
|
|
|
169
|
-
|
|
203
|
+
this.bulkSet(data, options, err => {
|
|
204
|
+
if (err) {
|
|
205
|
+
return reject(err);
|
|
206
|
+
}
|
|
170
207
|
|
|
171
|
-
|
|
208
|
+
resolve(values);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
172
212
|
});
|
|
173
|
-
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (callback) {
|
|
216
|
+
executor().then(result => callback(null, result)).catch(callback);
|
|
217
|
+
} else {
|
|
218
|
+
return executor();
|
|
219
|
+
}
|
|
174
220
|
};
|
|
175
221
|
|
|
176
222
|
/**
|
|
177
|
-
*
|
|
223
|
+
* Gets cached values for an array of keys. Supports both callback and promise styles.
|
|
224
|
+
* @param {Array} keys - An array of cache keys.
|
|
225
|
+
* @param {Function} [callback] - Optional callback(err, values). If omitted, returns a Promise.
|
|
226
|
+
* @returns {Promise|undefined} Resolves with an object mapping each key to its value, or null if not found.
|
|
178
227
|
*/
|
|
179
|
-
this.bulkGet =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
228
|
+
this.bulkGet = (keys, callback) => {
|
|
229
|
+
const executor = () => {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
// If there aren't any keys, return
|
|
232
|
+
if (!keys.length) {
|
|
233
|
+
return resolve({});
|
|
234
|
+
}
|
|
184
235
|
|
|
185
|
-
|
|
186
|
-
|
|
236
|
+
const _keys = Array.from(new Set(keys));
|
|
237
|
+
const values = {};
|
|
187
238
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
239
|
+
// Try to get values from memory cache
|
|
240
|
+
for (let i = _keys.length - 1; i >= 0; i--) {
|
|
241
|
+
const key = _keys[i];
|
|
242
|
+
const result = getFromMemoryCache(key);
|
|
192
243
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
244
|
+
if (result.exists) {
|
|
245
|
+
values[key] = result.value;
|
|
246
|
+
_keys.splice(i, 1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
198
249
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
250
|
+
// If there aren't any keys left, return
|
|
251
|
+
if (!_keys.length) {
|
|
252
|
+
return resolve(values);
|
|
253
|
+
}
|
|
203
254
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
255
|
+
// Try to get values from Redis
|
|
256
|
+
bulkGetFromRedis(_keys, (err, results) => {
|
|
257
|
+
if (err) {
|
|
258
|
+
return reject(err);
|
|
259
|
+
}
|
|
209
260
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
261
|
+
for (let i = 0; i < _keys.length; i++) {
|
|
262
|
+
const key = _keys[i];
|
|
263
|
+
const result = results[key];
|
|
213
264
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
265
|
+
if (!result.exists) {
|
|
266
|
+
values[key] = null;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
218
269
|
|
|
219
|
-
|
|
270
|
+
values[key] = result.value;
|
|
220
271
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
272
|
+
// Store value in memory cache with a short expiration
|
|
273
|
+
memoryCache.put(key, result.value, random(2000, 5000));
|
|
274
|
+
}
|
|
224
275
|
|
|
225
|
-
|
|
226
|
-
|
|
276
|
+
resolve(values);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (callback) {
|
|
282
|
+
executor().then(result => callback(null, result)).catch(callback);
|
|
283
|
+
} else {
|
|
284
|
+
return executor();
|
|
285
|
+
}
|
|
227
286
|
};
|
|
228
287
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Sets multiple key/value pairs in cache simultaneously. Supports both callback and promise styles.
|
|
290
|
+
* @param {Object} values - An object mapping cache keys to their values.
|
|
291
|
+
* @param {Object} [options] - Optional settings.
|
|
292
|
+
* @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties.
|
|
293
|
+
* @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise.
|
|
294
|
+
* @returns {Promise|undefined}
|
|
295
|
+
*/
|
|
296
|
+
this.bulkSet = (values, options = {}, callback) => {
|
|
297
|
+
if (typeof options === 'function') {
|
|
232
298
|
callback = options;
|
|
233
299
|
options = {};
|
|
234
300
|
}
|
|
235
301
|
|
|
236
|
-
|
|
237
|
-
|
|
302
|
+
const executor = () => {
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
// Get TTL based on specified options
|
|
305
|
+
const ttl = getTtl(options);
|
|
238
306
|
|
|
239
|
-
|
|
240
|
-
|
|
307
|
+
// Redis does not have a MSETEX command so we batch commands: http://redis.js.org/#api-clientbatchcommands
|
|
308
|
+
const batch = redisClient.batch();
|
|
241
309
|
|
|
242
|
-
|
|
243
|
-
|
|
310
|
+
Object.keys(values).forEach(key => {
|
|
311
|
+
const value = values[key];
|
|
244
312
|
|
|
245
|
-
|
|
246
|
-
|
|
313
|
+
// Store value in memory cache with a short expiration
|
|
314
|
+
memoryCache.put(key, value, random(2000, 5000));
|
|
247
315
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
316
|
+
// Add Redis command
|
|
317
|
+
batch.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value));
|
|
318
|
+
});
|
|
251
319
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
320
|
+
batch.exec((err) => {
|
|
321
|
+
if (err) {
|
|
322
|
+
return reject(err);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
resolve();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (callback) {
|
|
331
|
+
executor().then(result => callback(null, result)).catch(callback);
|
|
332
|
+
} else {
|
|
333
|
+
return executor();
|
|
334
|
+
}
|
|
255
335
|
};
|
|
256
336
|
|
|
257
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Deletes a key from both the memory cache and Redis. Supports both callback and promise styles.
|
|
339
|
+
* @param {string} key - The cache key to delete.
|
|
340
|
+
* @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise.
|
|
341
|
+
* @returns {Promise|undefined}
|
|
342
|
+
*/
|
|
343
|
+
this.del = (key, callback) => {
|
|
258
344
|
const executor = () => {
|
|
259
345
|
return new Promise((resolve, reject) => {
|
|
260
|
-
redisClient.del(key,
|
|
346
|
+
redisClient.del(key, (err) => {
|
|
261
347
|
if (err) {
|
|
262
348
|
return reject(err);
|
|
263
349
|
}
|
|
@@ -275,21 +361,26 @@ function PettyCache() {
|
|
|
275
361
|
}
|
|
276
362
|
};
|
|
277
363
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Returns data from cache if available; otherwise executes func, stores the result, and returns it.
|
|
366
|
+
* Uses double-checked locking to prevent cache stampedes. Supports async and callback func signatures.
|
|
367
|
+
* @param {string} key - The cache key.
|
|
368
|
+
* @param {Function} func - Called on cache miss. Use func(callback) for callbacks or async func() for promises.
|
|
369
|
+
* @param {Object} [options] - Optional settings.
|
|
370
|
+
* @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties.
|
|
371
|
+
* @param {Function} [callback] - Optional callback(err, value). Defaults to a noop.
|
|
372
|
+
*/
|
|
373
|
+
this.fetch = (key, func, options = {}, callback) => {
|
|
283
374
|
if (typeof options === 'function') {
|
|
284
375
|
callback = options;
|
|
285
376
|
options = {};
|
|
286
377
|
}
|
|
287
378
|
|
|
288
379
|
// Default callback is a noop
|
|
289
|
-
callback = callback ||
|
|
380
|
+
callback = callback || (() => {});
|
|
290
381
|
|
|
291
382
|
// Try to get value from memory cache
|
|
292
|
-
|
|
383
|
+
let result = getFromMemoryCache(key);
|
|
293
384
|
|
|
294
385
|
// Return value from memory cache if it exists
|
|
295
386
|
if (result.exists) {
|
|
@@ -299,8 +390,8 @@ function PettyCache() {
|
|
|
299
390
|
const _this = this;
|
|
300
391
|
|
|
301
392
|
// Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking
|
|
302
|
-
lock(`fetch-memory-cache-lock-${key}`,
|
|
303
|
-
async.reflect(
|
|
393
|
+
lock(`fetch-memory-cache-lock-${key}`, (releaseMemoryCacheLock) => {
|
|
394
|
+
async.reflect((callback) => {
|
|
304
395
|
// Try to get value from memory cache
|
|
305
396
|
result = getFromMemoryCache(key);
|
|
306
397
|
|
|
@@ -310,7 +401,7 @@ function PettyCache() {
|
|
|
310
401
|
}
|
|
311
402
|
|
|
312
403
|
// Try to get value from Redis
|
|
313
|
-
getFromRedis(key,
|
|
404
|
+
getFromRedis(key, (err, result) => {
|
|
314
405
|
if (err) {
|
|
315
406
|
return callback(err);
|
|
316
407
|
}
|
|
@@ -322,8 +413,8 @@ function PettyCache() {
|
|
|
322
413
|
}
|
|
323
414
|
|
|
324
415
|
// Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking
|
|
325
|
-
lock(`fetch-redis-lock-${key}`,
|
|
326
|
-
async.reflect(
|
|
416
|
+
lock(`fetch-redis-lock-${key}`, (releaseRedisLock) => {
|
|
417
|
+
async.reflect((callback) => {
|
|
327
418
|
// Try to get value from memory cache
|
|
328
419
|
result = getFromMemoryCache(key);
|
|
329
420
|
|
|
@@ -333,7 +424,7 @@ function PettyCache() {
|
|
|
333
424
|
}
|
|
334
425
|
|
|
335
426
|
// Try to get value from Redis
|
|
336
|
-
getFromRedis(key, async
|
|
427
|
+
getFromRedis(key, async (err, result) => {
|
|
337
428
|
if (err) {
|
|
338
429
|
return callback(err);
|
|
339
430
|
}
|
|
@@ -350,7 +441,7 @@ function PettyCache() {
|
|
|
350
441
|
try {
|
|
351
442
|
const data = await func();
|
|
352
443
|
|
|
353
|
-
_this.set(key, data, options,
|
|
444
|
+
_this.set(key, data, options, (err) => {
|
|
354
445
|
callback(err, data);
|
|
355
446
|
});
|
|
356
447
|
} catch(err) {
|
|
@@ -358,18 +449,18 @@ function PettyCache() {
|
|
|
358
449
|
}
|
|
359
450
|
} else {
|
|
360
451
|
// If the function has arguments, there was a callback provided
|
|
361
|
-
func(
|
|
452
|
+
func((err, data) => {
|
|
362
453
|
if (err) {
|
|
363
454
|
return callback(err);
|
|
364
455
|
}
|
|
365
456
|
|
|
366
|
-
_this.set(key, data, options,
|
|
457
|
+
_this.set(key, data, options, (err) => {
|
|
367
458
|
callback(err, data);
|
|
368
459
|
});
|
|
369
460
|
});
|
|
370
461
|
}
|
|
371
462
|
});
|
|
372
|
-
})(releaseRedisLock(
|
|
463
|
+
})(releaseRedisLock((err, result) => {
|
|
373
464
|
if (result.error) {
|
|
374
465
|
return callback(result.error);
|
|
375
466
|
}
|
|
@@ -378,7 +469,7 @@ function PettyCache() {
|
|
|
378
469
|
}));
|
|
379
470
|
});
|
|
380
471
|
});
|
|
381
|
-
})(releaseMemoryCacheLock(
|
|
472
|
+
})(releaseMemoryCacheLock((err, result) => {
|
|
382
473
|
if (result.error) {
|
|
383
474
|
return callback(result.error);
|
|
384
475
|
}
|
|
@@ -388,9 +479,16 @@ function PettyCache() {
|
|
|
388
479
|
});
|
|
389
480
|
};
|
|
390
481
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Like fetch(), but also sets up a background interval to proactively refresh the cached value
|
|
484
|
+
* before it expires, preventing cache misses under sustained load.
|
|
485
|
+
* @param {string} key - The cache key.
|
|
486
|
+
* @param {Function} func - Called on cache miss and on each refresh interval: func(callback).
|
|
487
|
+
* @param {Object} [options] - Optional settings.
|
|
488
|
+
* @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties.
|
|
489
|
+
* @param {Function} [callback] - Optional callback(err, value). Defaults to a noop.
|
|
490
|
+
*/
|
|
491
|
+
this.fetchAndRefresh = (key, func, options = {}, callback) => {
|
|
394
492
|
if (typeof options === 'function') {
|
|
395
493
|
callback = options;
|
|
396
494
|
options = {};
|
|
@@ -400,22 +498,22 @@ function PettyCache() {
|
|
|
400
498
|
const ttl = getTtl(options);
|
|
401
499
|
|
|
402
500
|
// Default callback is a noop
|
|
403
|
-
callback = callback ||
|
|
501
|
+
callback = callback || (() => {});
|
|
404
502
|
|
|
405
503
|
const _this = this;
|
|
406
504
|
|
|
407
505
|
if (!intervals[key]) {
|
|
408
506
|
const delay = ttl.min / 2;
|
|
409
507
|
|
|
410
|
-
intervals[key] = setInterval(
|
|
508
|
+
intervals[key] = setInterval(() => {
|
|
411
509
|
// This distributed lock prevents multiple clients from executing func at the same time
|
|
412
|
-
_this.mutex.lock(`interval-${key}`, { ttl: delay - 100 },
|
|
510
|
+
_this.mutex.lock(`interval-${key}`, { ttl: delay - 100 }, (err) => {
|
|
413
511
|
if (err) {
|
|
414
512
|
return;
|
|
415
513
|
}
|
|
416
514
|
|
|
417
515
|
// Execute the specified function and update cache
|
|
418
|
-
func(
|
|
516
|
+
func((err, data) => {
|
|
419
517
|
if (err) {
|
|
420
518
|
return;
|
|
421
519
|
}
|
|
@@ -429,58 +527,83 @@ function PettyCache() {
|
|
|
429
527
|
this.fetch(key, func, options, callback);
|
|
430
528
|
};
|
|
431
529
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking
|
|
442
|
-
lock(`get-memory-cache-lock-${key}`, function(releaseMemoryCacheLock) {
|
|
443
|
-
async.reflect(function(callback) {
|
|
530
|
+
/**
|
|
531
|
+
* Gets a cached value. Supports both callback and promise styles.
|
|
532
|
+
* @param {string} key - The cache key.
|
|
533
|
+
* @param {Function} [callback] - Optional callback(err, value). If omitted, returns a Promise.
|
|
534
|
+
* @returns {Promise|undefined} Resolves with the cached value, or null if not found.
|
|
535
|
+
*/
|
|
536
|
+
this.get = (key, callback) => {
|
|
537
|
+
const executor = () => {
|
|
538
|
+
return new Promise((resolve, reject) => {
|
|
444
539
|
// Try to get value from memory cache
|
|
445
|
-
result = getFromMemoryCache(key);
|
|
540
|
+
let result = getFromMemoryCache(key);
|
|
446
541
|
|
|
447
542
|
// Return value from memory cache if it exists
|
|
448
543
|
if (result.exists) {
|
|
449
|
-
return
|
|
544
|
+
return resolve(result.value);
|
|
450
545
|
}
|
|
451
546
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
547
|
+
// Double-checked locking: http://en.wikipedia.org/wiki/Double-checked_locking
|
|
548
|
+
lock(`get-memory-cache-lock-${key}`, (releaseMemoryCacheLock) => {
|
|
549
|
+
async.reflect((callback) => {
|
|
550
|
+
// Try to get value from memory cache
|
|
551
|
+
result = getFromMemoryCache(key);
|
|
456
552
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
553
|
+
// Return value from memory cache if it exists
|
|
554
|
+
if (result.exists) {
|
|
555
|
+
return callback(null, result.value);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
getFromRedis(key, (err, result) => {
|
|
559
|
+
if (err) {
|
|
560
|
+
return callback(err);
|
|
561
|
+
}
|
|
460
562
|
|
|
461
|
-
|
|
462
|
-
|
|
563
|
+
if (!result.exists) {
|
|
564
|
+
return callback(null, null);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
memoryCache.put(key, result.value, random(2000, 5000));
|
|
568
|
+
callback(null, result.value);
|
|
569
|
+
});
|
|
570
|
+
})(releaseMemoryCacheLock((err, result) => {
|
|
571
|
+
if (result.error) {
|
|
572
|
+
return reject(result.error);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
resolve(result.value);
|
|
576
|
+
}));
|
|
463
577
|
});
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
return callback(result.error);
|
|
467
|
-
}
|
|
578
|
+
});
|
|
579
|
+
};
|
|
468
580
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
581
|
+
if (callback) {
|
|
582
|
+
executor().then(result => callback(null, result)).catch(callback);
|
|
583
|
+
} else {
|
|
584
|
+
return executor();
|
|
585
|
+
}
|
|
472
586
|
};
|
|
473
587
|
|
|
474
588
|
this.mutex = {
|
|
475
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Acquires a distributed mutex lock in Redis. Supports both callback and promise styles.
|
|
591
|
+
* @param {string} key - The lock key.
|
|
592
|
+
* @param {Object} [options] - Optional settings.
|
|
593
|
+
* @param {number} [options.ttl=1000] - Lock TTL in ms.
|
|
594
|
+
* @param {Object} [options.retry] - Retry options.
|
|
595
|
+
* @param {number} [options.retry.times=1] - Number of acquisition attempts.
|
|
596
|
+
* @param {number} [options.retry.interval=100] - Delay between retries in ms.
|
|
597
|
+
* @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise.
|
|
598
|
+
* @returns {Promise|undefined}
|
|
599
|
+
*/
|
|
600
|
+
lock: (key, options = {}, callback) => {
|
|
476
601
|
// Options are optional
|
|
477
602
|
if (!callback && typeof options === 'function') {
|
|
478
603
|
callback = options;
|
|
479
604
|
options = {};
|
|
480
605
|
}
|
|
481
606
|
|
|
482
|
-
options = options || {};
|
|
483
|
-
|
|
484
607
|
options.retry = Object.hasOwn(options, 'retry') ? options.retry : {};
|
|
485
608
|
options.retry.interval = Object.hasOwn(options.retry, 'interval') ? options.retry.interval : 100;
|
|
486
609
|
options.retry.times = Object.hasOwn(options.retry, 'times') ? options.retry.times : 1;
|
|
@@ -489,7 +612,7 @@ function PettyCache() {
|
|
|
489
612
|
const executor = () => {
|
|
490
613
|
return new Promise((resolve, reject) => {
|
|
491
614
|
async.retry({ interval: options.retry.interval, times: options.retry.times }, callback => {
|
|
492
|
-
redisClient.set(key, '1', 'NX', 'PX', options.ttl,
|
|
615
|
+
redisClient.set(key, '1', 'NX', 'PX', options.ttl, (err, res) => {
|
|
493
616
|
if (err) {
|
|
494
617
|
return callback(err);
|
|
495
618
|
}
|
|
@@ -504,7 +627,7 @@ function PettyCache() {
|
|
|
504
627
|
|
|
505
628
|
callback();
|
|
506
629
|
});
|
|
507
|
-
},
|
|
630
|
+
}, (err) => {
|
|
508
631
|
if (err) {
|
|
509
632
|
return reject(err);
|
|
510
633
|
}
|
|
@@ -520,10 +643,16 @@ function PettyCache() {
|
|
|
520
643
|
return executor();
|
|
521
644
|
}
|
|
522
645
|
},
|
|
646
|
+
/**
|
|
647
|
+
* Releases a distributed mutex lock in Redis. Supports both callback and promise styles.
|
|
648
|
+
* @param {string} key - The lock key to release.
|
|
649
|
+
* @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise.
|
|
650
|
+
* @returns {Promise|undefined}
|
|
651
|
+
*/
|
|
523
652
|
unlock: (key, callback) => {
|
|
524
653
|
const executor = () => {
|
|
525
654
|
return new Promise((resolve, reject) => {
|
|
526
|
-
redisClient.del(key,
|
|
655
|
+
redisClient.del(key, (err) => {
|
|
527
656
|
if (err) {
|
|
528
657
|
return reject(err);
|
|
529
658
|
}
|
|
@@ -541,41 +670,75 @@ function PettyCache() {
|
|
|
541
670
|
}
|
|
542
671
|
};
|
|
543
672
|
|
|
544
|
-
|
|
545
|
-
|
|
673
|
+
/**
|
|
674
|
+
* Updates specific properties of a cached object without replacing the whole value.
|
|
675
|
+
* Supports both callback and promise styles.
|
|
676
|
+
* @param {string} key - The cache key of the object to patch.
|
|
677
|
+
* @param {Object} value - Properties to merge into the cached object.
|
|
678
|
+
* @param {Object} [options] - Optional settings passed to set().
|
|
679
|
+
* @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties.
|
|
680
|
+
* @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise.
|
|
681
|
+
* @returns {Promise|undefined}
|
|
682
|
+
*/
|
|
683
|
+
this.patch = (key, value, options = {}, callback) => {
|
|
684
|
+
if (!callback && typeof options === 'function') {
|
|
546
685
|
callback = options;
|
|
547
686
|
options = {};
|
|
548
687
|
}
|
|
549
688
|
|
|
550
689
|
const _this = this;
|
|
551
690
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
691
|
+
const executor = () => {
|
|
692
|
+
return new Promise((resolve, reject) => {
|
|
693
|
+
_this.get(key, (err, data) => {
|
|
694
|
+
if (err) {
|
|
695
|
+
return reject(err);
|
|
696
|
+
}
|
|
556
697
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
698
|
+
if (!data) {
|
|
699
|
+
return reject(new Error(`Key ${key} does not exist`));
|
|
700
|
+
}
|
|
560
701
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
702
|
+
for (let k in value) {
|
|
703
|
+
data[k] = value[k];
|
|
704
|
+
}
|
|
564
705
|
|
|
565
|
-
|
|
566
|
-
|
|
706
|
+
_this.set(key, data, options, (err) => {
|
|
707
|
+
if (err) {
|
|
708
|
+
return reject(err);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
resolve();
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
if (callback) {
|
|
718
|
+
executor().then(result => callback(null, result)).catch(callback);
|
|
719
|
+
} else {
|
|
720
|
+
return executor();
|
|
721
|
+
}
|
|
567
722
|
};
|
|
568
723
|
|
|
569
724
|
this.semaphore = {
|
|
570
|
-
|
|
725
|
+
/**
|
|
726
|
+
* Acquires a slot in an existing semaphore pool. Retries if no slot is currently available.
|
|
727
|
+
* @param {string} key - The semaphore key.
|
|
728
|
+
* @param {Object} [options] - Optional settings.
|
|
729
|
+
* @param {number} [options.ttl=1000] - Slot TTL in ms; expired slots may be reclaimed.
|
|
730
|
+
* @param {Object} [options.retry] - Retry options.
|
|
731
|
+
* @param {number} [options.retry.times=1] - Number of acquisition attempts.
|
|
732
|
+
* @param {number} [options.retry.interval=100] - Delay between retries in ms.
|
|
733
|
+
* @param {Function} callback - callback(err, index) where index is the acquired slot number.
|
|
734
|
+
*/
|
|
735
|
+
acquireLock: (key, options = {}, callback) => {
|
|
571
736
|
// Options are optional
|
|
572
737
|
if (!callback && typeof options === 'function') {
|
|
573
738
|
callback = options;
|
|
574
739
|
options = {};
|
|
575
740
|
}
|
|
576
741
|
|
|
577
|
-
options = options || {};
|
|
578
|
-
|
|
579
742
|
options.retry = Object.prototype.hasOwnProperty.call(options, 'retry') ? options.retry : {};
|
|
580
743
|
options.retry.interval = Object.prototype.hasOwnProperty.call(options.retry, 'interval') ? options.retry.interval : 100;
|
|
581
744
|
options.retry.times = Object.prototype.hasOwnProperty.call(options.retry, 'times') ? options.retry.times : 1;
|
|
@@ -583,14 +746,14 @@ function PettyCache() {
|
|
|
583
746
|
|
|
584
747
|
const _this = this;
|
|
585
748
|
|
|
586
|
-
async.retry({ interval: options.retry.interval, times: options.retry.times },
|
|
749
|
+
async.retry({ interval: options.retry.interval, times: options.retry.times }, (callback) => {
|
|
587
750
|
// Mutex lock around semaphore
|
|
588
|
-
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } },
|
|
751
|
+
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, (err) => {
|
|
589
752
|
if (err) {
|
|
590
753
|
return callback(err);
|
|
591
754
|
}
|
|
592
755
|
|
|
593
|
-
redisClient.get(key,
|
|
756
|
+
redisClient.get(key, (err, data) => {
|
|
594
757
|
// If we encountered an error, unlock the mutex lock and return error
|
|
595
758
|
if (err) {
|
|
596
759
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
@@ -601,10 +764,10 @@ function PettyCache() {
|
|
|
601
764
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); });
|
|
602
765
|
}
|
|
603
766
|
|
|
604
|
-
|
|
767
|
+
const pool = JSON.parse(data);
|
|
605
768
|
|
|
606
769
|
// Try to find a slot that's available.
|
|
607
|
-
|
|
770
|
+
let index = pool.findIndex(s => s.status === 'available');
|
|
608
771
|
|
|
609
772
|
if (index === -1) {
|
|
610
773
|
index = pool.findIndex(s => s.ttl <= Date.now());
|
|
@@ -617,7 +780,7 @@ function PettyCache() {
|
|
|
617
780
|
|
|
618
781
|
pool[index] = { status: 'acquired', ttl: Date.now() + options.ttl };
|
|
619
782
|
|
|
620
|
-
redisClient.set(key, JSON.stringify(pool),
|
|
783
|
+
redisClient.set(key, JSON.stringify(pool), (err) => {
|
|
621
784
|
if (err) {
|
|
622
785
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
623
786
|
}
|
|
@@ -628,18 +791,25 @@ function PettyCache() {
|
|
|
628
791
|
});
|
|
629
792
|
}, callback);
|
|
630
793
|
},
|
|
631
|
-
|
|
632
|
-
|
|
794
|
+
/**
|
|
795
|
+
* Permanently consumes a semaphore slot, marking it consumed rather than available.
|
|
796
|
+
* Ensures at least one slot always remains non-consumed.
|
|
797
|
+
* @param {string} key - The semaphore key.
|
|
798
|
+
* @param {number} index - The slot index to consume.
|
|
799
|
+
* @param {Function} [callback] - Optional callback(err). Defaults to a noop.
|
|
800
|
+
*/
|
|
801
|
+
consumeLock: (key, index, callback) => {
|
|
802
|
+
callback = callback || (() => {});
|
|
633
803
|
|
|
634
804
|
const _this = this;
|
|
635
805
|
|
|
636
806
|
// Mutex lock around semaphore
|
|
637
|
-
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } },
|
|
807
|
+
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, (err) => {
|
|
638
808
|
if (err) {
|
|
639
809
|
return callback(err);
|
|
640
810
|
}
|
|
641
811
|
|
|
642
|
-
redisClient.get(key,
|
|
812
|
+
redisClient.get(key, (err, data) => {
|
|
643
813
|
// If we encountered an error, unlock the mutex lock and return error
|
|
644
814
|
if (err) {
|
|
645
815
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
@@ -650,7 +820,7 @@ function PettyCache() {
|
|
|
650
820
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); });
|
|
651
821
|
}
|
|
652
822
|
|
|
653
|
-
|
|
823
|
+
const pool = JSON.parse(data);
|
|
654
824
|
|
|
655
825
|
// Ensure index exists.
|
|
656
826
|
if (pool.length <= index) {
|
|
@@ -664,7 +834,7 @@ function PettyCache() {
|
|
|
664
834
|
pool[index] = { status: 'available' };
|
|
665
835
|
}
|
|
666
836
|
|
|
667
|
-
redisClient.set(key, JSON.stringify(pool),
|
|
837
|
+
redisClient.set(key, JSON.stringify(pool), (err) => {
|
|
668
838
|
if (err) {
|
|
669
839
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
670
840
|
}
|
|
@@ -674,17 +844,23 @@ function PettyCache() {
|
|
|
674
844
|
});
|
|
675
845
|
});
|
|
676
846
|
},
|
|
677
|
-
|
|
678
|
-
|
|
847
|
+
/**
|
|
848
|
+
* Increases the size of an existing semaphore pool. Cannot shrink a pool.
|
|
849
|
+
* @param {string} key - The semaphore key.
|
|
850
|
+
* @param {number} size - The desired pool size (must be >= current size).
|
|
851
|
+
* @param {Function} [callback] - Optional callback(err). Defaults to a noop.
|
|
852
|
+
*/
|
|
853
|
+
expand: (key, size, callback) => {
|
|
854
|
+
callback = callback || (() => {});
|
|
679
855
|
|
|
680
856
|
const _this = this;
|
|
681
857
|
|
|
682
|
-
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } },
|
|
858
|
+
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, (err) => {
|
|
683
859
|
if (err) {
|
|
684
860
|
return callback(err);
|
|
685
861
|
}
|
|
686
862
|
|
|
687
|
-
redisClient.get(key,
|
|
863
|
+
redisClient.get(key, (err, data) => {
|
|
688
864
|
// If we encountered an error, unlock the mutex lock and return error
|
|
689
865
|
if (err) {
|
|
690
866
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
@@ -695,7 +871,7 @@ function PettyCache() {
|
|
|
695
871
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); });
|
|
696
872
|
}
|
|
697
873
|
|
|
698
|
-
|
|
874
|
+
let pool = JSON.parse(data);
|
|
699
875
|
|
|
700
876
|
if (pool.length > size) {
|
|
701
877
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Cannot shrink pool, size is ${pool.length} and you requested a size of ${size}.`)); });
|
|
@@ -707,7 +883,7 @@ function PettyCache() {
|
|
|
707
883
|
|
|
708
884
|
pool = pool.concat(Array(size - pool.length).fill({ status: 'available' }));
|
|
709
885
|
|
|
710
|
-
redisClient.set(key, JSON.stringify(pool),
|
|
886
|
+
redisClient.set(key, JSON.stringify(pool), (err) => {
|
|
711
887
|
if (err) {
|
|
712
888
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
713
889
|
}
|
|
@@ -717,18 +893,24 @@ function PettyCache() {
|
|
|
717
893
|
});
|
|
718
894
|
});
|
|
719
895
|
},
|
|
720
|
-
|
|
721
|
-
|
|
896
|
+
/**
|
|
897
|
+
* Releases an acquired semaphore slot, marking it available again.
|
|
898
|
+
* @param {string} key - The semaphore key.
|
|
899
|
+
* @param {number} index - The slot index to release.
|
|
900
|
+
* @param {Function} [callback] - Optional callback(err). Defaults to a noop.
|
|
901
|
+
*/
|
|
902
|
+
releaseLock: (key, index, callback) => {
|
|
903
|
+
callback = callback || (() => {});
|
|
722
904
|
|
|
723
905
|
const _this = this;
|
|
724
906
|
|
|
725
907
|
// Mutex lock around semaphore
|
|
726
|
-
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } },
|
|
908
|
+
_this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, (err) => {
|
|
727
909
|
if (err) {
|
|
728
910
|
return callback(err);
|
|
729
911
|
}
|
|
730
912
|
|
|
731
|
-
redisClient.get(key,
|
|
913
|
+
redisClient.get(key, (err, data) => {
|
|
732
914
|
// If we encountered an error, unlock the mutex lock and return error
|
|
733
915
|
if (err) {
|
|
734
916
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
@@ -739,7 +921,7 @@ function PettyCache() {
|
|
|
739
921
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); });
|
|
740
922
|
}
|
|
741
923
|
|
|
742
|
-
|
|
924
|
+
const pool = JSON.parse(data);
|
|
743
925
|
|
|
744
926
|
// Ensure index exists.
|
|
745
927
|
if (pool.length <= index) {
|
|
@@ -748,7 +930,7 @@ function PettyCache() {
|
|
|
748
930
|
|
|
749
931
|
pool[index] = { status: 'available' };
|
|
750
932
|
|
|
751
|
-
redisClient.set(key, JSON.stringify(pool),
|
|
933
|
+
redisClient.set(key, JSON.stringify(pool), (err) => {
|
|
752
934
|
if (err) {
|
|
753
935
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
754
936
|
}
|
|
@@ -758,19 +940,24 @@ function PettyCache() {
|
|
|
758
940
|
});
|
|
759
941
|
});
|
|
760
942
|
},
|
|
761
|
-
|
|
762
|
-
|
|
943
|
+
/**
|
|
944
|
+
* Resets all slots in an existing semaphore pool to available.
|
|
945
|
+
* @param {string} key - The semaphore key.
|
|
946
|
+
* @param {Function} [callback] - Optional callback(err, pool). Defaults to a noop.
|
|
947
|
+
*/
|
|
948
|
+
reset: (key, callback) => {
|
|
949
|
+
callback = callback || (() => {});
|
|
763
950
|
|
|
764
951
|
const _this = this;
|
|
765
952
|
|
|
766
953
|
// Mutex lock around semaphore
|
|
767
|
-
this.mutex.lock(`lock:${key}`, { retry: { times: 100 } },
|
|
954
|
+
this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, (err) => {
|
|
768
955
|
if (err) {
|
|
769
956
|
return callback(err);
|
|
770
957
|
}
|
|
771
958
|
|
|
772
959
|
// Try to get previously created semaphore
|
|
773
|
-
redisClient.get(key,
|
|
960
|
+
redisClient.get(key, (err, data) => {
|
|
774
961
|
// If we encountered an error, unlock the mutex lock and return error
|
|
775
962
|
if (err) {
|
|
776
963
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
@@ -781,10 +968,10 @@ function PettyCache() {
|
|
|
781
968
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(new Error(`Semaphore ${key} doesn't exist.`)); });
|
|
782
969
|
}
|
|
783
970
|
|
|
784
|
-
|
|
971
|
+
let pool = JSON.parse(data);
|
|
785
972
|
pool = Array(pool.length).fill({ status: 'available' });
|
|
786
973
|
|
|
787
|
-
redisClient.set(key, JSON.stringify(pool),
|
|
974
|
+
redisClient.set(key, JSON.stringify(pool), (err) => {
|
|
788
975
|
if (err) {
|
|
789
976
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
790
977
|
}
|
|
@@ -794,26 +981,32 @@ function PettyCache() {
|
|
|
794
981
|
});
|
|
795
982
|
});
|
|
796
983
|
},
|
|
797
|
-
|
|
984
|
+
/**
|
|
985
|
+
* Retrieves an existing semaphore pool, or creates one if it doesn't exist.
|
|
986
|
+
* @param {string} key - The semaphore key.
|
|
987
|
+
* @param {Object} [options] - Optional settings.
|
|
988
|
+
* @param {number|Function} [options.size=1] - Pool size, or a function(callback) that resolves the size.
|
|
989
|
+
* @param {Function} [callback] - Optional callback(err, pool). Defaults to a noop.
|
|
990
|
+
*/
|
|
991
|
+
retrieveOrCreate: (key, options = {}, callback) => {
|
|
798
992
|
// Options are optional
|
|
799
993
|
if (!callback && typeof options === 'function') {
|
|
800
994
|
callback = options;
|
|
801
995
|
options = {};
|
|
802
996
|
}
|
|
803
997
|
|
|
804
|
-
callback = callback ||
|
|
805
|
-
options = options || {};
|
|
998
|
+
callback = callback || (() => {});
|
|
806
999
|
|
|
807
1000
|
const _this = this;
|
|
808
1001
|
|
|
809
1002
|
// Mutex lock around semaphore retrival or creation
|
|
810
|
-
this.mutex.lock(`lock:${key}`, { retry: { times: 100 } },
|
|
1003
|
+
this.mutex.lock(`lock:${key}`, { retry: { times: 100 } }, (err) => {
|
|
811
1004
|
if (err) {
|
|
812
1005
|
return callback(err);
|
|
813
1006
|
}
|
|
814
1007
|
|
|
815
1008
|
// Try to get previously created semaphore
|
|
816
|
-
redisClient.get(key,
|
|
1009
|
+
redisClient.get(key, (err, data) => {
|
|
817
1010
|
// If we encountered an error, unlock the mutex lock and return error
|
|
818
1011
|
if (err) {
|
|
819
1012
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
@@ -824,7 +1017,7 @@ function PettyCache() {
|
|
|
824
1017
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(null, JSON.parse(data)); });
|
|
825
1018
|
}
|
|
826
1019
|
|
|
827
|
-
|
|
1020
|
+
const getSize = (callback) => {
|
|
828
1021
|
if (typeof options.size === 'function') {
|
|
829
1022
|
return options.size(callback);
|
|
830
1023
|
}
|
|
@@ -832,15 +1025,15 @@ function PettyCache() {
|
|
|
832
1025
|
callback(null, Object.prototype.hasOwnProperty.call(options, 'size') ? options.size : 1);
|
|
833
1026
|
};
|
|
834
1027
|
|
|
835
|
-
getSize(
|
|
1028
|
+
getSize((err, size) => {
|
|
836
1029
|
// If we encountered an error, unlock the mutex lock and return error
|
|
837
1030
|
if (err) {
|
|
838
1031
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
839
1032
|
}
|
|
840
1033
|
|
|
841
|
-
|
|
1034
|
+
const pool = Array(Math.max(size, 1)).fill({ status: 'available' });
|
|
842
1035
|
|
|
843
|
-
redisClient.set(key, JSON.stringify(pool),
|
|
1036
|
+
redisClient.set(key, JSON.stringify(pool), (err) => {
|
|
844
1037
|
if (err) {
|
|
845
1038
|
return _this.mutex.unlock(`lock:${key}`, () => { callback(err); });
|
|
846
1039
|
}
|
|
@@ -853,9 +1046,16 @@ function PettyCache() {
|
|
|
853
1046
|
}
|
|
854
1047
|
};
|
|
855
1048
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1049
|
+
/**
|
|
1050
|
+
* Stores a value in both the memory cache and Redis. Supports both callback and promise styles.
|
|
1051
|
+
* @param {string} key - The cache key.
|
|
1052
|
+
* @param {*} value - The value to cache.
|
|
1053
|
+
* @param {Object} [options] - Optional settings.
|
|
1054
|
+
* @param {number|Object} [options.ttl] - TTL in ms, or object with min/max properties.
|
|
1055
|
+
* @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise.
|
|
1056
|
+
* @returns {Promise|undefined}
|
|
1057
|
+
*/
|
|
1058
|
+
this.set = (key, value, options = {}, callback) => {
|
|
859
1059
|
if (typeof options === 'function') {
|
|
860
1060
|
callback = options;
|
|
861
1061
|
options = {};
|
|
@@ -864,22 +1064,41 @@ function PettyCache() {
|
|
|
864
1064
|
// Get TTL based on specified options
|
|
865
1065
|
const ttl = getTtl(options);
|
|
866
1066
|
|
|
867
|
-
|
|
868
|
-
|
|
1067
|
+
const executor = () => {
|
|
1068
|
+
return new Promise((resolve, reject) => {
|
|
1069
|
+
// Store value in memory cache with a short expiration
|
|
1070
|
+
memoryCache.put(key, value, random(2000, 5000));
|
|
869
1071
|
|
|
870
|
-
|
|
871
|
-
|
|
1072
|
+
// Store value in Redis
|
|
1073
|
+
redisClient.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value), (err) => {
|
|
1074
|
+
if (err) {
|
|
1075
|
+
return reject(err);
|
|
1076
|
+
}
|
|
872
1077
|
|
|
873
|
-
|
|
874
|
-
|
|
1078
|
+
resolve();
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
if (callback) {
|
|
1084
|
+
executor().then(result => callback(null, result)).catch(callback);
|
|
1085
|
+
} else {
|
|
1086
|
+
return executor();
|
|
1087
|
+
}
|
|
875
1088
|
};
|
|
876
1089
|
|
|
877
1090
|
// Semaphore functions need to be bound to the main PettyCache object
|
|
878
|
-
for (
|
|
1091
|
+
for (const method in this.semaphore) {
|
|
879
1092
|
this.semaphore[method] = this.semaphore[method].bind(this);
|
|
880
1093
|
}
|
|
881
1094
|
}
|
|
882
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* Returns a random integer between min and max, inclusive.
|
|
1098
|
+
* @param {number} min
|
|
1099
|
+
* @param {number} max
|
|
1100
|
+
* @returns {number}
|
|
1101
|
+
*/
|
|
883
1102
|
function random(min, max) {
|
|
884
1103
|
if (min === max) {
|
|
885
1104
|
return min;
|
|
@@ -888,8 +1107,13 @@ function random(min, max) {
|
|
|
888
1107
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
889
1108
|
}
|
|
890
1109
|
|
|
891
|
-
|
|
892
|
-
|
|
1110
|
+
/**
|
|
1111
|
+
* Parses a JSON string produced by PettyCache.stringify(), restoring NaN, null, and undefined.
|
|
1112
|
+
* @param {string} text
|
|
1113
|
+
* @returns {*}
|
|
1114
|
+
*/
|
|
1115
|
+
PettyCache.parse = (text) => {
|
|
1116
|
+
return JSON.parse(text, (k, v) => {
|
|
893
1117
|
if (v === '__NaN') {
|
|
894
1118
|
return NaN;
|
|
895
1119
|
} else if (v === '__null') {
|
|
@@ -902,8 +1126,14 @@ PettyCache.parse = function(text) {
|
|
|
902
1126
|
});
|
|
903
1127
|
};
|
|
904
1128
|
|
|
905
|
-
|
|
906
|
-
|
|
1129
|
+
/**
|
|
1130
|
+
* Serializes a value to JSON, encoding NaN, null, and undefined as sentinel strings
|
|
1131
|
+
* so they survive a Redis round-trip and can be restored by PettyCache.parse().
|
|
1132
|
+
* @param {*} value
|
|
1133
|
+
* @returns {string}
|
|
1134
|
+
*/
|
|
1135
|
+
PettyCache.stringify = (value) => {
|
|
1136
|
+
return JSON.stringify(value, (k, v) => {
|
|
907
1137
|
if (typeof v === 'number' && isNaN(v)) {
|
|
908
1138
|
return '__NaN';
|
|
909
1139
|
} else if (v === null) {
|