layercache 1.4.0 → 2.1.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.js CHANGED
@@ -12,12 +12,12 @@ import {
12
12
  validateTag,
13
13
  validateTags,
14
14
  validateTtlPolicy
15
- } from "./chunk-7KMKQ6QZ.js";
15
+ } from "./chunk-6X7NV5BG.js";
16
16
  import {
17
17
  MemoryLayer,
18
18
  TagIndex,
19
19
  createHonoCacheMiddleware
20
- } from "./chunk-FFZCC7EQ.js";
20
+ } from "./chunk-IVX6ABFX.js";
21
21
  import {
22
22
  PatternMatcher,
23
23
  createStoredValueEnvelope,
@@ -157,6 +157,9 @@ function addMetricMap(base, delta) {
157
157
 
158
158
  // src/CacheNamespace.ts
159
159
  var CacheNamespace = class _CacheNamespace {
160
+ /**
161
+ * Creates a namespace backed by an existing cache stack.
162
+ */
160
163
  constructor(cache, prefix) {
161
164
  this.cache = cache;
162
165
  this.prefix = prefix;
@@ -166,9 +169,16 @@ var CacheNamespace = class _CacheNamespace {
166
169
  prefix;
167
170
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
168
171
  metrics = createEmptyNamespaceMetrics();
172
+ /**
173
+ * Reads a key inside this namespace and optionally runs a read-through fetcher
174
+ * on miss or refresh.
175
+ */
169
176
  async get(key, fetcher, options) {
170
177
  return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
171
178
  }
179
+ /**
180
+ * Alias for `get(key, fetcher, options)` that makes the get-or-set behavior explicit.
181
+ */
172
182
  async getOrSet(key, fetcher, options) {
173
183
  return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
174
184
  }
@@ -178,24 +188,69 @@ var CacheNamespace = class _CacheNamespace {
178
188
  async getOrThrow(key, fetcher, options) {
179
189
  return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
180
190
  }
191
+ /**
192
+ * Returns true when the namespaced key exists and has not expired in any layer.
193
+ */
181
194
  async has(key) {
182
195
  return this.trackMetrics(() => this.cache.has(this.qualify(key)));
183
196
  }
197
+ /**
198
+ * Returns the remaining TTL in milliseconds for the namespaced key.
199
+ */
184
200
  async ttl(key) {
185
201
  return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
186
202
  }
203
+ /**
204
+ * Stores a value under a namespaced key.
205
+ */
187
206
  async set(key, value, options) {
188
207
  await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
189
208
  }
209
+ /**
210
+ * Deletes a namespaced key from all layers.
211
+ */
190
212
  async delete(key) {
191
213
  await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
192
214
  }
215
+ /**
216
+ * Deletes multiple namespaced keys from all layers.
217
+ */
193
218
  async mdelete(keys) {
194
219
  await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
195
220
  }
221
+ /**
222
+ * Alias for `delete(key)` scoped to this namespace.
223
+ */
224
+ async invalidateByKey(key) {
225
+ await this.trackMetrics(() => this.cache.invalidateByKey(this.qualify(key)));
226
+ }
227
+ /**
228
+ * Alias for `mdelete(keys)` scoped to this namespace.
229
+ */
230
+ async invalidateByKeys(keys) {
231
+ await this.trackMetrics(() => this.cache.invalidateByKeys(keys.map((k) => this.qualify(k))));
232
+ }
233
+ /**
234
+ * Marks one exact namespaced key expired without deleting its stale value.
235
+ */
236
+ async expireByKey(key) {
237
+ await this.trackMetrics(() => this.cache.expireByKey(this.qualify(key)));
238
+ }
239
+ /**
240
+ * Marks multiple exact namespaced keys expired without deleting their stale values.
241
+ */
242
+ async expireByKeys(keys) {
243
+ await this.trackMetrics(() => this.cache.expireByKeys(keys.map((k) => this.qualify(k))));
244
+ }
245
+ /**
246
+ * Clears all keys in this namespace by invalidating the namespace prefix.
247
+ */
196
248
  async clear() {
197
249
  await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
198
250
  }
251
+ /**
252
+ * Reads many namespaced keys concurrently.
253
+ */
199
254
  async mget(entries) {
200
255
  return this.trackMetrics(
201
256
  () => this.cache.mget(
@@ -207,6 +262,9 @@ var CacheNamespace = class _CacheNamespace {
207
262
  )
208
263
  );
209
264
  }
265
+ /**
266
+ * Writes many namespaced entries concurrently.
267
+ */
210
268
  async mset(entries) {
211
269
  await this.trackMetrics(
212
270
  () => this.cache.mset(
@@ -218,12 +276,21 @@ var CacheNamespace = class _CacheNamespace {
218
276
  )
219
277
  );
220
278
  }
279
+ /**
280
+ * Deletes keys associated with a tag scoped to this namespace.
281
+ */
221
282
  async invalidateByTag(tag) {
222
283
  await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
223
284
  }
285
+ /**
286
+ * Expires keys associated with a tag scoped to this namespace while preserving stale windows.
287
+ */
224
288
  async expireByTag(tag) {
225
289
  await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
226
290
  }
291
+ /**
292
+ * Deletes keys associated with any or all namespace-scoped tags.
293
+ */
227
294
  async invalidateByTags(tags, mode = "any") {
228
295
  await this.trackMetrics(
229
296
  () => this.cache.invalidateByTags(
@@ -232,6 +299,9 @@ var CacheNamespace = class _CacheNamespace {
232
299
  )
233
300
  );
234
301
  }
302
+ /**
303
+ * Expires keys associated with any or all namespace-scoped tags while preserving stale windows.
304
+ */
235
305
  async expireByTags(tags, mode = "any") {
236
306
  await this.trackMetrics(
237
307
  () => this.cache.expireByTags(
@@ -240,15 +310,27 @@ var CacheNamespace = class _CacheNamespace {
240
310
  )
241
311
  );
242
312
  }
313
+ /**
314
+ * Deletes namespaced keys matching a wildcard pattern.
315
+ */
243
316
  async invalidateByPattern(pattern) {
244
317
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
245
318
  }
319
+ /**
320
+ * Expires namespaced keys matching a wildcard pattern while preserving stale windows.
321
+ */
246
322
  async expireByPattern(pattern) {
247
323
  await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
248
324
  }
325
+ /**
326
+ * Deletes namespaced keys with the provided prefix.
327
+ */
249
328
  async invalidateByPrefix(prefix) {
250
329
  await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
251
330
  }
331
+ /**
332
+ * Expires namespaced keys with the provided prefix while preserving stale windows.
333
+ */
252
334
  async expireByPrefix(prefix) {
253
335
  await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
254
336
  }
@@ -265,9 +347,15 @@ var CacheNamespace = class _CacheNamespace {
265
347
  tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
266
348
  };
267
349
  }
350
+ /**
351
+ * Returns a cached wrapper whose generated keys are scoped to this namespace.
352
+ */
268
353
  wrap(keyPrefix, fetcher, options) {
269
354
  return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
270
355
  }
356
+ /**
357
+ * Warms entries after qualifying each key and tag with this namespace prefix.
358
+ */
271
359
  warm(entries, options) {
272
360
  return this.cache.warm(
273
361
  entries.map((entry) => ({
@@ -278,9 +366,15 @@ var CacheNamespace = class _CacheNamespace {
278
366
  options
279
367
  );
280
368
  }
369
+ /**
370
+ * Returns metrics accumulated by operations performed through this namespace.
371
+ */
281
372
  getMetrics() {
282
373
  return cloneNamespaceMetrics(this.metrics);
283
374
  }
375
+ /**
376
+ * Returns hit-rate statistics for operations performed through this namespace.
377
+ */
284
378
  getHitRate() {
285
379
  return computeNamespaceHitRate(this.metrics);
286
380
  }
@@ -297,6 +391,9 @@ var CacheNamespace = class _CacheNamespace {
297
391
  validateNamespaceKey(childPrefix);
298
392
  return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
299
393
  }
394
+ /**
395
+ * Qualifies a raw key with this namespace prefix.
396
+ */
300
397
  qualify(key) {
301
398
  return `${this.prefix}:${key}`;
302
399
  }
@@ -2167,9 +2264,15 @@ var TtlResolver = class {
2167
2264
 
2168
2265
  // src/serialization/JsonSerializer.ts
2169
2266
  var JsonSerializer = class {
2267
+ /**
2268
+ * Serializes a value to JSON.
2269
+ */
2170
2270
  serialize(value) {
2171
2271
  return JSON.stringify(value);
2172
2272
  }
2273
+ /**
2274
+ * Parses JSON and sanitizes the result before returning it.
2275
+ */
2173
2276
  deserialize(payload) {
2174
2277
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2175
2278
  let parsed;
@@ -2196,6 +2299,9 @@ var StampedeGuard = class {
2196
2299
  this.maxInFlight = options.maxInFlight ?? 1e4;
2197
2300
  this.entryTimeoutMs = options.entryTimeoutMs;
2198
2301
  }
2302
+ /**
2303
+ * Deduplicates concurrent work for the same key in this process.
2304
+ */
2199
2305
  async execute(key, task) {
2200
2306
  const existing = this.inFlight.get(key);
2201
2307
  if (existing) {
@@ -2295,6 +2401,9 @@ var DebugLogger = class {
2295
2401
  }
2296
2402
  };
2297
2403
  var CacheStack = class extends EventEmitter {
2404
+ /**
2405
+ * Creates a cache stack from ordered layers and optional global behavior settings.
2406
+ */
2298
2407
  constructor(layers, options = {}) {
2299
2408
  super();
2300
2409
  this.layers = layers;
@@ -2560,6 +2669,10 @@ var CacheStack = class extends EventEmitter {
2560
2669
  });
2561
2670
  });
2562
2671
  }
2672
+ /**
2673
+ * Clears every configured layer, removes tag metadata, resets internal TTL
2674
+ * profiles, and broadcasts a clear invalidation message.
2675
+ */
2563
2676
  async clear() {
2564
2677
  await this.awaitStartup("clear");
2565
2678
  this.maintenance.beginClearEpoch();
@@ -2589,6 +2702,58 @@ var CacheStack = class extends EventEmitter {
2589
2702
  operation: "delete"
2590
2703
  });
2591
2704
  }
2705
+ /**
2706
+ * Alias for `delete(key)` that matches the `invalidateBy*` API family.
2707
+ */
2708
+ async invalidateByKey(key) {
2709
+ await this.delete(key);
2710
+ }
2711
+ /**
2712
+ * Alias for `mdelete(keys)` that matches the `invalidateBy*` API family.
2713
+ */
2714
+ async invalidateByKeys(keys) {
2715
+ await this.mdelete(keys);
2716
+ }
2717
+ /**
2718
+ * Marks one exact key expired without deleting its stale value.
2719
+ */
2720
+ async expireByKey(key) {
2721
+ await this.observeOperation("layercache.expire_by_key", { "layercache.key": String(key ?? "") }, async () => {
2722
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2723
+ await this.awaitStartup("expireByKey");
2724
+ await this.expireKeys([normalizedKey]);
2725
+ await this.publishInvalidation({
2726
+ scope: "key",
2727
+ keys: [normalizedKey],
2728
+ sourceId: this.instanceId,
2729
+ operation: "expire"
2730
+ });
2731
+ });
2732
+ }
2733
+ /**
2734
+ * Marks multiple exact keys expired without deleting their stale values.
2735
+ */
2736
+ async expireByKeys(keys) {
2737
+ await this.observeOperation("layercache.expire_by_keys", void 0, async () => {
2738
+ if (keys.length === 0) {
2739
+ return;
2740
+ }
2741
+ const normalizedKeys = keys.map((k) => validateCacheKey(k));
2742
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
2743
+ await this.awaitStartup("expireByKeys");
2744
+ await this.expireKeys(cacheKeys);
2745
+ await this.publishInvalidation({
2746
+ scope: "keys",
2747
+ keys: cacheKeys,
2748
+ sourceId: this.instanceId,
2749
+ operation: "expire"
2750
+ });
2751
+ });
2752
+ }
2753
+ /**
2754
+ * Reads many keys concurrently. Simple reads use layer-level bulk fast paths;
2755
+ * entries with fetchers or options fall back to per-entry read-through logic.
2756
+ */
2592
2757
  async mget(entries) {
2593
2758
  return this.observeOperation("layercache.mget", void 0, async () => {
2594
2759
  this.assertActive("mget");
@@ -2676,6 +2841,10 @@ var CacheStack = class extends EventEmitter {
2676
2841
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2677
2842
  });
2678
2843
  }
2844
+ /**
2845
+ * Writes many entries concurrently using each layer's bulk write fast path
2846
+ * when available.
2847
+ */
2679
2848
  async mset(entries) {
2680
2849
  await this.observeOperation("layercache.mset", void 0, async () => {
2681
2850
  this.assertActive("mset");
@@ -2688,6 +2857,10 @@ var CacheStack = class extends EventEmitter {
2688
2857
  await this.writeBatch(normalizedEntries);
2689
2858
  });
2690
2859
  }
2860
+ /**
2861
+ * Pre-populates cache entries by running their fetchers with bounded
2862
+ * concurrency. Higher-priority entries run first.
2863
+ */
2691
2864
  async warm(entries, options = {}) {
2692
2865
  this.assertActive("warm");
2693
2866
  const concurrency = Math.max(1, options.concurrency ?? 4);
@@ -2738,6 +2911,10 @@ var CacheStack = class extends EventEmitter {
2738
2911
  validateNamespaceKey(prefix);
2739
2912
  return new CacheNamespace(this, prefix);
2740
2913
  }
2914
+ /**
2915
+ * Deletes every key currently associated with `tag` and broadcasts an
2916
+ * invalidation message.
2917
+ */
2741
2918
  async invalidateByTag(tag) {
2742
2919
  await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
2743
2920
  validateTag(tag);
@@ -2747,6 +2924,10 @@ var CacheStack = class extends EventEmitter {
2747
2924
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2748
2925
  });
2749
2926
  }
2927
+ /**
2928
+ * Marks every key associated with `tag` as expired while preserving stale
2929
+ * windows for stale serving.
2930
+ */
2750
2931
  async expireByTag(tag) {
2751
2932
  await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
2752
2933
  validateTag(tag);
@@ -2756,6 +2937,10 @@ var CacheStack = class extends EventEmitter {
2756
2937
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2757
2938
  });
2758
2939
  }
2940
+ /**
2941
+ * Deletes keys associated with any or all of the provided tags and broadcasts
2942
+ * an invalidation message.
2943
+ */
2759
2944
  async invalidateByTags(tags, mode = "any") {
2760
2945
  await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2761
2946
  if (tags.length === 0) {
@@ -2772,6 +2957,10 @@ var CacheStack = class extends EventEmitter {
2772
2957
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2773
2958
  });
2774
2959
  }
2960
+ /**
2961
+ * Marks keys associated with any or all of the provided tags as expired while
2962
+ * preserving stale windows for stale serving.
2963
+ */
2775
2964
  async expireByTags(tags, mode = "any") {
2776
2965
  await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
2777
2966
  if (tags.length === 0) {
@@ -2788,6 +2977,9 @@ var CacheStack = class extends EventEmitter {
2788
2977
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2789
2978
  });
2790
2979
  }
2980
+ /**
2981
+ * Deletes keys matching a wildcard pattern such as `user:*`.
2982
+ */
2791
2983
  async invalidateByPattern(pattern) {
2792
2984
  await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
2793
2985
  validatePattern(pattern);
@@ -2800,6 +2992,10 @@ var CacheStack = class extends EventEmitter {
2800
2992
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2801
2993
  });
2802
2994
  }
2995
+ /**
2996
+ * Marks keys matching a wildcard pattern as expired while preserving stale
2997
+ * windows for stale serving.
2998
+ */
2803
2999
  async expireByPattern(pattern) {
2804
3000
  await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
2805
3001
  validatePattern(pattern);
@@ -2812,6 +3008,9 @@ var CacheStack = class extends EventEmitter {
2812
3008
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2813
3009
  });
2814
3010
  }
3011
+ /**
3012
+ * Deletes keys that start with the provided prefix.
3013
+ */
2815
3014
  async invalidateByPrefix(prefix) {
2816
3015
  await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2817
3016
  await this.awaitStartup("invalidateByPrefix");
@@ -2821,6 +3020,10 @@ var CacheStack = class extends EventEmitter {
2821
3020
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2822
3021
  });
2823
3022
  }
3023
+ /**
3024
+ * Marks keys that start with the provided prefix as expired while preserving
3025
+ * stale windows for stale serving.
3026
+ */
2824
3027
  async expireByPrefix(prefix) {
2825
3028
  await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
2826
3029
  await this.awaitStartup("expireByPrefix");
@@ -2830,9 +3033,15 @@ var CacheStack = class extends EventEmitter {
2830
3033
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2831
3034
  });
2832
3035
  }
3036
+ /**
3037
+ * Returns cumulative cache metrics since startup or the last `resetMetrics()`.
3038
+ */
2833
3039
  getMetrics() {
2834
3040
  return this.metricsCollector.snapshot;
2835
3041
  }
3042
+ /**
3043
+ * Returns metrics plus layer degradation state and active background refresh count.
3044
+ */
2836
3045
  getStats() {
2837
3046
  return {
2838
3047
  metrics: this.getMetrics(),
@@ -2844,6 +3053,9 @@ var CacheStack = class extends EventEmitter {
2844
3053
  backgroundRefreshes: this.reader.activeRefreshCount
2845
3054
  };
2846
3055
  }
3056
+ /**
3057
+ * Resets cumulative metrics counters.
3058
+ */
2847
3059
  resetMetrics() {
2848
3060
  this.metricsCollector.reset();
2849
3061
  }
@@ -2853,6 +3065,10 @@ var CacheStack = class extends EventEmitter {
2853
3065
  getHitRate() {
2854
3066
  return this.metricsCollector.hitRate();
2855
3067
  }
3068
+ /**
3069
+ * Runs each layer's `ping()` hook when available and returns per-layer health
3070
+ * and latency information.
3071
+ */
2856
3072
  async healthCheck() {
2857
3073
  await this.startup;
2858
3074
  return Promise.all(
@@ -2937,22 +3153,38 @@ var CacheStack = class extends EventEmitter {
2937
3153
  const tags = await this.getTagsForKey(normalizedKey);
2938
3154
  return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
2939
3155
  }
3156
+ /**
3157
+ * Exports cache entries from configured layers for process-local snapshots.
3158
+ */
2940
3159
  async exportState() {
2941
3160
  await this.awaitStartup("exportState");
2942
3161
  return this.snapshots.exportState(this.snapshotMaxEntries());
2943
3162
  }
3163
+ /**
3164
+ * Imports entries produced by `exportState()` into the configured layers.
3165
+ */
2944
3166
  async importState(entries) {
2945
3167
  await this.awaitStartup("importState");
2946
3168
  await this.snapshots.importState(entries);
2947
3169
  }
3170
+ /**
3171
+ * Writes a snapshot file containing current cache entries.
3172
+ */
2948
3173
  async persistToFile(filePath) {
2949
3174
  this.assertActive("persistToFile");
2950
3175
  await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
2951
3176
  }
3177
+ /**
3178
+ * Restores cache entries from a snapshot file.
3179
+ */
2952
3180
  async restoreFromFile(filePath) {
2953
3181
  this.assertActive("restoreFromFile");
2954
3182
  await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
2955
3183
  }
3184
+ /**
3185
+ * Flushes background work, unsubscribes from buses, disposes timers, and then
3186
+ * disposes each layer that provides `dispose()`.
3187
+ */
2956
3188
  async disconnect() {
2957
3189
  if (!this.disconnectPromise) {
2958
3190
  this.isDisconnecting = true;
@@ -3452,6 +3684,9 @@ var RedisInvalidationBus = class {
3452
3684
  this.channel = options.channel ?? "layercache:invalidation";
3453
3685
  this.logger = options.logger;
3454
3686
  }
3687
+ /**
3688
+ * Subscribes to invalidation messages and returns an unsubscribe function.
3689
+ */
3455
3690
  async subscribe(handler) {
3456
3691
  const previousPromise = this.subscribePromise;
3457
3692
  let resolveThis;
@@ -3483,6 +3718,9 @@ var RedisInvalidationBus = class {
3483
3718
  }
3484
3719
  };
3485
3720
  }
3721
+ /**
3722
+ * Publishes an invalidation message to other subscribers.
3723
+ */
3486
3724
  async publish(message) {
3487
3725
  await this.publisher.publish(this.channel, JSON.stringify(message));
3488
3726
  }
@@ -3783,6 +4021,9 @@ var RedisLayer = class {
3783
4021
  decompressionMaxBytes;
3784
4022
  commandTimeoutMs;
3785
4023
  disconnectOnDispose;
4024
+ /**
4025
+ * Creates a Redis cache layer using an existing ioredis client.
4026
+ */
3786
4027
  constructor(options) {
3787
4028
  this.client = options.client;
3788
4029
  this.defaultTtl = options.ttl;
@@ -3797,10 +4038,16 @@ var RedisLayer = class {
3797
4038
  this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
3798
4039
  this.disconnectOnDispose = options.disconnectOnDispose ?? false;
3799
4040
  }
4041
+ /**
4042
+ * Reads and unwraps a fresh value from Redis.
4043
+ */
3800
4044
  async get(key) {
3801
4045
  const payload = await this.getEntry(key);
3802
4046
  return unwrapStoredValue(payload);
3803
4047
  }
4048
+ /**
4049
+ * Reads the raw stored value or envelope from Redis.
4050
+ */
3804
4051
  async getEntry(key) {
3805
4052
  this.validateKey(key);
3806
4053
  const payload = await this.runCommand(
@@ -3812,6 +4059,9 @@ var RedisLayer = class {
3812
4059
  }
3813
4060
  return this.deserializeOrDelete(key, payload);
3814
4061
  }
4062
+ /**
4063
+ * Reads many raw entries from Redis using a pipeline.
4064
+ */
3815
4065
  async getMany(keys) {
3816
4066
  if (keys.length === 0) {
3817
4067
  return [];
@@ -3837,6 +4087,9 @@ var RedisLayer = class {
3837
4087
  })
3838
4088
  );
3839
4089
  }
4090
+ /**
4091
+ * Writes many entries to Redis using a pipeline.
4092
+ */
3840
4093
  async setMany(entries) {
3841
4094
  if (entries.length === 0) {
3842
4095
  return;
@@ -3857,6 +4110,9 @@ var RedisLayer = class {
3857
4110
  }
3858
4111
  await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
3859
4112
  }
4113
+ /**
4114
+ * Stores a value in Redis using the provided TTL or layer default TTL.
4115
+ */
3860
4116
  async set(key, value, ttl = this.defaultTtl) {
3861
4117
  this.validateKey(key);
3862
4118
  const serialized = this.primarySerializer().serialize(value);
@@ -3871,10 +4127,16 @@ var RedisLayer = class {
3871
4127
  }
3872
4128
  await this.runCommand(`set(${this.displayKey(key)})`, () => this.client.set(normalizedKey, payload));
3873
4129
  }
4130
+ /**
4131
+ * Deletes a key from Redis.
4132
+ */
3874
4133
  async delete(key) {
3875
4134
  this.validateKey(key);
3876
4135
  await this.runCommand(`delete(${this.displayKey(key)})`, () => this.client.del(this.withPrefix(key)));
3877
4136
  }
4137
+ /**
4138
+ * Deletes multiple keys from Redis in batches.
4139
+ */
3878
4140
  async deleteMany(keys) {
3879
4141
  if (keys.length === 0) {
3880
4142
  return;
@@ -3887,11 +4149,17 @@ var RedisLayer = class {
3887
4149
  () => this.client.del(...keys.map((key) => this.withPrefix(key)))
3888
4150
  );
3889
4151
  }
4152
+ /**
4153
+ * Returns true when the key exists in Redis.
4154
+ */
3890
4155
  async has(key) {
3891
4156
  this.validateKey(key);
3892
4157
  const exists = await this.runCommand(`has(${this.displayKey(key)})`, () => this.client.exists(this.withPrefix(key)));
3893
4158
  return exists > 0;
3894
4159
  }
4160
+ /**
4161
+ * Returns remaining Redis TTL in milliseconds, or null when absent or non-expiring.
4162
+ */
3895
4163
  async ttl(key) {
3896
4164
  this.validateKey(key);
3897
4165
  const remaining = await this.runCommand(
@@ -3903,6 +4171,9 @@ var RedisLayer = class {
3903
4171
  }
3904
4172
  return remaining;
3905
4173
  }
4174
+ /**
4175
+ * Returns the number of keys under this layer's prefix.
4176
+ */
3906
4177
  async size() {
3907
4178
  if (!this.prefix) {
3908
4179
  return this.runCommand("dbsize()", () => this.client.dbsize());
@@ -3920,6 +4191,9 @@ var RedisLayer = class {
3920
4191
  } while (cursor !== "0");
3921
4192
  return count;
3922
4193
  }
4194
+ /**
4195
+ * Runs a Redis ping command.
4196
+ */
3923
4197
  async ping() {
3924
4198
  try {
3925
4199
  return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
@@ -3927,6 +4201,9 @@ var RedisLayer = class {
3927
4201
  return false;
3928
4202
  }
3929
4203
  }
4204
+ /**
4205
+ * Disconnects the Redis client when `disconnectOnDispose` is enabled.
4206
+ */
3930
4207
  async dispose() {
3931
4208
  if (this.disconnectOnDispose) {
3932
4209
  this.client.disconnect();
@@ -3959,6 +4236,9 @@ var RedisLayer = class {
3959
4236
  }
3960
4237
  } while (cursor !== "0");
3961
4238
  }
4239
+ /**
4240
+ * Returns keys under this layer's prefix without the prefix included.
4241
+ */
3962
4242
  async keys() {
3963
4243
  const pattern = `${this.prefix}*`;
3964
4244
  const keys = await this.scanKeys(pattern);
@@ -3967,6 +4247,9 @@ var RedisLayer = class {
3967
4247
  }
3968
4248
  return keys.map((key) => key.slice(this.prefix.length));
3969
4249
  }
4250
+ /**
4251
+ * Visits keys under this layer's prefix without materializing all results.
4252
+ */
3970
4253
  async forEachKey(visitor) {
3971
4254
  const pattern = `${this.prefix}*`;
3972
4255
  let cursor = "0";
@@ -4324,6 +4607,9 @@ var DiskLayer = class {
4324
4607
  maxEntryBytes;
4325
4608
  protection;
4326
4609
  writeQueue = Promise.resolve();
4610
+ /**
4611
+ * Creates a disk-backed cache layer.
4612
+ */
4327
4613
  constructor(options) {
4328
4614
  this.directory = this.resolveDirectory(options.directory);
4329
4615
  this.defaultTtl = options.ttl;
@@ -4336,9 +4622,15 @@ var DiskLayer = class {
4336
4622
  signingKey: options.signingKey
4337
4623
  });
4338
4624
  }
4625
+ /**
4626
+ * Reads and unwraps a fresh value from disk.
4627
+ */
4339
4628
  async get(key) {
4340
4629
  return unwrapStoredValue(await this.getEntry(key));
4341
4630
  }
4631
+ /**
4632
+ * Reads the raw stored value or envelope from disk.
4633
+ */
4342
4634
  async getEntry(key) {
4343
4635
  const filePath = this.keyToPath(key);
4344
4636
  const raw = await this.readEntryFile(filePath);
@@ -4358,6 +4650,9 @@ var DiskLayer = class {
4358
4650
  }
4359
4651
  return entry.value;
4360
4652
  }
4653
+ /**
4654
+ * Stores a value on disk using the provided TTL or layer default TTL.
4655
+ */
4361
4656
  async set(key, value, ttl = this.defaultTtl) {
4362
4657
  await this.enqueueWrite(async () => {
4363
4658
  await fs2.mkdir(this.directory, { recursive: true });
@@ -4383,16 +4678,28 @@ var DiskLayer = class {
4383
4678
  }
4384
4679
  });
4385
4680
  }
4681
+ /**
4682
+ * Reads many raw entries from disk.
4683
+ */
4386
4684
  async getMany(keys) {
4387
4685
  return Promise.all(keys.map((key) => this.getEntry(key)));
4388
4686
  }
4687
+ /**
4688
+ * Writes many entries to disk.
4689
+ */
4389
4690
  async setMany(entries) {
4390
4691
  await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
4391
4692
  }
4693
+ /**
4694
+ * Returns true when the key exists and has not expired.
4695
+ */
4392
4696
  async has(key) {
4393
4697
  const value = await this.getEntry(key);
4394
4698
  return value !== null;
4395
4699
  }
4700
+ /**
4701
+ * Returns remaining TTL in milliseconds, or null when absent or non-expiring.
4702
+ */
4396
4703
  async ttl(key) {
4397
4704
  const filePath = this.keyToPath(key);
4398
4705
  const raw = await this.readEntryFile(filePath);
@@ -4415,14 +4722,23 @@ var DiskLayer = class {
4415
4722
  }
4416
4723
  return remaining;
4417
4724
  }
4725
+ /**
4726
+ * Deletes a key from disk.
4727
+ */
4418
4728
  async delete(key) {
4419
4729
  await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
4420
4730
  }
4731
+ /**
4732
+ * Deletes multiple keys from disk.
4733
+ */
4421
4734
  async deleteMany(keys) {
4422
4735
  await this.enqueueWrite(async () => {
4423
4736
  await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
4424
4737
  });
4425
4738
  }
4739
+ /**
4740
+ * Removes all cache entry files from this layer's directory.
4741
+ */
4426
4742
  async clear() {
4427
4743
  await this.enqueueWrite(async () => {
4428
4744
  let entries;
@@ -4447,11 +4763,17 @@ var DiskLayer = class {
4447
4763
  });
4448
4764
  return keys;
4449
4765
  }
4766
+ /**
4767
+ * Visits all non-expired keys stored on disk.
4768
+ */
4450
4769
  async forEachKey(visitor) {
4451
4770
  await this.scanEntries(async (entry) => {
4452
4771
  await visitor(entry.key);
4453
4772
  });
4454
4773
  }
4774
+ /**
4775
+ * Returns the number of non-expired entries stored on disk.
4776
+ */
4455
4777
  async size() {
4456
4778
  let count = 0;
4457
4779
  await this.scanEntries(async () => {
@@ -4459,6 +4781,9 @@ var DiskLayer = class {
4459
4781
  });
4460
4782
  return count;
4461
4783
  }
4784
+ /**
4785
+ * Verifies the cache directory can be created.
4786
+ */
4462
4787
  async ping() {
4463
4788
  try {
4464
4789
  await fs2.mkdir(this.directory, { recursive: true });
@@ -4467,6 +4792,9 @@ var DiskLayer = class {
4467
4792
  return false;
4468
4793
  }
4469
4794
  }
4795
+ /**
4796
+ * Reserved for interface compatibility; DiskLayer does not hold persistent handles.
4797
+ */
4470
4798
  async dispose() {
4471
4799
  }
4472
4800
  keyToPath(key) {
@@ -4670,6 +4998,9 @@ var MemcachedLayer = class {
4670
4998
  client;
4671
4999
  keyPrefix;
4672
5000
  serializer;
5001
+ /**
5002
+ * Creates a Memcached cache layer using a compatible client.
5003
+ */
4673
5004
  constructor(options) {
4674
5005
  this.client = options.client;
4675
5006
  this.defaultTtl = options.ttl;
@@ -4677,9 +5008,15 @@ var MemcachedLayer = class {
4677
5008
  this.keyPrefix = options.keyPrefix ?? "";
4678
5009
  this.serializer = options.serializer ?? new JsonSerializer();
4679
5010
  }
5011
+ /**
5012
+ * Reads and unwraps a fresh value from Memcached.
5013
+ */
4680
5014
  async get(key) {
4681
5015
  return unwrapStoredValue(await this.getEntry(key));
4682
5016
  }
5017
+ /**
5018
+ * Reads the raw stored value or envelope from Memcached.
5019
+ */
4683
5020
  async getEntry(key) {
4684
5021
  this.validateKey(key);
4685
5022
  const result = await this.client.get(this.withPrefix(key));
@@ -4692,9 +5029,15 @@ var MemcachedLayer = class {
4692
5029
  return null;
4693
5030
  }
4694
5031
  }
5032
+ /**
5033
+ * Reads many raw entries from Memcached.
5034
+ */
4695
5035
  async getMany(keys) {
4696
5036
  return Promise.all(keys.map((key) => this.getEntry(key)));
4697
5037
  }
5038
+ /**
5039
+ * Stores a value in Memcached using the provided TTL or layer default TTL.
5040
+ */
4698
5041
  async set(key, value, ttl = this.defaultTtl) {
4699
5042
  this.validateKey(key);
4700
5043
  const payload = this.serializer.serialize(value);
@@ -4702,18 +5045,30 @@ var MemcachedLayer = class {
4702
5045
  expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
4703
5046
  });
4704
5047
  }
5048
+ /**
5049
+ * Returns true when the key exists in Memcached.
5050
+ */
4705
5051
  async has(key) {
4706
5052
  this.validateKey(key);
4707
5053
  const result = await this.client.get(this.withPrefix(key));
4708
5054
  return result !== null && result.value !== null;
4709
5055
  }
5056
+ /**
5057
+ * Deletes a key from Memcached.
5058
+ */
4710
5059
  async delete(key) {
4711
5060
  this.validateKey(key);
4712
5061
  await this.client.delete(this.withPrefix(key));
4713
5062
  }
5063
+ /**
5064
+ * Deletes multiple keys from Memcached.
5065
+ */
4714
5066
  async deleteMany(keys) {
4715
5067
  await Promise.all(keys.map((key) => this.delete(key)));
4716
5068
  }
5069
+ /**
5070
+ * Always throws because Memcached has no safe prefix clear primitive.
5071
+ */
4717
5072
  async clear() {
4718
5073
  throw new Error(
4719
5074
  "MemcachedLayer.clear() is not supported. Use a key prefix and rotate it to effectively invalidate all keys."
@@ -4741,9 +5096,15 @@ var MemcachedLayer = class {
4741
5096
  // src/serialization/MsgpackSerializer.ts
4742
5097
  import { decode, encode } from "@msgpack/msgpack";
4743
5098
  var MsgpackSerializer = class {
5099
+ /**
5100
+ * Serializes a value to MessagePack bytes.
5101
+ */
4744
5102
  serialize(value) {
4745
5103
  return Buffer.from(encode(value));
4746
5104
  }
5105
+ /**
5106
+ * Decodes MessagePack bytes and sanitizes the result before returning it.
5107
+ */
4747
5108
  deserialize(payload) {
4748
5109
  const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
4749
5110
  return sanitizeStructuredData(decode(normalized), {
@@ -4777,6 +5138,10 @@ var RedisSingleFlightCoordinator = class {
4777
5138
  this.prefix = options.prefix ?? "layercache:singleflight";
4778
5139
  this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
4779
5140
  }
5141
+ /**
5142
+ * Executes `worker` when this process acquires the Redis lock; otherwise runs
5143
+ * `waiter` while another process owns the work.
5144
+ */
4780
5145
  async execute(key, options, worker, waiter) {
4781
5146
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
4782
5147
  const token = randomUUID();