@zudojs/cache 0.0.1 → 1.0.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.
Files changed (88) hide show
  1. package/README.md +90 -14
  2. package/dist/cache.d.ts +107 -18
  3. package/dist/cache.js +393 -33
  4. package/dist/constants.d.ts +46 -12
  5. package/dist/constants.js +47 -19
  6. package/dist/index.d.ts +10 -9
  7. package/dist/index.js +9 -8
  8. package/dist/invalidation.d.ts +23 -16
  9. package/dist/invalidation.js +43 -33
  10. package/dist/key-builder.d.ts +15 -0
  11. package/dist/key-builder.js +43 -3
  12. package/dist/lock.d.ts +36 -2
  13. package/dist/lock.js +160 -30
  14. package/dist/memory.d.ts +63 -7
  15. package/dist/memory.js +223 -39
  16. package/dist/metrics.d.ts +10 -0
  17. package/dist/metrics.js +49 -4
  18. package/dist/serializer.d.ts +19 -10
  19. package/dist/serializer.js +88 -21
  20. package/dist/store.d.ts +21 -11
  21. package/dist/store.js +125 -65
  22. package/dist/tags.d.ts +32 -12
  23. package/dist/tags.js +113 -33
  24. package/dist/types-adapter.d.ts +23 -9
  25. package/dist/types-config.d.ts +46 -9
  26. package/dist/types-health.d.ts +5 -2
  27. package/dist/types-keys.d.ts +8 -4
  28. package/dist/types-lock.d.ts +8 -0
  29. package/dist/types-operations.d.ts +13 -20
  30. package/dist/types-results.d.ts +7 -1
  31. package/dist/types-tags.d.ts +9 -0
  32. package/dist/types-utility.d.ts +6 -10
  33. package/dist/types-values.d.ts +9 -14
  34. package/dist/types.d.ts +6 -6
  35. package/dist/utils.d.ts +65 -0
  36. package/dist/utils.js +210 -0
  37. package/package.json +14 -7
  38. package/dist/.tsbuildinfo +0 -1
  39. package/dist/cache.d.ts.map +0 -1
  40. package/dist/cache.js.map +0 -1
  41. package/dist/constants.d.ts.map +0 -1
  42. package/dist/constants.js.map +0 -1
  43. package/dist/errors.d.ts.map +0 -1
  44. package/dist/errors.js.map +0 -1
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/index.js.map +0 -1
  47. package/dist/invalidation.d.ts.map +0 -1
  48. package/dist/invalidation.js.map +0 -1
  49. package/dist/key-builder.d.ts.map +0 -1
  50. package/dist/key-builder.js.map +0 -1
  51. package/dist/lock.d.ts.map +0 -1
  52. package/dist/lock.js.map +0 -1
  53. package/dist/memory.d.ts.map +0 -1
  54. package/dist/memory.js.map +0 -1
  55. package/dist/metrics.d.ts.map +0 -1
  56. package/dist/metrics.js.map +0 -1
  57. package/dist/serializer.d.ts.map +0 -1
  58. package/dist/serializer.js.map +0 -1
  59. package/dist/store.d.ts.map +0 -1
  60. package/dist/store.js.map +0 -1
  61. package/dist/tags.d.ts.map +0 -1
  62. package/dist/tags.js.map +0 -1
  63. package/dist/types-adapter.d.ts.map +0 -1
  64. package/dist/types-adapter.js.map +0 -1
  65. package/dist/types-config.d.ts.map +0 -1
  66. package/dist/types-config.js.map +0 -1
  67. package/dist/types-events.d.ts.map +0 -1
  68. package/dist/types-events.js.map +0 -1
  69. package/dist/types-health.d.ts.map +0 -1
  70. package/dist/types-health.js.map +0 -1
  71. package/dist/types-keys.d.ts.map +0 -1
  72. package/dist/types-keys.js.map +0 -1
  73. package/dist/types-lock.d.ts.map +0 -1
  74. package/dist/types-lock.js.map +0 -1
  75. package/dist/types-metrics.d.ts.map +0 -1
  76. package/dist/types-metrics.js.map +0 -1
  77. package/dist/types-operations.d.ts.map +0 -1
  78. package/dist/types-operations.js.map +0 -1
  79. package/dist/types-results.d.ts.map +0 -1
  80. package/dist/types-results.js.map +0 -1
  81. package/dist/types-tags.d.ts.map +0 -1
  82. package/dist/types-tags.js.map +0 -1
  83. package/dist/types-utility.d.ts.map +0 -1
  84. package/dist/types-utility.js.map +0 -1
  85. package/dist/types-values.d.ts.map +0 -1
  86. package/dist/types-values.js.map +0 -1
  87. package/dist/types.d.ts.map +0 -1
  88. package/dist/types.js.map +0 -1
package/dist/cache.js CHANGED
@@ -1,14 +1,28 @@
1
1
  /**
2
2
  * @zudojs/cache — Cache Service
3
- * High-level cache service combining adapter, serializer, key builder, tags, invalidation, locking, and metrics.
3
+ * High-level cache service combining adapter, key builder, tags,
4
+ * invalidation, locking, metrics, middleware, events, and an optional
5
+ * serializer.
6
+ *
7
+ * Serialization: when `config.serializer` is provided, values are
8
+ * serialized on set and deserialized on get, so cached values are
9
+ * structural copies. Without a serializer the memory adapter stores
10
+ * values by reference — mutations of a cached object are visible to
11
+ * later readers.
12
+ *
13
+ * Scoping: keys, tags and locks are all namespace-scoped. A namespace is
14
+ * validated as an identity part everywhere it is used (including in glob
15
+ * patterns), so it can never widen an operation beyond its own tenant.
4
16
  */
5
- import { DEFAULT_TTL_MS } from "./constants.js";
17
+ import { DEFAULT_LOCK_RETRY_DELAY_MS, DEFAULT_SEPARATOR, DEFAULT_TTL_MS, } from "./constants.js";
6
18
  import { DefaultKeyBuilder } from "./key-builder.js";
7
19
  import { createCacheStore, DefaultCacheStore } from "./store.js";
8
- import { createTagStore, InMemoryTagStore } from "./tags.js";
20
+ import { assertValidTag, createTagStore, InMemoryTagStore } from "./tags.js";
9
21
  import { CacheInvalidationManager, createInvalidationManager, } from "./invalidation.js";
10
22
  import { CacheLockManager, createLockManager } from "./lock.js";
11
23
  import { createCacheMetrics, InMemoryCacheMetrics } from "./metrics.js";
24
+ import { CacheError, cacheDeserializationError, cacheSerializationError, isCacheError, } from "./errors.js";
25
+ import { createGlobMatcher } from "./utils.js";
12
26
  export class CacheService {
13
27
  store;
14
28
  keyBuilder;
@@ -16,93 +30,376 @@ export class CacheService {
16
30
  invalidation;
17
31
  lockManager;
18
32
  metrics;
33
+ serializer;
19
34
  defaultTtl;
20
35
  enabled;
36
+ failSilently;
37
+ /** Namespace configured for this service; the default scope for keys, tags and locks. */
38
+ namespace;
39
+ separator;
40
+ /** In-flight getOrSet computations, keyed by full key (stampede protection). */
41
+ inFlight = new Map();
21
42
  constructor(options) {
22
43
  this.enabled = options.config?.enabled ?? true;
23
- this.defaultTtl = options.config?.defaultTtl ?? DEFAULT_TTL_MS;
44
+ this.failSilently = options.config?.failSilently ?? false;
45
+ this.defaultTtl =
46
+ options.config?.defaultTtl !== undefined
47
+ ? options.config.defaultTtl
48
+ : DEFAULT_TTL_MS;
49
+ this.serializer = options.config?.serializer ?? null;
50
+ this.namespace = options.config?.namespace;
51
+ this.separator = options.config?.separator ?? DEFAULT_SEPARATOR;
24
52
  this.metrics =
25
53
  options.config?.collectStats !== false ? createCacheMetrics() : null;
26
54
  this.store = createCacheStore({
27
55
  adapter: options.adapter,
28
- metrics: this.metrics,
56
+ ...(this.metrics ? { metrics: this.metrics } : {}),
57
+ ...(options.config?.middlewares
58
+ ? { middlewares: options.config.middlewares }
59
+ : {}),
29
60
  });
30
61
  this.keyBuilder =
31
62
  options.keyBuilder ??
32
63
  new DefaultKeyBuilder({
33
- prefix: options.config?.prefix,
34
- separator: options.config?.separator,
35
- namespace: options.config?.namespace,
64
+ ...(options.config?.prefix !== undefined
65
+ ? { prefix: options.config.prefix }
66
+ : {}),
67
+ ...(options.config?.separator !== undefined
68
+ ? { separator: options.config.separator }
69
+ : {}),
70
+ ...(options.config?.namespace !== undefined
71
+ ? { namespace: options.config.namespace }
72
+ : {}),
36
73
  });
37
74
  this.tagStore = createTagStore();
75
+ // Route invalidation through the instrumented store so metrics and
76
+ // events fire for invalidation-driven deletions too.
38
77
  this.invalidation = createInvalidationManager({
39
- adapter: options.adapter,
78
+ adapter: this.store,
40
79
  tagStore: this.tagStore,
80
+ keyBuilder: this.keyBuilder,
41
81
  });
42
- this.lockManager = createLockManager();
82
+ this.lockManager = createLockManager(options.config?.lockStore ? { store: options.config.lockStore } : {});
43
83
  }
44
84
  async get(key, options) {
45
85
  if (!this.enabled)
46
86
  return { hit: false, value: null };
47
- return this.store.get(this.keyBuilder.build(key, options));
87
+ const fullKey = this.keyBuilder.build(key, options);
88
+ try {
89
+ const result = await this.store.get(fullKey);
90
+ if (!result.hit || !this.serializer)
91
+ return result;
92
+ const value = this.deserialize(result.value);
93
+ return {
94
+ ...result,
95
+ value,
96
+ ...(result.entry
97
+ ? { entry: { ...result.entry, value } }
98
+ : {}),
99
+ };
100
+ }
101
+ catch (error) {
102
+ if (this.failSilently)
103
+ return { hit: false, value: null };
104
+ throw error;
105
+ }
48
106
  }
49
107
  async set(key, value, options) {
50
108
  if (!this.enabled)
51
109
  return { success: false, key, expiresAt: null };
52
110
  const fullKey = this.keyBuilder.build(key, options);
53
- const result = await this.store.set(fullKey, value, {
54
- ttl: options?.ttl ?? this.defaultTtl,
55
- ...options,
56
- });
57
- if (options?.tags && options.tags.length > 0)
58
- await this.tagStore.add(fullKey, options.tags);
59
- return result;
111
+ if (options?.tags)
112
+ for (const tag of options.tags)
113
+ assertValidTag(tag);
114
+ try {
115
+ const stored = this.serializer ? this.serialize(value) : value;
116
+ const result = await this.store.set(fullKey, stored, {
117
+ ttl: options?.ttl !== undefined ? options.ttl : this.defaultTtl,
118
+ ...(options?.tags !== undefined ? { tags: options.tags } : {}),
119
+ ...(options?.overwrite !== undefined
120
+ ? { overwrite: options.overwrite }
121
+ : {}),
122
+ ...(options?.metadata !== undefined
123
+ ? { metadata: options.metadata }
124
+ : {}),
125
+ });
126
+ if (result.success && options?.tags && options.tags.length > 0)
127
+ await this.tagStore.add(fullKey, options.tags, this.tagScope(options));
128
+ return result;
129
+ }
130
+ catch (error) {
131
+ if (this.failSilently)
132
+ return { success: false, key, expiresAt: null };
133
+ throw error;
134
+ }
60
135
  }
61
136
  async delete(key, options) {
62
137
  if (!this.enabled)
63
138
  return { deleted: false, key };
64
- return this.store.delete(this.keyBuilder.build(key, options));
139
+ const fullKey = this.keyBuilder.build(key, options);
140
+ try {
141
+ const result = await this.store.delete(fullKey);
142
+ this.tagStore.removeKey(fullKey);
143
+ return result;
144
+ }
145
+ catch (error) {
146
+ if (this.failSilently)
147
+ return { deleted: false, key };
148
+ throw error;
149
+ }
65
150
  }
66
151
  async has(key, options) {
67
152
  if (!this.enabled)
68
153
  return false;
69
- return this.store.has(this.keyBuilder.build(key, options));
154
+ const fullKey = this.keyBuilder.build(key, options);
155
+ try {
156
+ return await this.store.has(fullKey);
157
+ }
158
+ catch (error) {
159
+ if (this.failSilently)
160
+ return false;
161
+ throw error;
162
+ }
70
163
  }
164
+ /**
165
+ * Clears cache entries.
166
+ * - No options: clears everything (and flushes the tag store).
167
+ * - `namespace` and/or `pattern`: builds a fully-qualified pattern via
168
+ * the key builder (prefix + namespace + pattern) so only matching
169
+ * entries are removed. Both parts are validated: a `namespace` of `"*"`
170
+ * is rejected rather than escaping its own scope.
171
+ */
71
172
  async clear(options) {
72
173
  if (!this.enabled)
73
174
  return { cleared: 0 };
74
- return this.store.clear(options);
175
+ if (options?.pattern !== undefined || options?.namespace !== undefined) {
176
+ // Validation happens outside the failSilently guard: a malformed
177
+ // pattern is programmer error, not an adapter fault.
178
+ const pattern = this.qualifyPattern(options.pattern ?? "*", options.namespace);
179
+ try {
180
+ const result = await this.store.clear({ pattern });
181
+ this.purgeTagsMatching(pattern);
182
+ return result;
183
+ }
184
+ catch (error) {
185
+ if (this.failSilently)
186
+ return { cleared: 0 };
187
+ throw error;
188
+ }
189
+ }
190
+ try {
191
+ const result = await this.store.clear();
192
+ this.tagStore.clear();
193
+ return result;
194
+ }
195
+ catch (error) {
196
+ if (this.failSilently)
197
+ return { cleared: 0 };
198
+ throw error;
199
+ }
200
+ }
201
+ /** Remaining TTL for a key (undefined = missing, null = never expires). */
202
+ async ttl(key, options) {
203
+ if (!this.enabled)
204
+ return undefined;
205
+ const fullKey = this.keyBuilder.build(key, options);
206
+ try {
207
+ return await this.store.ttl?.(fullKey);
208
+ }
209
+ catch (error) {
210
+ if (this.failSilently)
211
+ return undefined;
212
+ throw error;
213
+ }
214
+ }
215
+ /** Updates the TTL of an existing key. Returns false when unsupported or missing. */
216
+ async expire(key, ttl, options) {
217
+ if (!this.enabled)
218
+ return false;
219
+ const fullKey = this.keyBuilder.build(key, options);
220
+ try {
221
+ return (await this.store.expire?.(fullKey, ttl)) ?? false;
222
+ }
223
+ catch (error) {
224
+ if (this.failSilently)
225
+ return false;
226
+ throw error;
227
+ }
75
228
  }
76
229
  async getOrSet(key, fn, options) {
77
230
  if (!this.enabled)
78
231
  return { value: await fn(), cached: false };
232
+ const fullKey = this.keyBuilder.build(key, options);
79
233
  if (!options?.forceRefresh) {
80
234
  const cached = await this.get(key, options);
81
235
  if (cached.hit)
82
236
  return { value: cached.value, cached: true };
237
+ // Stampede protection: concurrent misses share a single fn() call.
238
+ const pending = this.inFlight.get(fullKey);
239
+ if (pending)
240
+ return { value: (await pending), cached: false };
241
+ }
242
+ const promise = (async () => {
243
+ const value = await fn();
244
+ await this.set(key, value, options);
245
+ return value;
246
+ })();
247
+ this.inFlight.set(fullKey, promise);
248
+ try {
249
+ return { value: (await promise), cached: false };
250
+ }
251
+ finally {
252
+ if (this.inFlight.get(fullKey) === promise)
253
+ this.inFlight.delete(fullKey);
83
254
  }
84
- const value = await fn();
85
- await this.set(key, value, options);
86
- return { value, cached: false };
87
255
  }
88
- async invalidateByTag(tags) {
89
- return this.invalidation.invalidateByTag(tags);
256
+ /**
257
+ * Invalidates every entry tagged with any of `tags`, within this
258
+ * service's namespace (or `options.namespace`). Tags registered under a
259
+ * different namespace are untouched.
260
+ */
261
+ async invalidateByTag(tags, options) {
262
+ if (!this.enabled)
263
+ return { cleared: 0 };
264
+ for (const tag of tags)
265
+ assertValidTag(tag);
266
+ try {
267
+ return await this.invalidation.invalidateByTag(tags, this.tagScope(options));
268
+ }
269
+ catch (error) {
270
+ if (this.failSilently)
271
+ return { cleared: 0 };
272
+ throw error;
273
+ }
90
274
  }
91
- async invalidateByPattern(pattern) {
92
- return this.invalidation.invalidateByPattern(pattern);
275
+ /**
276
+ * Invalidates entries matching a service-level glob pattern. The
277
+ * pattern is qualified with the key builder's prefix (and namespace,
278
+ * if configured), so `invalidateByPattern("user.*")` matches keys this
279
+ * service wrote via `set("user.1", ...)`.
280
+ *
281
+ * `*` never crosses the key separator, so a pattern cannot reach into a
282
+ * namespace the caller did not name. Use `**` as the pattern to span
283
+ * whole namespaces deliberately.
284
+ */
285
+ async invalidateByPattern(pattern, options) {
286
+ if (!this.enabled)
287
+ return { cleared: 0 };
288
+ const qualified = this.qualifyPattern(pattern, options?.namespace);
289
+ try {
290
+ const result = await this.invalidation.invalidateByPattern(qualified);
291
+ this.purgeTagsMatching(qualified);
292
+ return result;
293
+ }
294
+ catch (error) {
295
+ if (this.failSilently)
296
+ return { cleared: 0 };
297
+ throw error;
298
+ }
93
299
  }
300
+ /**
301
+ * Runs `fn` under a namespace-scoped, fully-qualified lock.
302
+ *
303
+ * The lock name goes through the key builder, so it is prefixed and
304
+ * namespaced exactly like a cache key and validated the same way — two
305
+ * tenants using the same lock name do not collide.
306
+ *
307
+ * The lease is renewed while `fn` runs and a lost lease throws (see
308
+ * `CacheLockManager.withLock`). `failSilently` deliberately does not apply:
309
+ * running a critical section without exclusion is never a safe
310
+ * degradation, and neither is running it while the cache is disabled.
311
+ */
94
312
  async withLock(key, fn, options) {
95
- return this.lockManager.withLock(key, fn, {
96
- ttl: options?.ttl,
97
- retry: options?.retryAttempts
98
- ? { attempts: options.retryAttempts, delay: 100 }
99
- : undefined,
313
+ if (!this.enabled) {
314
+ const code = "CACHE_DISABLED";
315
+ throw new CacheError(`Cannot acquire lock "${key}": the cache is disabled.`, { code, statusCode: 503 });
316
+ }
317
+ const lockKey = this.keyBuilder.build(key, options?.namespace !== undefined
318
+ ? { namespace: options.namespace }
319
+ : undefined);
320
+ return this.lockManager.withLock(lockKey, fn, {
321
+ ...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
322
+ // `retryAttempts: 0` is honored (single attempt, no retries).
323
+ ...(options?.retryAttempts !== undefined
324
+ ? {
325
+ retry: {
326
+ attempts: options.retryAttempts,
327
+ delay: DEFAULT_LOCK_RETRY_DELAY_MS,
328
+ },
329
+ }
330
+ : {}),
100
331
  });
101
332
  }
333
+ /**
334
+ * Applies a sequence of operations, one at a time, returning one result
335
+ * per operation in submission order. A failing operation does not stop
336
+ * the batch; its error is reported on its own result.
337
+ */
338
+ async batch(operations, options) {
339
+ const results = [];
340
+ for (const operation of operations) {
341
+ try {
342
+ const result = await this.applyBatchOperation(operation, options);
343
+ results.push({ operation, success: true, result });
344
+ }
345
+ catch (error) {
346
+ results.push({ operation, success: false, error });
347
+ }
348
+ }
349
+ return results;
350
+ }
351
+ /* ---- Observability ---- */
102
352
  getStats() {
103
353
  return this.metrics?.getStats() ?? null;
104
354
  }
355
+ /** Number of live entries, when the underlying adapter can report it. */
356
+ async size() {
357
+ if (!this.enabled)
358
+ return undefined;
359
+ try {
360
+ return await this.store.size();
361
+ }
362
+ catch (error) {
363
+ if (this.failSilently)
364
+ return undefined;
365
+ throw error;
366
+ }
367
+ }
368
+ /** Latency percentiles for one operation. */
369
+ getLatencyStats(operation) {
370
+ return this.metrics?.getLatencyStats(operation) ?? null;
371
+ }
372
+ /** Latency histogram for one operation. */
373
+ getLatencyHistogram(operation) {
374
+ return this.metrics?.getLatencyHistogram(operation) ?? null;
375
+ }
376
+ /** The most-read keys currently tracked, hottest first. */
377
+ getHotKeys(topN) {
378
+ return this.metrics?.getHotKeys(topN) ?? null;
379
+ }
380
+ /** Resets all collected metrics. */
381
+ resetStats() {
382
+ this.metrics?.reset();
383
+ }
384
+ /**
385
+ * Subscribes to cache events (`cache.hit`, `cache.miss`, `cache.set`,
386
+ * `cache.delete`, `cache.clear`, `cache.error`), or to `"*"` for all.
387
+ */
388
+ subscribe(eventType, handler) {
389
+ return this.store.subscribe(eventType, handler);
390
+ }
105
391
  async healthCheck() {
392
+ if (!this.enabled) {
393
+ // A disabled cache is not unhealthy — but the caller must be able to
394
+ // tell the two apart, so say so explicitly instead of probing an
395
+ // adapter the service has been told not to use.
396
+ return {
397
+ healthy: true,
398
+ adapter: this.store.name,
399
+ checkedAt: new Date(),
400
+ disabled: true,
401
+ };
402
+ }
106
403
  const start = performance.now();
107
404
  try {
108
405
  await this.store.has("__health__");
@@ -127,6 +424,69 @@ export class CacheService {
127
424
  }
128
425
  async disconnect() {
129
426
  await this.store.disconnect?.();
427
+ // Drop service-local state so a reconnect does not resurrect stale
428
+ // in-flight computations or tag mappings.
429
+ this.inFlight.clear();
430
+ this.tagStore.clear();
431
+ }
432
+ /* ---- Internals ---- */
433
+ async applyBatchOperation(operation, options) {
434
+ const scope = options?.namespace !== undefined
435
+ ? { namespace: options.namespace }
436
+ : undefined;
437
+ switch (operation.type) {
438
+ case "get":
439
+ return this.get(operation.key, scope);
440
+ case "set":
441
+ return this.set(operation.key, operation.value, {
442
+ ...operation.options,
443
+ ...(scope ?? {}),
444
+ });
445
+ case "delete":
446
+ return this.delete(operation.key, scope);
447
+ default: {
448
+ const code = "CACHE_OPERATION_FAILED";
449
+ throw new CacheError(`Unsupported batch operation type: ${String(operation.type)}.`, { code, key: operation.key, statusCode: 400, expose: true });
450
+ }
451
+ }
452
+ }
453
+ /** The namespace scope applied to tag registrations and lookups. */
454
+ tagScope(options) {
455
+ const namespace = options?.namespace ?? this.namespace;
456
+ return namespace !== undefined ? { namespace } : {};
457
+ }
458
+ qualifyPattern(pattern, namespace) {
459
+ if (this.keyBuilder.buildPattern) {
460
+ return this.keyBuilder.buildPattern(pattern, namespace !== undefined ? { namespace } : undefined);
461
+ }
462
+ return pattern;
463
+ }
464
+ purgeTagsMatching(pattern) {
465
+ const matches = createGlobMatcher(pattern, { separator: this.separator });
466
+ for (const key of this.tagStore.trackedKeys()) {
467
+ if (matches(key))
468
+ this.tagStore.removeKey(key);
469
+ }
470
+ }
471
+ serialize(value) {
472
+ try {
473
+ return this.serializer.serialize(value);
474
+ }
475
+ catch (error) {
476
+ if (isCacheError(error))
477
+ throw error;
478
+ throw cacheSerializationError(undefined, { cause: error });
479
+ }
480
+ }
481
+ deserialize(value) {
482
+ try {
483
+ return this.serializer.deserialize(value);
484
+ }
485
+ catch (error) {
486
+ if (isCacheError(error))
487
+ throw error;
488
+ throw cacheDeserializationError(undefined, { cause: error });
489
+ }
130
490
  }
131
491
  }
132
492
  export function createCacheService(options) {
@@ -5,36 +5,70 @@
5
5
  */
6
6
  /** Default time-to-live in milliseconds (5 minutes). */
7
7
  export declare const DEFAULT_TTL_MS: number;
8
- /** Maximum supported TTL (24 hours). */
8
+ /** Maximum supported TTL (24 hours). TTLs above this are rejected. */
9
9
  export declare const MAX_TTL_MS: number;
10
- /** Minimum TTL (1 second). */
11
- export declare const MIN_TTL_MS = 1000;
10
+ /**
11
+ * Minimum TTL (1 millisecond). TTLs below this (zero or negative) are
12
+ * rejected; use `ttl: null` for entries that never expire.
13
+ */
14
+ export declare const MIN_TTL_MS = 1;
12
15
  /** Default namespace separator. */
13
16
  export declare const DEFAULT_SEPARATOR = ":";
14
17
  /** Default key prefix. */
15
18
  export declare const DEFAULT_PREFIX = "zudojs";
16
19
  /** Maximum key length in characters. */
17
20
  export declare const MAX_KEY_LENGTH = 256;
18
- /** Pattern used to validate cache keys. */
21
+ /**
22
+ * Pattern used to validate each individual cache key part (prefix,
23
+ * namespace, and raw key). Deliberately excludes the default separator
24
+ * (`:`) so callers cannot forge namespaced keys (e.g. `build("admin:x")`
25
+ * throws). Parts are additionally checked against the active separator.
26
+ */
19
27
  export declare const CACHE_KEY_PATTERN: RegExp;
28
+ /**
29
+ * Pattern used to validate the caller-supplied *glob* segment of a key
30
+ * pattern. It is the key alphabet plus the two glob metacharacters `*` and
31
+ * `?` — nothing else has meaning for the matcher, so anything else is
32
+ * caller error. The separator is excluded so a pattern cannot escape the
33
+ * prefix/namespace scope it is composed into.
34
+ */
35
+ export declare const CACHE_PATTERN_PART_PATTERN: RegExp;
36
+ /** Maximum length of a cache tag. Tags are untrusted map keys. */
37
+ export declare const MAX_TAG_LENGTH = 128;
20
38
  /** Default lock TTL in milliseconds (30 seconds). */
21
39
  export declare const DEFAULT_LOCK_TTL_MS = 30000;
22
40
  /** Default number of retry attempts for lock acquisition. */
23
41
  export declare const DEFAULT_LOCK_RETRY_ATTEMPTS = 3;
24
42
  /** Default delay between lock retry attempts in milliseconds. */
25
43
  export declare const DEFAULT_LOCK_RETRY_DELAY_MS = 100;
44
+ /**
45
+ * Divisor applied to a lock's TTL to derive its heartbeat interval, so a
46
+ * lease is renewed roughly three times per TTL window while the critical
47
+ * section runs.
48
+ */
49
+ export declare const LOCK_HEARTBEAT_DIVISOR = 3;
26
50
  /** Maximum number of latency samples to keep per operation. */
27
51
  export declare const MAX_LATENCY_SAMPLES = 1000;
28
- /** Bucket boundaries for latency histograms (ms). */
52
+ /** Maximum number of distinct keys tracked for hot-key metrics. */
53
+ export declare const MAX_TRACKED_KEYS = 1024;
54
+ /** Bucket boundaries for latency histograms (ms). A +Infinity bucket is appended at query time. */
29
55
  export declare const LATENCY_BUCKETS: readonly [1, 5, 10, 25, 50, 100, 250, 500, 1000];
30
56
  /** Default maximum number of entries for the in-memory adapter. */
31
57
  export declare const DEFAULT_MAX_ENTRIES = 10000;
32
- /** Default maximum memory budget in bytes (50 MB). */
58
+ /**
59
+ * Default maximum memory budget in bytes (50 MB) for the in-memory adapter.
60
+ * Entry sizes are estimated (see `MemoryCacheAdapter`), so the budget is
61
+ * approximate; it exists to bound worst-case retention, not to be exact.
62
+ */
33
63
  export declare const DEFAULT_MAX_MEMORY_BYTES: number;
34
- /** Maximum number of keys in a single batch operation. */
35
- export declare const MAX_BATCH_SIZE = 100;
36
- /** Glob pattern for matching all keys. */
37
- export declare const MATCH_ALL_PATTERN = "*";
38
- /** Regex pattern that matches valid namespace characters. */
39
- export declare const NAMESPACE_PATTERN: RegExp;
64
+ /**
65
+ * How often (at most) the in-memory adapter opportunistically purges expired
66
+ * entries on the overwrite path, where no eviction is otherwise triggered.
67
+ */
68
+ export declare const EXPIRED_PURGE_INTERVAL_MS = 30000;
69
+ /**
70
+ * Maximum number of value nodes visited when estimating an entry's size.
71
+ * Keeps `set` O(1)-ish for large object graphs at the cost of accuracy.
72
+ */
73
+ export declare const SIZE_ESTIMATE_NODE_BUDGET = 512;
40
74
  //# sourceMappingURL=constants.d.ts.map