@sebspark/promise-cache 3.3.3 → 3.4.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/dist/index.mjs CHANGED
@@ -1,9 +1,14 @@
1
+ var __defProp = Object.defineProperty;
1
2
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
3
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
4
  }) : x)(function(x) {
4
5
  if (typeof require !== "undefined") return require.apply(this, arguments);
5
6
  throw Error('Dynamic require of "' + x + '" is not supported');
6
7
  });
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
7
12
 
8
13
  // src/promiseCache.ts
9
14
  import { randomUUID } from "node:crypto";
@@ -20,7 +25,7 @@ var LocalStorage = class {
20
25
  }
21
26
  set(key, value, options) {
22
27
  this.client.set(key, value);
23
- if (options == null ? void 0 : options.PX) {
28
+ if (options?.PX) {
24
29
  setTimeout(() => {
25
30
  this.client.delete(key);
26
31
  }, options.PX);
@@ -57,8 +62,8 @@ var fixESM = __require("fix-esm");
57
62
  var superjson = fixESM.require("superjson");
58
63
  var CACHE_CLIENT = createClient;
59
64
  var isTestRunning = process.env.NODE_ENV === "test";
60
- function toMillis(seconds) {
61
- return seconds * 1e3;
65
+ function toMillis(seconds2) {
66
+ return seconds2 * 1e3;
62
67
  }
63
68
  var Persistor = class {
64
69
  client = null;
@@ -90,17 +95,15 @@ var Persistor = class {
90
95
  }
91
96
  }
92
97
  async startConnection() {
93
- var _a;
94
98
  try {
95
99
  await new Promise((resolve, reject) => {
96
- var _a2, _b, _c, _d, _e;
97
100
  this.client = CACHE_CLIENT({
98
- url: (_a2 = this.redis) == null ? void 0 : _a2.url,
99
- username: (_b = this.redis) == null ? void 0 : _b.username,
100
- password: (_c = this.redis) == null ? void 0 : _c.password,
101
- pingInterval: ((_d = this.redis) == null ? void 0 : _d.pingInterval) || void 0,
101
+ url: this.redis?.url,
102
+ username: this.redis?.username,
103
+ password: this.redis?.password,
104
+ pingInterval: this.redis?.pingInterval || void 0,
102
105
  socket: {
103
- ...(_e = this.redis) == null ? void 0 : _e.socket,
106
+ ...this.redis?.socket,
104
107
  reconnectStrategy: (retries, cause) => {
105
108
  console.error(cause);
106
109
  return 1e3 * 2 ** retries;
@@ -113,17 +116,15 @@ var Persistor = class {
113
116
  this.onSuccess();
114
117
  resolve(true);
115
118
  }).on("reconnecting", () => {
116
- var _a3;
117
- (_a3 = this.logger) == null ? void 0 : _a3.info("reconnecting...", this.clientId);
119
+ this.logger?.info("reconnecting...", this.clientId);
118
120
  }).on("end", () => {
119
- var _a3;
120
- (_a3 = this.logger) == null ? void 0 : _a3.info("end...", this.clientId);
121
+ this.logger?.info("end...", this.clientId);
121
122
  });
122
123
  this.client.connect();
123
124
  });
124
125
  } catch (ex) {
125
126
  this.onError(`${ex}`);
126
- (_a = this.logger) == null ? void 0 : _a.error(ex);
127
+ this.logger?.error(ex);
127
128
  }
128
129
  }
129
130
  async size() {
@@ -136,8 +137,7 @@ var Persistor = class {
136
137
  return this.clientId;
137
138
  }
138
139
  getIsClientConnected() {
139
- var _a;
140
- return !!((_a = this.client) == null ? void 0 : _a.isReady);
140
+ return !!this.client?.isReady;
141
141
  }
142
142
  createOptions(ttl) {
143
143
  if (ttl !== null && ttl !== void 0) {
@@ -153,9 +153,8 @@ var Persistor = class {
153
153
  * @param object.timestamp Timestamp
154
154
  */
155
155
  async set(key, { value, timestamp = Date.now(), ttl }) {
156
- var _a, _b;
157
156
  if (!this.client || !this.client.isReady) {
158
- (_a = this.logger) == null ? void 0 : _a.error("Client not ready");
157
+ this.logger?.error("Client not ready");
159
158
  return;
160
159
  }
161
160
  try {
@@ -167,7 +166,7 @@ var Persistor = class {
167
166
  const options = this.createOptions(ttl);
168
167
  await this.client.set(key, serializedData, options);
169
168
  } catch (error) {
170
- (_b = this.logger) == null ? void 0 : _b.error(`Error setting data in redis: ${error}`);
169
+ this.logger?.error(`Error setting data in redis: ${error}`);
171
170
  throw new Error(`Error setting data in redis: ${error}`);
172
171
  }
173
172
  }
@@ -177,9 +176,8 @@ var Persistor = class {
177
176
  * @returns GetType<T> value
178
177
  */
179
178
  async get(key) {
180
- var _a, _b;
181
179
  if (!this.client) {
182
- (_a = this.logger) == null ? void 0 : _a.error("Client not ready");
180
+ this.logger?.error("Client not ready");
183
181
  return null;
184
182
  }
185
183
  try {
@@ -189,7 +187,7 @@ var Persistor = class {
189
187
  }
190
188
  return superjson.parse(data);
191
189
  } catch (error) {
192
- (_b = this.logger) == null ? void 0 : _b.error(`Error getting data in redis: ${error}`);
190
+ this.logger?.error(`Error getting data in redis: ${error}`);
193
191
  throw new Error(`Error getting data from redis: ${error}`);
194
192
  }
195
193
  }
@@ -198,15 +196,14 @@ var Persistor = class {
198
196
  * @param key Cache key
199
197
  */
200
198
  async delete(key) {
201
- var _a, _b;
202
199
  if (!this.client || !this.client.isReady) {
203
- (_a = this.logger) == null ? void 0 : _a.error("Client not ready");
200
+ this.logger?.error("Client not ready");
204
201
  return;
205
202
  }
206
203
  try {
207
204
  await this.client.del(key);
208
205
  } catch (error) {
209
- (_b = this.logger) == null ? void 0 : _b.error(`Error deleting data from redis: ${error}`);
206
+ this.logger?.error(`Error deleting data from redis: ${error}`);
210
207
  throw new Error(`Error deleting data from redis: ${error}`);
211
208
  }
212
209
  }
@@ -221,20 +218,20 @@ var getPersistor = ({
221
218
  onSuccess,
222
219
  clientId
223
220
  }) => {
224
- const connectionName = redis ? (redis == null ? void 0 : redis.name) || "default" : "local";
221
+ const connectionName = redis ? redis?.name || "default" : "local";
225
222
  if (!persistors[connectionName]) {
226
223
  persistors[connectionName] = new Persistor({
227
224
  redis,
228
225
  onError: (error) => {
229
- onError == null ? void 0 : onError(error);
230
- logger == null ? void 0 : logger.error(
231
- `\u274C REDIS | Client Error | ${connectionName} | ${redis == null ? void 0 : redis.url}: ${error}`
226
+ onError?.(error);
227
+ logger?.error(
228
+ `\u274C REDIS | Client Error | ${connectionName} | ${redis?.url}: ${error}`
232
229
  );
233
230
  },
234
231
  onSuccess: () => {
235
- onSuccess == null ? void 0 : onSuccess();
236
- logger == null ? void 0 : logger.info(
237
- `\u{1F4E6} REDIS | Connection Ready | ${connectionName} | ${redis == null ? void 0 : redis.url}`
232
+ onSuccess?.();
233
+ logger?.info(
234
+ `\u{1F4E6} REDIS | Connection Ready | ${connectionName} | ${redis?.url}`
238
235
  );
239
236
  },
240
237
  clientId,
@@ -302,7 +299,7 @@ var PromiseCache = class {
302
299
  */
303
300
  async find(key) {
304
301
  const result = await this.persistor.get(key);
305
- return (result == null ? void 0 : result.value) ?? null;
302
+ return result?.value ?? null;
306
303
  }
307
304
  /**
308
305
  * A simple promise cache wrapper.
@@ -339,7 +336,259 @@ var PromiseCache = class {
339
336
  return response;
340
337
  }
341
338
  };
339
+
340
+ // src/serializer.ts
341
+ var fixESM2 = __require("fix-esm");
342
+ var superjson2 = fixESM2.require("superjson");
343
+ var serialize = (data) => {
344
+ return superjson2.stringify(data);
345
+ };
346
+ var deserialize = (serialized) => {
347
+ if (serialized === void 0 || serialized === null) return serialized;
348
+ return superjson2.parse(serialized);
349
+ };
350
+
351
+ // src/setOptions.ts
352
+ var toSetOptions = (expiry) => {
353
+ const options = {};
354
+ if (expiry !== void 0) {
355
+ if (typeof expiry === "number") {
356
+ options.PX = expiry;
357
+ } else if (expiry instanceof Date && !Number.isNaN(expiry.getTime())) {
358
+ const timestamp = expiry.getTime();
359
+ if (timestamp > Date.now()) {
360
+ options.PXAT = timestamp;
361
+ } else {
362
+ options.PX = 1;
363
+ }
364
+ }
365
+ }
366
+ if (!options.PX && !options.PXAT) {
367
+ options.EX = 1;
368
+ }
369
+ return options;
370
+ };
371
+
372
+ // src/cache.ts
373
+ var createCache = (persistor, prefix) => {
374
+ const pendingPromises = /* @__PURE__ */ new Map();
375
+ const cache = {
376
+ persistor,
377
+ wrap: (delegate, options) => {
378
+ return async (...args) => {
379
+ let key = typeof options.key === "string" ? options.key : options.key(...args);
380
+ if (prefix) {
381
+ key = `${prefix}:${key}`;
382
+ }
383
+ if (pendingPromises.has(key)) {
384
+ return pendingPromises.get(key);
385
+ }
386
+ const resultPromise = (async () => {
387
+ try {
388
+ const cached = await persistor.get(key);
389
+ if (cached !== null) {
390
+ return deserialize(cached);
391
+ }
392
+ const result = await delegate(...args);
393
+ const expiry = typeof options.expiry === "function" ? options.expiry(args, result) : options.expiry;
394
+ const serialized = serialize(result);
395
+ const setOptions = toSetOptions(expiry);
396
+ await persistor.set(key, serialized, setOptions);
397
+ return result;
398
+ } finally {
399
+ pendingPromises.delete(key);
400
+ }
401
+ })();
402
+ pendingPromises.set(key, resultPromise);
403
+ return resultPromise;
404
+ };
405
+ }
406
+ };
407
+ return cache;
408
+ };
409
+
410
+ // src/inMemoryPersistor.ts
411
+ var InMemoryPersistor = class {
412
+ /**
413
+ * Internal key-value store for caching string values.
414
+ * @private
415
+ */
416
+ store;
417
+ /**
418
+ * Tracks active timeouts for expiring keys.
419
+ * Each key maps to a `setTimeout` reference that deletes the key when triggered.
420
+ * @private
421
+ */
422
+ expirations;
423
+ /**
424
+ * Stores absolute expiration timestamps (in milliseconds since epoch) for each key.
425
+ * Used to compute remaining TTL.
426
+ * @private
427
+ */
428
+ expiryTimestamps;
429
+ /**
430
+ * Creates a new instance of `InMemoryPersistor`.
431
+ * Initializes an empty store, expiration map, and TTL tracker.
432
+ */
433
+ constructor() {
434
+ this.store = /* @__PURE__ */ new Map();
435
+ this.expirations = /* @__PURE__ */ new Map();
436
+ this.expiryTimestamps = /* @__PURE__ */ new Map();
437
+ }
438
+ /**
439
+ * Stores a key-value pair with optional expiration settings.
440
+ * If an expiration is provided (`EX`, `PX`, `EXAT`, `PXAT`), the key is automatically removed when TTL expires.
441
+ *
442
+ * @param {string} key - The key to store.
443
+ * @param {string} value - The string value to associate with the key.
444
+ * @param {SetOptions} [options] - Optional Redis-style expiration settings.
445
+ * @returns {Promise<'OK' | null>} Resolves to `'OK'` on success, or `null` if a conditional set (`NX`) fails.
446
+ */
447
+ async set(key, value, options) {
448
+ this.store.set(key, value);
449
+ if (options?.EX !== void 0) {
450
+ this.setExpiration(key, options.EX * 1e3);
451
+ } else if (options?.PX !== void 0) {
452
+ this.setExpiration(key, options.PX);
453
+ } else if (options?.EXAT !== void 0) {
454
+ const timeToExpire = options.EXAT * 1e3 - Date.now();
455
+ this.setExpiration(key, Math.max(0, timeToExpire));
456
+ } else if (options?.PXAT !== void 0) {
457
+ const timeToExpire = options.PXAT - Date.now();
458
+ this.setExpiration(key, Math.max(0, timeToExpire));
459
+ }
460
+ return "OK";
461
+ }
462
+ /**
463
+ * Retrieves the value associated with a key.
464
+ *
465
+ * @param {string} key - The key to retrieve.
466
+ * @returns {Promise<string | null>} Resolves to the string value, or `null` if the key does not exist.
467
+ */
468
+ async get(key) {
469
+ return this.store.get(key) ?? null;
470
+ }
471
+ /**
472
+ * Deletes a key from the store.
473
+ * If the key exists, it is removed along with any associated expiration.
474
+ *
475
+ * @param {string} key - The key to delete.
476
+ * @returns {Promise<number>} Resolves to `1` if the key was deleted, or `0` if the key did not exist.
477
+ */
478
+ async del(key) {
479
+ const existed = this.store.has(key);
480
+ if (existed) {
481
+ this.store.delete(key);
482
+ this.clearExpiration(key);
483
+ }
484
+ return existed ? 1 : 0;
485
+ }
486
+ /**
487
+ * Sets a time-to-live (TTL) in seconds for a key.
488
+ * If the key exists, it will be deleted after the specified duration.
489
+ *
490
+ * @param {string} key - The key to set an expiration on.
491
+ * @param {number} seconds - The TTL in seconds.
492
+ * @returns {Promise<number>} Resolves to `1` if the TTL was set, or `0` if the key does not exist.
493
+ */
494
+ async expire(key, seconds2) {
495
+ if (!this.store.has(key)) return false;
496
+ this.setExpiration(key, seconds2 * 1e3);
497
+ return true;
498
+ }
499
+ /**
500
+ * Retrieves the remaining time-to-live (TTL) of a key in seconds.
501
+ *
502
+ * @param {string} key - The key to check.
503
+ * @returns {Promise<number>} Resolves to:
504
+ * - Remaining TTL in **seconds** if the key exists and has an expiration.
505
+ * - `-1` if the key exists but has no expiration.
506
+ * - `-2` if the key does not exist.
507
+ */
508
+ async ttl(key) {
509
+ if (!this.store.has(key)) return -2;
510
+ if (!this.expiryTimestamps.has(key)) return -1;
511
+ const timeLeft = this.expiryTimestamps.get(key) - Date.now();
512
+ return timeLeft > 0 ? Math.ceil(timeLeft / 1e3) : -2;
513
+ }
514
+ /**
515
+ * Removes all keys from the store and clears all active expirations.
516
+ *
517
+ * @returns {Promise<'OK'>} Resolves to `'OK'` after all data is cleared.
518
+ */
519
+ async flushAll() {
520
+ this.store.clear();
521
+ for (const timeout of this.expirations.values()) {
522
+ clearTimeout(timeout);
523
+ }
524
+ this.expirations.clear();
525
+ this.expiryTimestamps.clear();
526
+ return "OK";
527
+ }
528
+ /**
529
+ * Sets an expiration timeout for a key.
530
+ * Cancels any existing expiration before setting a new one.
531
+ *
532
+ * @private
533
+ * @param {string} key - The key to expire.
534
+ * @param {number} ttlMs - Time-to-live in milliseconds.
535
+ */
536
+ setExpiration(key, ttlMs) {
537
+ this.clearExpiration(key);
538
+ const expiryTimestamp = Date.now() + ttlMs;
539
+ this.expiryTimestamps.set(key, expiryTimestamp);
540
+ const timeout = setTimeout(() => {
541
+ this.store.delete(key);
542
+ this.expirations.delete(key);
543
+ this.expiryTimestamps.delete(key);
544
+ }, ttlMs);
545
+ this.expirations.set(key, timeout);
546
+ }
547
+ /**
548
+ * Cancels an active expiration timeout for a key and removes its TTL record.
549
+ *
550
+ * @private
551
+ * @param {string} key - The key whose expiration should be cleared.
552
+ */
553
+ clearExpiration(key) {
554
+ if (this.expirations.has(key)) {
555
+ clearTimeout(this.expirations.get(key));
556
+ this.expirations.delete(key);
557
+ this.expiryTimestamps.delete(key);
558
+ }
559
+ }
560
+ };
561
+
562
+ // src/time.ts
563
+ var time_exports = {};
564
+ __export(time_exports, {
565
+ DAY: () => DAY,
566
+ HOUR: () => HOUR,
567
+ MINUTE: () => MINUTE,
568
+ SECOND: () => SECOND,
569
+ WEEK: () => WEEK,
570
+ add: () => add,
571
+ days: () => days,
572
+ hours: () => hours,
573
+ minutes: () => minutes,
574
+ seconds: () => seconds,
575
+ weeks: () => weeks
576
+ });
577
+ import { add } from "date-fns";
578
+ var SECOND = 1e3;
579
+ var MINUTE = 60 * SECOND;
580
+ var HOUR = 60 * MINUTE;
581
+ var DAY = 24 * HOUR;
582
+ var WEEK = 7 * DAY;
583
+ var seconds = (num) => num * SECOND;
584
+ var minutes = (num) => num * MINUTE;
585
+ var hours = (num) => num * HOUR;
586
+ var days = (num) => num * DAY;
587
+ var weeks = (num) => num * WEEK;
342
588
  export {
589
+ InMemoryPersistor,
343
590
  Persistor,
344
- PromiseCache
591
+ PromiseCache,
592
+ createCache,
593
+ time_exports as time
345
594
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sebspark/promise-cache",
3
- "version": "3.3.3",
3
+ "version": "3.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -13,12 +13,16 @@
13
13
  "dev": "tsc --watch --noEmit",
14
14
  "lint": "biome check .",
15
15
  "test": "vitest run --passWithNoTests --coverage",
16
+ "test:e2e": "vitest --config vitest.config.e2e.ts --run",
16
17
  "typecheck": "vitest --typecheck.only --passWithNoTests"
17
18
  },
18
19
  "devDependencies": {
20
+ "@testcontainers/redis": "^10.17.2",
21
+ "testcontainers": "^10.17.2",
19
22
  "tsconfig": "*"
20
23
  },
21
24
  "dependencies": {
25
+ "date-fn": "^0.0.2",
22
26
  "fix-esm": "1.0.1",
23
27
  "redis": "4.7.0",
24
28
  "superjson": "2.2.2"