@catbee/utils 2.0.4 → 2.1.1

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/README.md CHANGED
@@ -111,6 +111,7 @@ server.enableGracefulShutdown();
111
111
  | Module | Description |
112
112
  | --------------------------------------------------------------------------- | ------------------------------------------------------- |
113
113
  | [Express Server](https://catbee.in/docs/@catbee/utils/server) | Fast, secure, and scalable server setup |
114
+ | [Healthz Server](https://catbee.in/docs/@catbee/utils/healthz-server) | Healthz Server |
114
115
  | [Array Utilities](https://catbee.in/docs/@catbee/utils/array) | Advanced array manipulation |
115
116
  | [Async Utilities](https://catbee.in/docs/@catbee/utils/async) | Promise helpers, concurrency, timing |
116
117
  | [Cache Utilities](https://catbee.in/docs/@catbee/utils/cache) | In-memory caching with TTL |
package/cache/index.cjs CHANGED
@@ -33,6 +33,7 @@ var TTLCache = class {
33
33
  __name(this, "TTLCache");
34
34
  }
35
35
  cache = /* @__PURE__ */ new Map();
36
+ pending = /* @__PURE__ */ new Map();
36
37
  ttlMs;
37
38
  maxSize;
38
39
  cleanupInterval;
@@ -90,7 +91,7 @@ var TTLCache = class {
90
91
  const entry = this.cache.get(key);
91
92
  if (!entry) return void 0;
92
93
  const now = Date.now();
93
- if (now > entry.expiresAt) {
94
+ if (now >= entry.expiresAt) {
94
95
  this.cache.delete(key);
95
96
  return void 0;
96
97
  }
@@ -102,17 +103,31 @@ var TTLCache = class {
102
103
  /**
103
104
  * Retrieves or computes a value if it's not in the cache or has expired.
104
105
  *
106
+ * Concurrent calls for the same key share the same producer promise,
107
+ * preventing duplicate computations while the value is being generated.
108
+ *
105
109
  * @param key - The key to retrieve
106
110
  * @param producer - Function to generate the value if not cached
107
111
  * @param ttlMs - Optional custom TTL for the computed value
108
112
  * @returns The cached or computed value
109
113
  */
110
114
  async getOrCompute(key, producer, ttlMs) {
111
- const value = this.get(key);
112
- if (value !== void 0) return value;
113
- const newValue = await producer();
114
- this.setWithTTL(key, newValue, ttlMs ?? this.ttlMs);
115
- return newValue;
115
+ const cached = this.get(key);
116
+ if (cached !== void 0) {
117
+ return cached;
118
+ }
119
+ const pending = this.pending.get(key);
120
+ if (pending) {
121
+ return pending;
122
+ }
123
+ const promise = Promise.resolve().then(producer).then((value) => {
124
+ this.setWithTTL(key, value, ttlMs ?? this.ttlMs);
125
+ return value;
126
+ }).finally(() => {
127
+ this.pending.delete(key);
128
+ });
129
+ this.pending.set(key, promise);
130
+ return promise;
116
131
  }
117
132
  /**
118
133
  * Checks if the key exists and hasn't expired.
@@ -174,7 +189,7 @@ var TTLCache = class {
174
189
  let removed = 0;
175
190
  const now = Date.now();
176
191
  for (const [key, entry] of this.cache.entries()) {
177
- if (now > entry.expiresAt) {
192
+ if (now >= entry.expiresAt) {
178
193
  this.cache.delete(key);
179
194
  removed++;
180
195
  }
@@ -188,7 +203,7 @@ var TTLCache = class {
188
203
  */
189
204
  *entries() {
190
205
  for (const [key, entry] of this.cache.entries()) {
191
- if (Date.now() <= entry.expiresAt) {
206
+ if (Date.now() < entry.expiresAt) {
192
207
  yield [
193
208
  key,
194
209
  entry.value
@@ -203,7 +218,7 @@ var TTLCache = class {
203
218
  */
204
219
  *keys() {
205
220
  for (const [key, entry] of this.cache.entries()) {
206
- if (Date.now() <= entry.expiresAt) {
221
+ if (Date.now() < entry.expiresAt) {
207
222
  yield key;
208
223
  }
209
224
  }
@@ -215,7 +230,7 @@ var TTLCache = class {
215
230
  */
216
231
  *values() {
217
232
  for (const entry of this.cache.values()) {
218
- if (Date.now() <= entry.expiresAt) {
233
+ if (Date.now() < entry.expiresAt) {
219
234
  yield entry.value;
220
235
  }
221
236
  }
@@ -231,12 +246,14 @@ var TTLCache = class {
231
246
  const entry = this.cache.get(key);
232
247
  if (!entry) return false;
233
248
  const now = Date.now();
234
- if (now > entry.expiresAt) {
249
+ if (now >= entry.expiresAt) {
235
250
  this.cache.delete(key);
236
251
  return false;
237
252
  }
238
253
  entry.expiresAt = now + (ttlMs ?? this.ttlMs);
239
254
  entry.lastAccessed = now;
255
+ this.cache.delete(key);
256
+ this.cache.set(key, entry);
240
257
  return true;
241
258
  }
242
259
  /**
@@ -249,7 +266,7 @@ var TTLCache = class {
249
266
  let expired = 0;
250
267
  let valid = 0;
251
268
  for (const entry of this.cache.values()) {
252
- if (now > entry.expiresAt) {
269
+ if (now >= entry.expiresAt) {
253
270
  expired++;
254
271
  } else {
255
272
  valid++;
@@ -277,14 +294,17 @@ var TTLCache = class {
277
294
  */
278
295
  evictLRU() {
279
296
  const now = Date.now();
280
- for (const [key, entry] of Array.from(this.cache.entries())) {
281
- if (now > entry.expiresAt) {
297
+ for (const [key, entry] of this.cache.entries()) {
298
+ if (now >= entry.expiresAt) {
282
299
  this.cache.delete(key);
283
300
  }
284
301
  }
285
302
  while (this.maxSize && this.cache.size > this.maxSize) {
286
- const oldestKey = this.cache.keys().next().value;
287
- this.cache.delete(oldestKey);
303
+ const next = this.cache.keys().next();
304
+ if (next.done) {
305
+ break;
306
+ }
307
+ this.cache.delete(next.value);
288
308
  }
289
309
  }
290
310
  };
package/cache/index.d.ts CHANGED
@@ -41,11 +41,12 @@ interface TTLCacheOptions {
41
41
  *
42
42
  * @example
43
43
  * const cache = new TTLCache<string, number>({ ttlMs: 1000 });
44
- * cache.set("x", 123);
45
- * const value = cache.get("x"); // 123
44
+ * cache.set('x', 123);
45
+ * const value = cache.get('x'); // 123
46
46
  */
47
47
  declare class TTLCache<K, V> {
48
48
  private readonly cache;
49
+ private readonly pending;
49
50
  private readonly ttlMs;
50
51
  private readonly maxSize?;
51
52
  private cleanupInterval?;
@@ -78,6 +79,9 @@ declare class TTLCache<K, V> {
78
79
  /**
79
80
  * Retrieves or computes a value if it's not in the cache or has expired.
80
81
  *
82
+ * Concurrent calls for the same key share the same producer promise,
83
+ * preventing duplicate computations while the value is being generated.
84
+ *
81
85
  * @param key - The key to retrieve
82
86
  * @param producer - Function to generate the value if not cached
83
87
  * @param ttlMs - Optional custom TTL for the computed value
package/cache/index.mjs CHANGED
@@ -31,6 +31,7 @@ var TTLCache = class {
31
31
  __name(this, "TTLCache");
32
32
  }
33
33
  cache = /* @__PURE__ */ new Map();
34
+ pending = /* @__PURE__ */ new Map();
34
35
  ttlMs;
35
36
  maxSize;
36
37
  cleanupInterval;
@@ -88,7 +89,7 @@ var TTLCache = class {
88
89
  const entry = this.cache.get(key);
89
90
  if (!entry) return void 0;
90
91
  const now = Date.now();
91
- if (now > entry.expiresAt) {
92
+ if (now >= entry.expiresAt) {
92
93
  this.cache.delete(key);
93
94
  return void 0;
94
95
  }
@@ -100,17 +101,31 @@ var TTLCache = class {
100
101
  /**
101
102
  * Retrieves or computes a value if it's not in the cache or has expired.
102
103
  *
104
+ * Concurrent calls for the same key share the same producer promise,
105
+ * preventing duplicate computations while the value is being generated.
106
+ *
103
107
  * @param key - The key to retrieve
104
108
  * @param producer - Function to generate the value if not cached
105
109
  * @param ttlMs - Optional custom TTL for the computed value
106
110
  * @returns The cached or computed value
107
111
  */
108
112
  async getOrCompute(key, producer, ttlMs) {
109
- const value = this.get(key);
110
- if (value !== void 0) return value;
111
- const newValue = await producer();
112
- this.setWithTTL(key, newValue, ttlMs ?? this.ttlMs);
113
- return newValue;
113
+ const cached = this.get(key);
114
+ if (cached !== void 0) {
115
+ return cached;
116
+ }
117
+ const pending = this.pending.get(key);
118
+ if (pending) {
119
+ return pending;
120
+ }
121
+ const promise = Promise.resolve().then(producer).then((value) => {
122
+ this.setWithTTL(key, value, ttlMs ?? this.ttlMs);
123
+ return value;
124
+ }).finally(() => {
125
+ this.pending.delete(key);
126
+ });
127
+ this.pending.set(key, promise);
128
+ return promise;
114
129
  }
115
130
  /**
116
131
  * Checks if the key exists and hasn't expired.
@@ -172,7 +187,7 @@ var TTLCache = class {
172
187
  let removed = 0;
173
188
  const now = Date.now();
174
189
  for (const [key, entry] of this.cache.entries()) {
175
- if (now > entry.expiresAt) {
190
+ if (now >= entry.expiresAt) {
176
191
  this.cache.delete(key);
177
192
  removed++;
178
193
  }
@@ -186,7 +201,7 @@ var TTLCache = class {
186
201
  */
187
202
  *entries() {
188
203
  for (const [key, entry] of this.cache.entries()) {
189
- if (Date.now() <= entry.expiresAt) {
204
+ if (Date.now() < entry.expiresAt) {
190
205
  yield [
191
206
  key,
192
207
  entry.value
@@ -201,7 +216,7 @@ var TTLCache = class {
201
216
  */
202
217
  *keys() {
203
218
  for (const [key, entry] of this.cache.entries()) {
204
- if (Date.now() <= entry.expiresAt) {
219
+ if (Date.now() < entry.expiresAt) {
205
220
  yield key;
206
221
  }
207
222
  }
@@ -213,7 +228,7 @@ var TTLCache = class {
213
228
  */
214
229
  *values() {
215
230
  for (const entry of this.cache.values()) {
216
- if (Date.now() <= entry.expiresAt) {
231
+ if (Date.now() < entry.expiresAt) {
217
232
  yield entry.value;
218
233
  }
219
234
  }
@@ -229,12 +244,14 @@ var TTLCache = class {
229
244
  const entry = this.cache.get(key);
230
245
  if (!entry) return false;
231
246
  const now = Date.now();
232
- if (now > entry.expiresAt) {
247
+ if (now >= entry.expiresAt) {
233
248
  this.cache.delete(key);
234
249
  return false;
235
250
  }
236
251
  entry.expiresAt = now + (ttlMs ?? this.ttlMs);
237
252
  entry.lastAccessed = now;
253
+ this.cache.delete(key);
254
+ this.cache.set(key, entry);
238
255
  return true;
239
256
  }
240
257
  /**
@@ -247,7 +264,7 @@ var TTLCache = class {
247
264
  let expired = 0;
248
265
  let valid = 0;
249
266
  for (const entry of this.cache.values()) {
250
- if (now > entry.expiresAt) {
267
+ if (now >= entry.expiresAt) {
251
268
  expired++;
252
269
  } else {
253
270
  valid++;
@@ -275,14 +292,17 @@ var TTLCache = class {
275
292
  */
276
293
  evictLRU() {
277
294
  const now = Date.now();
278
- for (const [key, entry] of Array.from(this.cache.entries())) {
279
- if (now > entry.expiresAt) {
295
+ for (const [key, entry] of this.cache.entries()) {
296
+ if (now >= entry.expiresAt) {
280
297
  this.cache.delete(key);
281
298
  }
282
299
  }
283
300
  while (this.maxSize && this.cache.size > this.maxSize) {
284
- const oldestKey = this.cache.keys().next().value;
285
- this.cache.delete(oldestKey);
301
+ const next = this.cache.keys().next();
302
+ if (next.done) {
303
+ break;
304
+ }
305
+ this.cache.delete(next.value);
286
306
  }
287
307
  }
288
308
  };
@@ -28,23 +28,94 @@ var async_hooks = require('async_hooks');
28
28
 
29
29
  var __defProp = Object.defineProperty;
30
30
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
31
+ var TypedContextKey = class {
32
+ static {
33
+ __name(this, "TypedContextKey");
34
+ }
35
+ symbol;
36
+ defaultValue;
37
+ /**
38
+ * Creates a new typed context key.
39
+ * @param symbol - The unique symbol for this key
40
+ * @param defaultValue - Optional default value if key is not found
41
+ */
42
+ constructor(symbol, defaultValue) {
43
+ this.symbol = symbol;
44
+ this.defaultValue = defaultValue;
45
+ }
46
+ /**
47
+ * Gets the current value for this key.
48
+ * @returns The value or defaultValue if not found
49
+ */
50
+ get() {
51
+ return ContextStore.get(this.symbol) ?? this.defaultValue;
52
+ }
53
+ /**
54
+ * Sets the value for this key.
55
+ * @param value The value to set
56
+ */
57
+ set(value) {
58
+ ContextStore.set(this.symbol, value);
59
+ }
60
+ /**
61
+ * Checks if this key exists in the context.
62
+ * @returns True if the key exists
63
+ */
64
+ exists() {
65
+ return ContextStore.has(this.symbol);
66
+ }
67
+ /**
68
+ * Deletes this key from the context.
69
+ * @returns True if the key was deleted
70
+ */
71
+ delete() {
72
+ return ContextStore.delete(this.symbol);
73
+ }
74
+ /**
75
+ * Gets the symbol for this key.
76
+ * @returns Symbol for this key
77
+ */
78
+ getSymbol() {
79
+ return this.symbol;
80
+ }
81
+ };
31
82
  var StoreKeys = {
32
83
  LOGGER: Symbol("LOGGER"),
33
84
  REQUEST_ID: Symbol("REQUEST_ID"),
34
- USER: Symbol("USER"),
35
- SESSION: Symbol("SESSION"),
36
- TRANSACTION_ID: Symbol("TRANSACTION_ID"),
85
+ CORRELATION_ID: Symbol("CORRELATION_ID"),
37
86
  USER_ID: Symbol("USER_ID"),
87
+ TRANSACTION_ID: Symbol("TRANSACTION_ID"),
38
88
  TENANT_ID: Symbol("TENANT_ID"),
39
89
  TRACE_ID: Symbol("TRACE_ID"),
40
- CORRELATION_ID: Symbol("CORRELATION_ID")
90
+ SPAN_ID: Symbol("SPAN_ID"),
91
+ MESSAGE_ID: Symbol("MESSAGE_ID"),
92
+ MESSAGE_TYPE: Symbol("MESSAGE_TYPE"),
93
+ QUEUE_NAME: Symbol("QUEUE_NAME")
94
+ };
95
+ var TypedStoreKeys = {
96
+ LOGGER: new TypedContextKey(StoreKeys.LOGGER),
97
+ REQUEST_ID: new TypedContextKey(StoreKeys.REQUEST_ID),
98
+ CORRELATION_ID: new TypedContextKey(StoreKeys.CORRELATION_ID),
99
+ USER_ID: new TypedContextKey(StoreKeys.USER_ID),
100
+ TRANSACTION_ID: new TypedContextKey(StoreKeys.TRANSACTION_ID),
101
+ TENANT_ID: new TypedContextKey(StoreKeys.TENANT_ID),
102
+ TRACE_ID: new TypedContextKey(StoreKeys.TRACE_ID),
103
+ SPAN_ID: new TypedContextKey(StoreKeys.SPAN_ID),
104
+ MESSAGE_ID: new TypedContextKey(StoreKeys.MESSAGE_ID),
105
+ MESSAGE_TYPE: new TypedContextKey(StoreKeys.MESSAGE_TYPE),
106
+ QUEUE_NAME: new TypedContextKey(StoreKeys.QUEUE_NAME)
41
107
  };
42
108
  function getRequestId() {
43
- return ContextStore.get(StoreKeys.REQUEST_ID);
109
+ return TypedStoreKeys.REQUEST_ID.get();
44
110
  }
45
111
  __name(getRequestId, "getRequestId");
112
+ function resolveKey(key) {
113
+ return key instanceof TypedContextKey ? key.getSymbol() : key;
114
+ }
115
+ __name(resolveKey, "resolveKey");
46
116
  function getFromContext(key) {
47
- return ContextStore.get(key);
117
+ const symbol = resolveKey(key);
118
+ return ContextStore.get(symbol);
48
119
  }
49
120
  __name(getFromContext, "getFromContext");
50
121
  var ContextStore = class _ContextStore {
@@ -72,8 +143,9 @@ var ContextStore = class _ContextStore {
72
143
  * @returns {T | undefined} The value found (typed) or undefined if not present.
73
144
  */
74
145
  static get(key) {
146
+ const symbol = resolveKey(key);
75
147
  const store = this.storage.getStore();
76
- return store?.[key];
148
+ return store?.[symbol];
77
149
  }
78
150
  /**
79
151
  * Sets a value in the current context store by symbol key.
@@ -84,11 +156,12 @@ var ContextStore = class _ContextStore {
84
156
  * @throws {Error} If called outside an active context (not within a .run call or in the wrong async boundaries).
85
157
  */
86
158
  static set(key, value) {
159
+ const symbol = resolveKey(key);
87
160
  const store = this.storage.getStore();
88
161
  if (!store) {
89
- throw new Error(`Failed to set ${String(key)}: AsyncLocalStorage store is not initialized.`);
162
+ throw new Error(`Failed to set ${String(symbol)}: AsyncLocalStorage store is not initialized.`);
90
163
  }
91
- store[key] = value;
164
+ store[symbol] = value;
92
165
  }
93
166
  /**
94
167
  * Retrieves the entire context store object for the current async context.
@@ -117,8 +190,9 @@ var ContextStore = class _ContextStore {
117
190
  * @returns {boolean} True if the key exists, false otherwise.
118
191
  */
119
192
  static has(key) {
193
+ const symbol = resolveKey(key);
120
194
  const store = this.storage.getStore();
121
- return store !== void 0 && key in store;
195
+ return store !== void 0 && symbol in store;
122
196
  }
123
197
  /**
124
198
  * Removes a value from the current context store by symbol key.
@@ -128,11 +202,12 @@ var ContextStore = class _ContextStore {
128
202
  * @throws {Error} If called outside an active context.
129
203
  */
130
204
  static delete(key) {
205
+ const symbol = resolveKey(key);
131
206
  const store = this.storage.getStore();
132
207
  if (!store) {
133
- throw new Error(`Failed to delete ${String(key)}: AsyncLocalStorage store is not initialized.`);
208
+ throw new Error(`Failed to delete ${String(symbol)}: AsyncLocalStorage store is not initialized.`);
134
209
  }
135
- return delete store[key];
210
+ return delete store[symbol];
136
211
  }
137
212
  /**
138
213
  * Updates multiple values in the current context store at once.
@@ -161,22 +236,16 @@ var ContextStore = class _ContextStore {
161
236
  * @throws {Error} If called outside an active context.
162
237
  */
163
238
  static withValue(key, value, callback) {
164
- const store = this.storage.getStore();
165
- if (!store) {
166
- throw new Error(`Failed to set temporary value: AsyncLocalStorage store is not initialized.`);
167
- }
168
- const hasOriginal = key in store;
169
- const originalValue = store[key];
170
- store[key] = value;
171
- try {
172
- return callback();
173
- } finally {
174
- if (hasOriginal) {
175
- store[key] = originalValue;
176
- } else {
177
- delete store[key];
178
- }
239
+ const symbol = resolveKey(key);
240
+ const currentStore = this.storage.getStore();
241
+ if (!currentStore) {
242
+ throw new Error("Failed to set temporary value: AsyncLocalStorage store is not initialized.");
179
243
  }
244
+ const newStore = {
245
+ ...currentStore,
246
+ [symbol]: value
247
+ };
248
+ return this.storage.run(newStore, callback);
180
249
  }
181
250
  /**
182
251
  * Creates a new context that inherits values from the current context.
@@ -209,58 +278,10 @@ var ContextStore = class _ContextStore {
209
278
  };
210
279
  }
211
280
  };
212
- var TypedContextKey = class {
213
- static {
214
- __name(this, "TypedContextKey");
215
- }
216
- symbol;
217
- defaultValue;
218
- /**
219
- * Creates a new typed context key.
220
- *
221
- * @param symbol - The unique symbol for this key
222
- * @param defaultValue - Optional default value if key is not found
223
- */
224
- constructor(symbol, defaultValue) {
225
- this.symbol = symbol;
226
- this.defaultValue = defaultValue;
227
- }
228
- /**
229
- * Gets the current value for this key.
230
- *
231
- * @returns The value or defaultValue if not found
232
- */
233
- get() {
234
- return ContextStore.get(this.symbol) ?? this.defaultValue;
235
- }
236
- /**
237
- * Sets the value for this key.
238
- *
239
- * @param value The value to set
240
- */
241
- set(value) {
242
- ContextStore.set(this.symbol, value);
243
- }
244
- /**
245
- * Checks if this key exists in the context.
246
- *
247
- * @returns True if the key exists
248
- */
249
- exists() {
250
- return ContextStore.has(this.symbol);
251
- }
252
- /**
253
- * Deletes this key from the context.
254
- *
255
- * @returns True if the key was deleted
256
- */
257
- delete() {
258
- return ContextStore.delete(this.symbol);
259
- }
260
- };
261
281
 
262
282
  exports.ContextStore = ContextStore;
263
283
  exports.StoreKeys = StoreKeys;
264
284
  exports.TypedContextKey = TypedContextKey;
285
+ exports.TypedStoreKeys = TypedStoreKeys;
265
286
  exports.getFromContext = getFromContext;
266
287
  exports.getRequestId = getRequestId;