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/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, function(err, data) {
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 (var i = 0; i < keys.length; i++) {
29
- var key = keys[i];
30
- var value = data[i];
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, function(err, data) {
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
- * @param {Array} keys - An array of keys.
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 = function(keys, func, options, callback) {
107
- // Options are optional
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
- // If there aren't any keys, return
114
- if (!keys.length) {
115
- return callback(null, {});
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
- const _keys = Array.from(new Set(keys));
119
- const values = {};
152
+ const _keys = Array.from(new Set(keys));
153
+ const values = {};
120
154
 
121
- // Try to get values from memory cache
122
- for (var i = _keys.length - 1; i >= 0; i--) {
123
- const key = _keys[i];
124
- const result = getFromMemoryCache(key);
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
- if (result.exists) {
127
- values[key] = result.value;
128
- _keys.splice(i, 1);
129
- }
130
- }
160
+ if (result.exists) {
161
+ values[key] = result.value;
162
+ _keys.splice(i, 1);
163
+ }
164
+ }
131
165
 
132
- // If there aren't any keys left, return
133
- if (!_keys.length) {
134
- return callback(null, values);
135
- }
166
+ // If there aren't any keys left, return
167
+ if (!_keys.length) {
168
+ return resolve(values);
169
+ }
136
170
 
137
- const _this = this;
171
+ // Try to get values from Redis
172
+ bulkGetFromRedis(_keys, (err, results) => {
173
+ if (err) {
174
+ return reject(err);
175
+ }
138
176
 
139
- // Try to get values from Redis
140
- bulkGetFromRedis(_keys, function(err, results) {
141
- if (err) {
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
- for (var i = _keys.length - 1; i >= 0; i--) {
146
- const key = _keys[i];
147
- const result = results[key];
181
+ if (result.exists) {
182
+ _keys.splice(i, 1);
183
+ values[key] = result.value;
148
184
 
149
- if (result.exists) {
150
- _keys.splice(i, 1);
151
- values[key] = result.value;
185
+ // Store value in memory cache with a short expiration
186
+ memoryCache.put(key, result.value, random(2000, 5000));
187
+ }
188
+ }
152
189
 
153
- // Store value in memory cache with a short expiration
154
- memoryCache.put(key, result.value, random(2000, 5000));
155
- }
156
- }
190
+ // If there aren't any keys left, return
191
+ if (!_keys.length) {
192
+ return resolve(values);
193
+ }
157
194
 
158
- // If there aren't any keys left, return
159
- if (!_keys.length) {
160
- return callback(null, values);
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
- // Execute the specified function for remaining keys
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
- Object.keys(data).forEach(key => values[key] = data[key]);
203
+ this.bulkSet(data, options, err => {
204
+ if (err) {
205
+ return reject(err);
206
+ }
170
207
 
171
- _this.bulkSet(data, options, err => callback(err, values));
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
- * @param {Array} keys - An array of keys.
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 = function(keys, callback) {
180
- // If there aren't any keys, return
181
- if (!keys.length) {
182
- return callback(null, {});
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
- const _keys = Array.from(new Set(keys));
186
- const values = {};
236
+ const _keys = Array.from(new Set(keys));
237
+ const values = {};
187
238
 
188
- // Try to get values from memory cache
189
- for (var i = _keys.length - 1; i >= 0; i--) {
190
- const key = _keys[i];
191
- const result = getFromMemoryCache(key);
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
- if (result.exists) {
194
- values[key] = result.value;
195
- _keys.splice(i, 1);
196
- }
197
- }
244
+ if (result.exists) {
245
+ values[key] = result.value;
246
+ _keys.splice(i, 1);
247
+ }
248
+ }
198
249
 
199
- // If there aren't any keys left, return
200
- if (!_keys.length) {
201
- return callback(null, values);
202
- }
250
+ // If there aren't any keys left, return
251
+ if (!_keys.length) {
252
+ return resolve(values);
253
+ }
203
254
 
204
- // Try to get values from Redis
205
- bulkGetFromRedis(_keys, function(err, results) {
206
- if (err) {
207
- return callback(err);
208
- }
255
+ // Try to get values from Redis
256
+ bulkGetFromRedis(_keys, (err, results) => {
257
+ if (err) {
258
+ return reject(err);
259
+ }
209
260
 
210
- for (var i = 0; i < _keys.length; i++) {
211
- var key = _keys[i];
212
- var result = results[key];
261
+ for (let i = 0; i < _keys.length; i++) {
262
+ const key = _keys[i];
263
+ const result = results[key];
213
264
 
214
- if (!result.exists) {
215
- values[key] = null;
216
- continue;
217
- }
265
+ if (!result.exists) {
266
+ values[key] = null;
267
+ continue;
268
+ }
218
269
 
219
- values[key] = result.value;
270
+ values[key] = result.value;
220
271
 
221
- // Store value in memory cache with a short expiration
222
- memoryCache.put(key, result.value, random(2000, 5000));
223
- }
272
+ // Store value in memory cache with a short expiration
273
+ memoryCache.put(key, result.value, random(2000, 5000));
274
+ }
224
275
 
225
- callback(null, values);
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
- this.bulkSet = function(values, options, callback) {
230
- // Options are optional
231
- if (!callback) {
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
- // Get TTL based on specified options
237
- const ttl = getTtl(options);
302
+ const executor = () => {
303
+ return new Promise((resolve, reject) => {
304
+ // Get TTL based on specified options
305
+ const ttl = getTtl(options);
238
306
 
239
- // Redis does not have a MSETEX command so we batch commands: http://redis.js.org/#api-clientbatchcommands
240
- const batch = redisClient.batch();
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
- Object.keys(values).forEach(key => {
243
- const value = values[key];
310
+ Object.keys(values).forEach(key => {
311
+ const value = values[key];
244
312
 
245
- // Store value in memory cache with a short expiration
246
- memoryCache.put(key, value, random(2000, 5000));
313
+ // Store value in memory cache with a short expiration
314
+ memoryCache.put(key, value, random(2000, 5000));
247
315
 
248
- // Add Redis command
249
- batch.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value));
250
- });
316
+ // Add Redis command
317
+ batch.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value));
318
+ });
251
319
 
252
- batch.exec(function(err) {
253
- callback(err);
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
- this.del = function(key, callback) {
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, function(err) {
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
- // Returns data from cache if available;
279
- // otherwise executes the specified function and places the results in cache before returning the data.
280
- this.fetch = function(key, func, options, callback) {
281
- options = options || {};
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 || function() {};
380
+ callback = callback || (() => {});
290
381
 
291
382
  // Try to get value from memory cache
292
- var result = getFromMemoryCache(key);
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}`, function(releaseMemoryCacheLock) {
303
- async.reflect(function(callback) {
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, function(err, result) {
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}`, function(releaseRedisLock) {
326
- async.reflect(function(callback) {
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 function(err, result) {
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, function(err) {
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(function(err, data) {
452
+ func((err, data) => {
362
453
  if (err) {
363
454
  return callback(err);
364
455
  }
365
456
 
366
- _this.set(key, data, options, function(err) {
457
+ _this.set(key, data, options, (err) => {
367
458
  callback(err, data);
368
459
  });
369
460
  });
370
461
  }
371
462
  });
372
- })(releaseRedisLock(function(err, result) {
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(function(err, result) {
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
- this.fetchAndRefresh = function(key, func, options, callback) {
392
- options = options || {};
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 || function() {};
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(function() {
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 }, function(err) {
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(function(err, data) {
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
- this.get = function(key, callback) {
433
- // Try to get value from memory cache
434
- let result = getFromMemoryCache(key);
435
-
436
- // Return value from memory cache if it exists
437
- if (result.exists) {
438
- return callback(null, result.value);
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 callback(null, result.value);
544
+ return resolve(result.value);
450
545
  }
451
546
 
452
- getFromRedis(key, function(err, result) {
453
- if (err) {
454
- return callback(err);
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
- if (!result.exists) {
458
- return callback(null, null);
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
- memoryCache.put(key, result.value, random(2000, 5000));
462
- callback(null, result.value);
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
- })(releaseMemoryCacheLock(function(err, result) {
465
- if (result.error) {
466
- return callback(result.error);
467
- }
578
+ });
579
+ };
468
580
 
469
- callback(null, result.value);
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
- lock: (key, options, callback) => {
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, function(err, res) {
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
- }, function(err) {
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, function(err) {
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
- this.patch = function(key, value, options, callback) {
545
- if (!callback) {
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
- this.get(key, function(err, data) {
553
- if (err) {
554
- return callback(err);
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
- if (!data) {
558
- return callback(new Error(`Key ${key} does not exist`));
559
- }
698
+ if (!data) {
699
+ return reject(new Error(`Key ${key} does not exist`));
700
+ }
560
701
 
561
- for (var k in value) {
562
- data[k] = value[k];
563
- }
702
+ for (let k in value) {
703
+ data[k] = value[k];
704
+ }
564
705
 
565
- _this.set(key, data, options, callback);
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
- acquireLock: function(key, options, callback) {
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 }, function(callback) {
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 } }, function(err) {
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, function(err, data) {
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
- var pool = JSON.parse(data);
767
+ const pool = JSON.parse(data);
605
768
 
606
769
  // Try to find a slot that's available.
607
- var index = pool.findIndex(s => s.status === 'available');
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), function(err) {
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
- consumeLock: function(key, index, callback) {
632
- callback = callback || function() {};
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 } }, function(err) {
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, function(err, data) {
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
- var pool = JSON.parse(data);
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), function(err) {
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
- expand: function(key, size, callback) {
678
- callback = callback || function() {};
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 } }, function(err) {
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, function(err, data) {
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
- var pool = JSON.parse(data);
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), function(err) {
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
- releaseLock: function(key, index, callback) {
721
- callback = callback || function() {};
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 } }, function(err) {
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, function(err, data) {
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
- var pool = JSON.parse(data);
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), function(err) {
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
- reset: function(key, callback) {
762
- callback = callback || function() {};
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 } }, function(err) {
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, function(err, data) {
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
- var pool = JSON.parse(data);
971
+ let pool = JSON.parse(data);
785
972
  pool = Array(pool.length).fill({ status: 'available' });
786
973
 
787
- redisClient.set(key, JSON.stringify(pool), function(err) {
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
- retrieveOrCreate: function(key, options, callback) {
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 || function() {};
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 } }, function(err) {
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, function(err, data) {
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
- var getSize = function(callback) {
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(function(err, size) {
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
- var pool = Array(Math.max(size, 1)).fill({ status: 'available' });
1034
+ const pool = Array(Math.max(size, 1)).fill({ status: 'available' });
842
1035
 
843
- redisClient.set(key, JSON.stringify(pool), function(err) {
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
- this.set = function(key, value, options, callback) {
857
- options = options || {};
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
- // Default callback is a noop
868
- callback = callback || function() {};
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
- // Store value in memory cache with a short expiration
871
- memoryCache.put(key, value, random(2000, 5000));
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
- // Store value is Redis
874
- redisClient.psetex(key, random(ttl.min, ttl.max), PettyCache.stringify(value), callback);
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 (var method in this.semaphore) {
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
- PettyCache.parse = function(text) {
892
- return JSON.parse(text, function(k, v) {
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
- PettyCache.stringify = function(value) {
906
- return JSON.stringify(value, function(k, v) {
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) {