layercache 1.2.5 → 1.2.7

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.
@@ -259,27 +259,147 @@ var Mutex = class {
259
259
  }
260
260
  };
261
261
 
262
+ // ../../src/internal/CacheNamespaceMetrics.ts
263
+ function createEmptyNamespaceMetrics(resetAt = Date.now()) {
264
+ return {
265
+ hits: 0,
266
+ misses: 0,
267
+ fetches: 0,
268
+ sets: 0,
269
+ deletes: 0,
270
+ backfills: 0,
271
+ invalidations: 0,
272
+ staleHits: 0,
273
+ refreshes: 0,
274
+ refreshErrors: 0,
275
+ writeFailures: 0,
276
+ singleFlightWaits: 0,
277
+ negativeCacheHits: 0,
278
+ circuitBreakerTrips: 0,
279
+ degradedOperations: 0,
280
+ hitsByLayer: {},
281
+ missesByLayer: {},
282
+ latencyByLayer: {},
283
+ resetAt
284
+ };
285
+ }
286
+ function cloneNamespaceMetrics(metrics) {
287
+ return {
288
+ ...metrics,
289
+ hitsByLayer: { ...metrics.hitsByLayer },
290
+ missesByLayer: { ...metrics.missesByLayer },
291
+ latencyByLayer: Object.fromEntries(
292
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
293
+ )
294
+ };
295
+ }
296
+ function diffNamespaceMetrics(before, after) {
297
+ const latencyByLayer = Object.fromEntries(
298
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
299
+ layer,
300
+ {
301
+ avgMs: value.avgMs,
302
+ maxMs: value.maxMs,
303
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
304
+ }
305
+ ])
306
+ );
307
+ return {
308
+ hits: after.hits - before.hits,
309
+ misses: after.misses - before.misses,
310
+ fetches: after.fetches - before.fetches,
311
+ sets: after.sets - before.sets,
312
+ deletes: after.deletes - before.deletes,
313
+ backfills: after.backfills - before.backfills,
314
+ invalidations: after.invalidations - before.invalidations,
315
+ staleHits: after.staleHits - before.staleHits,
316
+ refreshes: after.refreshes - before.refreshes,
317
+ refreshErrors: after.refreshErrors - before.refreshErrors,
318
+ writeFailures: after.writeFailures - before.writeFailures,
319
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
320
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
321
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
322
+ degradedOperations: after.degradedOperations - before.degradedOperations,
323
+ hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
324
+ missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
325
+ latencyByLayer,
326
+ resetAt: after.resetAt
327
+ };
328
+ }
329
+ function addNamespaceMetrics(base, delta) {
330
+ return {
331
+ hits: base.hits + delta.hits,
332
+ misses: base.misses + delta.misses,
333
+ fetches: base.fetches + delta.fetches,
334
+ sets: base.sets + delta.sets,
335
+ deletes: base.deletes + delta.deletes,
336
+ backfills: base.backfills + delta.backfills,
337
+ invalidations: base.invalidations + delta.invalidations,
338
+ staleHits: base.staleHits + delta.staleHits,
339
+ refreshes: base.refreshes + delta.refreshes,
340
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
341
+ writeFailures: base.writeFailures + delta.writeFailures,
342
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
343
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
344
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
345
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
346
+ hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
347
+ missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
348
+ latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
349
+ resetAt: base.resetAt
350
+ };
351
+ }
352
+ function computeNamespaceHitRate(metrics) {
353
+ const total = metrics.hits + metrics.misses;
354
+ const overall = total === 0 ? 0 : metrics.hits / total;
355
+ const byLayer = {};
356
+ const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
357
+ for (const layer of layers) {
358
+ const hits = metrics.hitsByLayer[layer] ?? 0;
359
+ const misses = metrics.missesByLayer[layer] ?? 0;
360
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
361
+ }
362
+ return { overall, byLayer };
363
+ }
364
+ function diffMetricMap(before, after) {
365
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
366
+ const result = {};
367
+ for (const key of keys) {
368
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
369
+ }
370
+ return result;
371
+ }
372
+ function addMetricMap(base, delta) {
373
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
374
+ const result = {};
375
+ for (const key of keys) {
376
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
377
+ }
378
+ return result;
379
+ }
380
+
262
381
  // ../../src/CacheNamespace.ts
263
382
  var CacheNamespace = class _CacheNamespace {
264
383
  constructor(cache, prefix) {
265
384
  this.cache = cache;
266
385
  this.prefix = prefix;
386
+ validateNamespaceKey(prefix);
267
387
  }
268
388
  cache;
269
389
  prefix;
270
390
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
271
- metrics = emptyMetrics();
391
+ metrics = createEmptyNamespaceMetrics();
272
392
  async get(key, fetcher, options) {
273
- return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
393
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
274
394
  }
275
395
  async getOrSet(key, fetcher, options) {
276
- return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
396
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
277
397
  }
278
398
  /**
279
399
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
280
400
  */
281
401
  async getOrThrow(key, fetcher, options) {
282
- return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
402
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
283
403
  }
284
404
  async has(key) {
285
405
  return this.trackMetrics(() => this.cache.has(this.qualify(key)));
@@ -288,7 +408,7 @@ var CacheNamespace = class _CacheNamespace {
288
408
  return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
289
409
  }
290
410
  async set(key, value, options) {
291
- await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
411
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
292
412
  }
293
413
  async delete(key) {
294
414
  await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
@@ -304,7 +424,8 @@ var CacheNamespace = class _CacheNamespace {
304
424
  () => this.cache.mget(
305
425
  entries.map((entry) => ({
306
426
  ...entry,
307
- key: this.qualify(entry.key)
427
+ key: this.qualify(entry.key),
428
+ options: this.qualifyGetOptions(entry.options)
308
429
  }))
309
430
  )
310
431
  );
@@ -314,16 +435,22 @@ var CacheNamespace = class _CacheNamespace {
314
435
  () => this.cache.mset(
315
436
  entries.map((entry) => ({
316
437
  ...entry,
317
- key: this.qualify(entry.key)
438
+ key: this.qualify(entry.key),
439
+ options: this.qualifyWriteOptions(entry.options)
318
440
  }))
319
441
  )
320
442
  );
321
443
  }
322
444
  async invalidateByTag(tag) {
323
- await this.trackMetrics(() => this.cache.invalidateByTag(tag));
445
+ await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
324
446
  }
325
447
  async invalidateByTags(tags, mode = "any") {
326
- await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
448
+ await this.trackMetrics(
449
+ () => this.cache.invalidateByTags(
450
+ tags.map((tag) => this.qualifyTag(tag)),
451
+ mode
452
+ )
453
+ );
327
454
  }
328
455
  async invalidateByPattern(pattern) {
329
456
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
@@ -335,34 +462,33 @@ var CacheNamespace = class _CacheNamespace {
335
462
  * Returns detailed metadata about a single cache key within this namespace.
336
463
  */
337
464
  async inspect(key) {
338
- return this.cache.inspect(this.qualify(key));
465
+ const result = await this.cache.inspect(this.qualify(key));
466
+ if (result === null) {
467
+ return null;
468
+ }
469
+ return {
470
+ ...result,
471
+ tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
472
+ };
339
473
  }
340
474
  wrap(keyPrefix, fetcher, options) {
341
- return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
475
+ return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
342
476
  }
343
477
  warm(entries, options) {
344
478
  return this.cache.warm(
345
479
  entries.map((entry) => ({
346
480
  ...entry,
347
- key: this.qualify(entry.key)
481
+ key: this.qualify(entry.key),
482
+ options: this.qualifyGetOptions(entry.options)
348
483
  })),
349
484
  options
350
485
  );
351
486
  }
352
487
  getMetrics() {
353
- return cloneMetrics(this.metrics);
488
+ return cloneNamespaceMetrics(this.metrics);
354
489
  }
355
490
  getHitRate() {
356
- const total = this.metrics.hits + this.metrics.misses;
357
- const overall = total === 0 ? 0 : this.metrics.hits / total;
358
- const byLayer = {};
359
- const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
360
- for (const layer of layers) {
361
- const hits = this.metrics.hitsByLayer[layer] ?? 0;
362
- const misses = this.metrics.missesByLayer[layer] ?? 0;
363
- byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
364
- }
365
- return { overall, byLayer };
491
+ return computeNamespaceHitRate(this.metrics);
366
492
  }
367
493
  /**
368
494
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -380,12 +506,30 @@ var CacheNamespace = class _CacheNamespace {
380
506
  qualify(key) {
381
507
  return `${this.prefix}:${key}`;
382
508
  }
509
+ qualifyTag(tag) {
510
+ return `${this.prefix}:${tag}`;
511
+ }
512
+ qualifyGetOptions(options) {
513
+ return this.qualifyWriteOptions(options);
514
+ }
515
+ qualifyWrapOptions(options) {
516
+ return this.qualifyWriteOptions(options);
517
+ }
518
+ qualifyWriteOptions(options) {
519
+ if (!options?.tags || options.tags.length === 0) {
520
+ return options;
521
+ }
522
+ return {
523
+ ...options,
524
+ tags: options.tags.map((tag) => this.qualifyTag(tag))
525
+ };
526
+ }
383
527
  async trackMetrics(operation) {
384
528
  return this.getMetricsMutex().runExclusive(async () => {
385
529
  const before = this.cache.getMetrics();
386
530
  const result = await operation();
387
531
  const after = this.cache.getMetrics();
388
- this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
532
+ this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
389
533
  return result;
390
534
  });
391
535
  }
@@ -399,223 +543,773 @@ var CacheNamespace = class _CacheNamespace {
399
543
  return mutex;
400
544
  }
401
545
  };
402
- function emptyMetrics() {
403
- return {
404
- hits: 0,
405
- misses: 0,
406
- fetches: 0,
407
- sets: 0,
408
- deletes: 0,
409
- backfills: 0,
410
- invalidations: 0,
411
- staleHits: 0,
412
- refreshes: 0,
413
- refreshErrors: 0,
414
- writeFailures: 0,
415
- singleFlightWaits: 0,
416
- negativeCacheHits: 0,
417
- circuitBreakerTrips: 0,
418
- degradedOperations: 0,
419
- hitsByLayer: {},
420
- missesByLayer: {},
421
- latencyByLayer: {},
422
- resetAt: Date.now()
423
- };
424
- }
425
- function cloneMetrics(metrics) {
426
- return {
427
- ...metrics,
428
- hitsByLayer: { ...metrics.hitsByLayer },
429
- missesByLayer: { ...metrics.missesByLayer },
430
- latencyByLayer: Object.fromEntries(
431
- Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
432
- )
433
- };
546
+ function validateNamespaceKey(key) {
547
+ if (key.length === 0) {
548
+ throw new Error("Namespace prefix must not be empty.");
549
+ }
550
+ if (key.length > 256) {
551
+ throw new Error("Namespace prefix must be at most 256 characters.");
552
+ }
553
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
554
+ throw new Error("Namespace prefix contains unsupported control characters.");
555
+ }
556
+ if (/[\uD800-\uDFFF]/.test(key)) {
557
+ throw new Error("Namespace prefix contains unsupported surrogate code points.");
558
+ }
434
559
  }
435
- function diffMetrics(before, after) {
436
- const latencyByLayer = Object.fromEntries(
437
- Object.entries(after.latencyByLayer).map(([layer, value]) => [
438
- layer,
439
- {
440
- avgMs: value.avgMs,
441
- maxMs: value.maxMs,
442
- count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
560
+
561
+ // ../../src/invalidation/PatternMatcher.ts
562
+ var PatternMatcher = class _PatternMatcher {
563
+ /**
564
+ * Tests whether a glob-style pattern matches a value.
565
+ * Supports `*` (any sequence of characters) and `?` (any single character).
566
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
567
+ * quadratic memory usage on long patterns/keys.
568
+ */
569
+ static matches(pattern, value) {
570
+ return _PatternMatcher.matchLinear(pattern, value);
571
+ }
572
+ /**
573
+ * Linear-time glob matching with O(1) extra memory.
574
+ */
575
+ static matchLinear(pattern, value) {
576
+ let patternIndex = 0;
577
+ let valueIndex = 0;
578
+ let starIndex = -1;
579
+ let backtrackValueIndex = 0;
580
+ while (valueIndex < value.length) {
581
+ const patternChar = pattern[patternIndex];
582
+ const valueChar = value[valueIndex];
583
+ if (patternChar === "*" && patternIndex < pattern.length) {
584
+ starIndex = patternIndex;
585
+ patternIndex += 1;
586
+ backtrackValueIndex = valueIndex;
587
+ continue;
443
588
  }
444
- ])
445
- );
446
- return {
447
- hits: after.hits - before.hits,
448
- misses: after.misses - before.misses,
449
- fetches: after.fetches - before.fetches,
450
- sets: after.sets - before.sets,
451
- deletes: after.deletes - before.deletes,
452
- backfills: after.backfills - before.backfills,
453
- invalidations: after.invalidations - before.invalidations,
454
- staleHits: after.staleHits - before.staleHits,
455
- refreshes: after.refreshes - before.refreshes,
456
- refreshErrors: after.refreshErrors - before.refreshErrors,
457
- writeFailures: after.writeFailures - before.writeFailures,
458
- singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
459
- negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
460
- circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
461
- degradedOperations: after.degradedOperations - before.degradedOperations,
462
- hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
463
- missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
464
- latencyByLayer,
465
- resetAt: after.resetAt
466
- };
467
- }
468
- function addMetrics(base, delta) {
469
- return {
470
- hits: base.hits + delta.hits,
471
- misses: base.misses + delta.misses,
472
- fetches: base.fetches + delta.fetches,
473
- sets: base.sets + delta.sets,
474
- deletes: base.deletes + delta.deletes,
475
- backfills: base.backfills + delta.backfills,
476
- invalidations: base.invalidations + delta.invalidations,
477
- staleHits: base.staleHits + delta.staleHits,
478
- refreshes: base.refreshes + delta.refreshes,
479
- refreshErrors: base.refreshErrors + delta.refreshErrors,
480
- writeFailures: base.writeFailures + delta.writeFailures,
481
- singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
482
- negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
483
- circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
484
- degradedOperations: base.degradedOperations + delta.degradedOperations,
485
- hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
486
- missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
487
- latencyByLayer: cloneMetrics(delta).latencyByLayer,
488
- resetAt: base.resetAt
489
- };
490
- }
491
- function diffMap(before, after) {
492
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
493
- const result = {};
494
- for (const key of keys) {
495
- result[key] = (after[key] ?? 0) - (before[key] ?? 0);
589
+ if (patternChar === "?" || patternChar === valueChar) {
590
+ patternIndex += 1;
591
+ valueIndex += 1;
592
+ continue;
593
+ }
594
+ if (starIndex !== -1) {
595
+ patternIndex = starIndex + 1;
596
+ backtrackValueIndex += 1;
597
+ valueIndex = backtrackValueIndex;
598
+ continue;
599
+ }
600
+ return false;
601
+ }
602
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
603
+ patternIndex += 1;
604
+ }
605
+ return patternIndex === pattern.length;
496
606
  }
497
- return result;
498
- }
499
- function addMap(base, delta) {
500
- const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
501
- const result = {};
502
- for (const key of keys) {
503
- result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
607
+ };
608
+
609
+ // ../../src/internal/CacheKeyDiscovery.ts
610
+ var CacheKeyDiscovery = class {
611
+ constructor(options) {
612
+ this.options = options;
613
+ }
614
+ options;
615
+ async collectKeysWithPrefix(prefix, maxMatches = false) {
616
+ const { tagIndex } = this.options;
617
+ const matches = /* @__PURE__ */ new Set();
618
+ if (tagIndex.forEachKeyForPrefix) {
619
+ await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
620
+ matches.add(key);
621
+ this.assertWithinMatchLimit(matches, maxMatches);
622
+ });
623
+ } else {
624
+ const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
625
+ for (const key of initialMatches) {
626
+ matches.add(key);
627
+ this.assertWithinMatchLimit(matches, maxMatches);
628
+ }
629
+ }
630
+ await Promise.all(
631
+ this.options.layers.map(async (layer) => {
632
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
633
+ return;
634
+ }
635
+ try {
636
+ if (layer.forEachKey) {
637
+ await layer.forEachKey(async (key) => {
638
+ if (key.startsWith(prefix)) {
639
+ matches.add(key);
640
+ this.assertWithinMatchLimit(matches, maxMatches);
641
+ }
642
+ });
643
+ return;
644
+ }
645
+ const keys = await layer.keys?.();
646
+ for (const key of keys ?? []) {
647
+ if (key.startsWith(prefix)) {
648
+ matches.add(key);
649
+ this.assertWithinMatchLimit(matches, maxMatches);
650
+ }
651
+ }
652
+ } catch (error) {
653
+ await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
654
+ }
655
+ })
656
+ );
657
+ return [...matches];
658
+ }
659
+ async collectKeysMatchingPattern(pattern, maxMatches = false) {
660
+ const matches = /* @__PURE__ */ new Set();
661
+ if (this.options.tagIndex.forEachKeyMatchingPattern) {
662
+ await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
663
+ matches.add(key);
664
+ this.assertWithinMatchLimit(matches, maxMatches);
665
+ });
666
+ } else {
667
+ for (const key of await this.options.tagIndex.matchPattern(pattern)) {
668
+ matches.add(key);
669
+ this.assertWithinMatchLimit(matches, maxMatches);
670
+ }
671
+ }
672
+ await Promise.all(
673
+ this.options.layers.map(async (layer) => {
674
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
675
+ return;
676
+ }
677
+ try {
678
+ if (layer.forEachKey) {
679
+ await layer.forEachKey(async (key) => {
680
+ if (PatternMatcher.matches(pattern, key)) {
681
+ matches.add(key);
682
+ this.assertWithinMatchLimit(matches, maxMatches);
683
+ }
684
+ });
685
+ return;
686
+ }
687
+ const keys = await layer.keys?.();
688
+ for (const key of keys ?? []) {
689
+ if (PatternMatcher.matches(pattern, key)) {
690
+ matches.add(key);
691
+ this.assertWithinMatchLimit(matches, maxMatches);
692
+ }
693
+ }
694
+ } catch (error) {
695
+ await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
696
+ }
697
+ })
698
+ );
699
+ return [...matches];
700
+ }
701
+ assertWithinMatchLimit(matches, maxMatches) {
702
+ if (maxMatches !== false && matches.size > maxMatches) {
703
+ throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
704
+ }
705
+ }
706
+ };
707
+
708
+ // ../../src/internal/CacheKeySerialization.ts
709
+ var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
710
+ function normalizeForSerialization(value) {
711
+ if (Array.isArray(value)) {
712
+ return value.map((entry) => normalizeForSerialization(entry));
713
+ }
714
+ if (value && typeof value === "object") {
715
+ return Object.keys(value).sort().reduce((normalized, key) => {
716
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
717
+ return normalized;
718
+ }
719
+ normalized[key] = normalizeForSerialization(value[key]);
720
+ return normalized;
721
+ }, {});
722
+ }
723
+ return value;
724
+ }
725
+ function serializeKeyPart(value) {
726
+ if (typeof value === "string") {
727
+ return `s:${value}`;
728
+ }
729
+ if (typeof value === "number") {
730
+ return `n:${value}`;
731
+ }
732
+ if (typeof value === "boolean") {
733
+ return `b:${value}`;
734
+ }
735
+ return `j:${JSON.stringify(normalizeForSerialization(value))}`;
736
+ }
737
+ function serializeOptions(options) {
738
+ return JSON.stringify(normalizeForSerialization(options) ?? null);
739
+ }
740
+ function createInstanceId() {
741
+ if (globalThis.crypto?.randomUUID) {
742
+ return globalThis.crypto.randomUUID();
743
+ }
744
+ const bytes = new Uint8Array(16);
745
+ if (globalThis.crypto?.getRandomValues) {
746
+ globalThis.crypto.getRandomValues(bytes);
747
+ } else {
748
+ for (let i = 0; i < bytes.length; i += 1) {
749
+ bytes[i] = Math.floor(Math.random() * 256);
750
+ }
751
+ }
752
+ return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
753
+ }
754
+
755
+ // ../../src/internal/CacheSnapshotFile.ts
756
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
757
+ const relative = path.relative(realBaseDir, candidatePath);
758
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
759
+ }
760
+ async function findExistingAncestor(directory, fs, path) {
761
+ let current = directory;
762
+ while (true) {
763
+ try {
764
+ await fs.lstat(current);
765
+ return current;
766
+ } catch (error) {
767
+ if (error.code !== "ENOENT") {
768
+ throw error;
769
+ }
770
+ }
771
+ const parent = path.dirname(current);
772
+ if (parent === current) {
773
+ return current;
774
+ }
775
+ current = parent;
776
+ }
777
+ }
778
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
779
+ if (filePath.length === 0) {
780
+ throw new Error("filePath must not be empty.");
781
+ }
782
+ if (filePath.includes("\0")) {
783
+ throw new Error("filePath must not contain null bytes.");
784
+ }
785
+ const { promises: fs } = await import("fs");
786
+ const path = await import("path");
787
+ const resolved = path.resolve(filePath);
788
+ const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
789
+ if (baseDir === false) {
790
+ return resolved;
791
+ }
792
+ await fs.mkdir(baseDir, { recursive: true });
793
+ const realBaseDir = await fs.realpath(baseDir);
794
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
795
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
796
+ }
797
+ if (mode === "read") {
798
+ const realTarget = await fs.realpath(resolved);
799
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
800
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
801
+ }
802
+ return realTarget;
803
+ }
804
+ const parentDir = path.dirname(resolved);
805
+ const existingAncestor = await findExistingAncestor(parentDir, fs, path);
806
+ const realExistingAncestor = await fs.realpath(existingAncestor);
807
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
808
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
809
+ }
810
+ await fs.mkdir(parentDir, { recursive: true });
811
+ const realParentDir = await fs.realpath(parentDir);
812
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
813
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
814
+ }
815
+ const targetPath = path.join(realParentDir, path.basename(resolved));
816
+ try {
817
+ const existing = await fs.lstat(targetPath);
818
+ if (existing.isSymbolicLink()) {
819
+ throw new Error("filePath must not point to a symbolic link.");
820
+ }
821
+ } catch (error) {
822
+ if (error.code !== "ENOENT") {
823
+ throw error;
824
+ }
825
+ }
826
+ return targetPath;
827
+ }
828
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
829
+ if (byteLimit === false) {
830
+ return handle.readFile({ encoding: "utf8" });
831
+ }
832
+ const chunks = [];
833
+ let totalBytes = 0;
834
+ let position = 0;
835
+ while (true) {
836
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
837
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
838
+ if (bytesRead === 0) {
839
+ break;
840
+ }
841
+ totalBytes += bytesRead;
842
+ if (totalBytes > byteLimit) {
843
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
844
+ }
845
+ chunks.push(buffer.subarray(0, bytesRead));
846
+ position += bytesRead;
847
+ }
848
+ return Buffer.concat(chunks).toString("utf8");
849
+ }
850
+
851
+ // ../../src/internal/CacheStackGeneration.ts
852
+ var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
853
+ function generationPrefix(generation) {
854
+ return generation === void 0 ? "" : `v${generation}:`;
855
+ }
856
+ function qualifyGenerationKey(key, generation) {
857
+ const prefix = generationPrefix(generation);
858
+ return prefix ? `${prefix}${key}` : key;
859
+ }
860
+ function qualifyGenerationPattern(pattern, generation) {
861
+ return qualifyGenerationKey(pattern, generation);
862
+ }
863
+ function stripGenerationPrefix(key, generation) {
864
+ const prefix = generationPrefix(generation);
865
+ if (!prefix || !key.startsWith(prefix)) {
866
+ return key;
867
+ }
868
+ return key.slice(prefix.length);
869
+ }
870
+ function resolveGenerationCleanupTarget({
871
+ previousGeneration,
872
+ nextGeneration,
873
+ generationCleanup
874
+ }) {
875
+ if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
876
+ return null;
877
+ }
878
+ return previousGeneration;
879
+ }
880
+ function resolveGenerationCleanupBatchSize(generationCleanup) {
881
+ if (typeof generationCleanup !== "object" || generationCleanup === null) {
882
+ return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
883
+ }
884
+ return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
885
+ }
886
+ function planGenerationCleanupBatches(keys, generationCleanup) {
887
+ if (keys.length === 0) {
888
+ return [];
889
+ }
890
+ const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
891
+ const batches = [];
892
+ for (let index = 0; index < keys.length; index += batchSize) {
893
+ batches.push(keys.slice(index, index + batchSize));
894
+ }
895
+ return batches;
896
+ }
897
+
898
+ // ../../src/internal/CacheStackMaintenance.ts
899
+ var CacheStackMaintenance = class {
900
+ keyEpochs = /* @__PURE__ */ new Map();
901
+ writeBehindQueue = [];
902
+ writeBehindTimer;
903
+ writeBehindFlushPromise;
904
+ generationCleanupPromise;
905
+ clearEpoch = 0;
906
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
907
+ if (writeStrategy !== "write-behind") {
908
+ return;
909
+ }
910
+ const flushIntervalMs = options?.flushIntervalMs;
911
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
912
+ return;
913
+ }
914
+ this.disposeWriteBehindTimer();
915
+ this.writeBehindTimer = setInterval(() => {
916
+ void flush();
917
+ }, flushIntervalMs);
918
+ this.writeBehindTimer.unref?.();
919
+ }
920
+ disposeWriteBehindTimer() {
921
+ if (!this.writeBehindTimer) {
922
+ return;
923
+ }
924
+ clearInterval(this.writeBehindTimer);
925
+ this.writeBehindTimer = void 0;
926
+ }
927
+ beginClearEpoch() {
928
+ this.clearEpoch += 1;
929
+ this.keyEpochs.clear();
930
+ this.writeBehindQueue.length = 0;
931
+ }
932
+ currentClearEpoch() {
933
+ return this.clearEpoch;
934
+ }
935
+ currentKeyEpoch(key) {
936
+ return this.keyEpochs.get(key) ?? 0;
937
+ }
938
+ bumpKeyEpochs(keys) {
939
+ for (const key of keys) {
940
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
941
+ }
942
+ }
943
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
944
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
945
+ return true;
946
+ }
947
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
948
+ return true;
949
+ }
950
+ return false;
951
+ }
952
+ async enqueueWriteBehind(operation, options, flushBatch) {
953
+ this.writeBehindQueue.push(operation);
954
+ const batchSize = options?.batchSize ?? 100;
955
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
956
+ if (this.writeBehindQueue.length >= batchSize) {
957
+ await this.flushWriteBehindQueue(options, flushBatch);
958
+ return;
959
+ }
960
+ if (this.writeBehindQueue.length >= maxQueueSize) {
961
+ await this.flushWriteBehindQueue(options, flushBatch);
962
+ }
963
+ }
964
+ async flushWriteBehindQueue(options, flushBatch) {
965
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
966
+ await this.writeBehindFlushPromise;
967
+ return;
968
+ }
969
+ const batchSize = options?.batchSize ?? 100;
970
+ const batch = this.writeBehindQueue.splice(0, batchSize);
971
+ this.writeBehindFlushPromise = flushBatch(batch);
972
+ try {
973
+ await this.writeBehindFlushPromise;
974
+ } finally {
975
+ this.writeBehindFlushPromise = void 0;
976
+ }
977
+ if (this.writeBehindQueue.length > 0) {
978
+ await this.flushWriteBehindQueue(options, flushBatch);
979
+ }
980
+ }
981
+ scheduleGenerationCleanup(generation, task, onError) {
982
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
983
+ onError(generation, error);
984
+ });
985
+ this.generationCleanupPromise = scheduledTask.finally(() => {
986
+ if (this.generationCleanupPromise === scheduledTask) {
987
+ this.generationCleanupPromise = void 0;
988
+ }
989
+ });
990
+ }
991
+ async waitForGenerationCleanup() {
992
+ await this.generationCleanupPromise;
993
+ }
994
+ };
995
+
996
+ // ../../src/internal/StoredValue.ts
997
+ function isStoredValueEnvelope(value) {
998
+ if (typeof value !== "object" || value === null) {
999
+ return false;
1000
+ }
1001
+ const v = value;
1002
+ if (v.__layercache !== 1) {
1003
+ return false;
1004
+ }
1005
+ if (v.kind !== "value" && v.kind !== "empty") {
1006
+ return false;
1007
+ }
1008
+ if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
1009
+ return false;
1010
+ }
1011
+ if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
1012
+ return false;
1013
+ }
1014
+ if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
1015
+ return false;
1016
+ }
1017
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
1018
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
1019
+ return false;
1020
+ }
1021
+ if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
1022
+ return false;
1023
+ }
1024
+ if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
1025
+ return false;
1026
+ }
1027
+ if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
1028
+ return false;
1029
+ }
1030
+ if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
1031
+ return false;
1032
+ }
1033
+ if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
1034
+ return false;
1035
+ }
1036
+ const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1037
+ if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1038
+ return false;
1039
+ }
1040
+ if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1041
+ return false;
1042
+ }
1043
+ if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1044
+ return false;
1045
+ }
1046
+ if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1047
+ return false;
1048
+ }
1049
+ return true;
1050
+ }
1051
+ function createStoredValueEnvelope(options) {
1052
+ const now = options.now ?? Date.now();
1053
+ const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
1054
+ const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
1055
+ const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
1056
+ const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
1057
+ const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
1058
+ const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
1059
+ return {
1060
+ __layercache: 1,
1061
+ kind: options.kind,
1062
+ value: options.value,
1063
+ freshUntil,
1064
+ staleUntil,
1065
+ errorUntil,
1066
+ freshTtlSeconds: freshTtlSeconds ?? null,
1067
+ staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
1068
+ staleIfErrorSeconds: staleIfErrorSeconds ?? null
1069
+ };
1070
+ }
1071
+ function resolveStoredValue(stored, now = Date.now()) {
1072
+ if (!isStoredValueEnvelope(stored)) {
1073
+ return { state: "fresh", value: stored, stored };
1074
+ }
1075
+ if (stored.freshUntil === null || stored.freshUntil > now) {
1076
+ return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
1077
+ }
1078
+ if (stored.staleUntil !== null && stored.staleUntil > now) {
1079
+ return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
1080
+ }
1081
+ if (stored.errorUntil !== null && stored.errorUntil > now) {
1082
+ return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
1083
+ }
1084
+ return { state: "expired", value: null, stored, envelope: stored };
1085
+ }
1086
+ function unwrapStoredValue(stored) {
1087
+ if (!isStoredValueEnvelope(stored)) {
1088
+ return stored;
1089
+ }
1090
+ if (stored.kind === "empty") {
1091
+ return null;
1092
+ }
1093
+ return stored.value ?? null;
1094
+ }
1095
+ function remainingStoredTtlSeconds(stored, now = Date.now()) {
1096
+ if (!isStoredValueEnvelope(stored)) {
1097
+ return void 0;
1098
+ }
1099
+ const expiry = maxExpiry(stored);
1100
+ if (expiry === null) {
1101
+ return void 0;
1102
+ }
1103
+ const remainingMs = expiry - now;
1104
+ if (remainingMs <= 0) {
1105
+ return 1;
1106
+ }
1107
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
1108
+ }
1109
+ function remainingFreshTtlSeconds(stored, now = Date.now()) {
1110
+ if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
1111
+ return void 0;
1112
+ }
1113
+ const remainingMs = stored.freshUntil - now;
1114
+ if (remainingMs <= 0) {
1115
+ return 0;
1116
+ }
1117
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
1118
+ }
1119
+ function refreshStoredEnvelope(stored, now = Date.now()) {
1120
+ if (!isStoredValueEnvelope(stored)) {
1121
+ return stored;
1122
+ }
1123
+ return createStoredValueEnvelope({
1124
+ kind: stored.kind,
1125
+ value: stored.value,
1126
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1127
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1128
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1129
+ now
1130
+ });
1131
+ }
1132
+ function maxExpiry(stored) {
1133
+ const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1134
+ (value) => value !== null
1135
+ );
1136
+ if (values.length === 0) {
1137
+ return null;
1138
+ }
1139
+ return Math.max(...values);
1140
+ }
1141
+ function normalizePositiveSeconds(value) {
1142
+ if (!value || value <= 0) {
1143
+ return void 0;
1144
+ }
1145
+ return value;
1146
+ }
1147
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1148
+ if (value == null) {
1149
+ return true;
1150
+ }
1151
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1152
+ }
1153
+
1154
+ // ../../src/internal/CacheStackRuntimePolicy.ts
1155
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
1156
+ return degradedUntil !== void 0 && degradedUntil > now;
1157
+ }
1158
+ function shouldStartBackgroundRefresh({
1159
+ isDisconnecting,
1160
+ hasRefreshInFlight
1161
+ }) {
1162
+ return !isDisconnecting && !hasRefreshInFlight;
1163
+ }
1164
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1165
+ if (!gracefulDegradation) {
1166
+ return { degrade: false };
1167
+ }
1168
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1169
+ return {
1170
+ degrade: true,
1171
+ degradedUntil: now + retryAfterMs
1172
+ };
1173
+ }
1174
+ function planFreshReadPolicies({
1175
+ stored,
1176
+ hasFetcher,
1177
+ slidingTtl,
1178
+ refreshAheadSeconds
1179
+ }) {
1180
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1181
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1182
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1183
+ return {
1184
+ refreshedStored,
1185
+ refreshedStoredTtl,
1186
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1187
+ };
1188
+ }
1189
+
1190
+ // ../../src/internal/CacheStackValidation.ts
1191
+ var MAX_CACHE_KEY_LENGTH = 1024;
1192
+ var MAX_PATTERN_LENGTH = 1024;
1193
+ var MAX_TAGS_PER_OPERATION = 128;
1194
+ function validatePositiveNumber(name, value) {
1195
+ if (value === void 0) {
1196
+ return;
1197
+ }
1198
+ if (!Number.isFinite(value) || value <= 0) {
1199
+ throw new Error(`${name} must be a positive finite number.`);
1200
+ }
1201
+ }
1202
+ function validateNonNegativeNumber(name, value) {
1203
+ if (!Number.isFinite(value) || value < 0) {
1204
+ throw new Error(`${name} must be a non-negative finite number.`);
1205
+ }
1206
+ }
1207
+ function validateLayerNumberOption(name, value) {
1208
+ if (value === void 0) {
1209
+ return;
1210
+ }
1211
+ if (typeof value === "number") {
1212
+ validateNonNegativeNumber(name, value);
1213
+ return;
1214
+ }
1215
+ for (const [layerName, layerValue] of Object.entries(value)) {
1216
+ if (layerValue === void 0) {
1217
+ continue;
1218
+ }
1219
+ validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
1220
+ }
1221
+ }
1222
+ function validateRateLimitOptions(name, options) {
1223
+ if (!options) {
1224
+ return;
1225
+ }
1226
+ validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
1227
+ validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
1228
+ validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
1229
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
1230
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
1231
+ }
1232
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
1233
+ throw new Error(`${name}.bucketKey must not be empty.`);
504
1234
  }
505
- return result;
506
1235
  }
507
- function validateNamespaceKey(key) {
1236
+ function validateCacheKey(key) {
508
1237
  if (key.length === 0) {
509
- throw new Error("Namespace prefix must not be empty.");
1238
+ throw new Error("Cache key must not be empty.");
510
1239
  }
511
- if (key.length > 256) {
512
- throw new Error("Namespace prefix must be at most 256 characters.");
1240
+ if (key.length > MAX_CACHE_KEY_LENGTH) {
1241
+ throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
513
1242
  }
514
1243
  if (/[\u0000-\u001F\u007F]/.test(key)) {
515
- throw new Error("Namespace prefix contains unsupported control characters.");
1244
+ throw new Error("Cache key contains unsupported control characters.");
1245
+ }
1246
+ if (/[\uD800-\uDFFF]/.test(key)) {
1247
+ throw new Error("Cache key contains unsupported surrogate code points.");
516
1248
  }
1249
+ return key;
517
1250
  }
518
-
519
- // ../../src/invalidation/PatternMatcher.ts
520
- var PatternMatcher = class _PatternMatcher {
521
- /**
522
- * Tests whether a glob-style pattern matches a value.
523
- * Supports `*` (any sequence of characters) and `?` (any single character).
524
- * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
525
- * quadratic memory usage on long patterns/keys.
526
- */
527
- static matches(pattern, value) {
528
- return _PatternMatcher.matchLinear(pattern, value);
1251
+ function validateTag(tag) {
1252
+ if (tag.length === 0) {
1253
+ throw new Error("Cache tag must not be empty.");
529
1254
  }
530
- /**
531
- * Linear-time glob matching with O(1) extra memory.
532
- */
533
- static matchLinear(pattern, value) {
534
- let patternIndex = 0;
535
- let valueIndex = 0;
536
- let starIndex = -1;
537
- let backtrackValueIndex = 0;
538
- while (valueIndex < value.length) {
539
- const patternChar = pattern[patternIndex];
540
- const valueChar = value[valueIndex];
541
- if (patternChar === "*" && patternIndex < pattern.length) {
542
- starIndex = patternIndex;
543
- patternIndex += 1;
544
- backtrackValueIndex = valueIndex;
545
- continue;
546
- }
547
- if (patternChar === "?" || patternChar === valueChar) {
548
- patternIndex += 1;
549
- valueIndex += 1;
550
- continue;
551
- }
552
- if (starIndex !== -1) {
553
- patternIndex = starIndex + 1;
554
- backtrackValueIndex += 1;
555
- valueIndex = backtrackValueIndex;
556
- continue;
557
- }
558
- return false;
559
- }
560
- while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
561
- patternIndex += 1;
562
- }
563
- return patternIndex === pattern.length;
1255
+ if (tag.length > MAX_CACHE_KEY_LENGTH) {
1256
+ throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
564
1257
  }
565
- };
566
-
567
- // ../../src/internal/CacheKeyDiscovery.ts
568
- var CacheKeyDiscovery = class {
569
- constructor(options) {
570
- this.options = options;
1258
+ if (/[\u0000-\u001F\u007F]/.test(tag)) {
1259
+ throw new Error("Cache tag contains unsupported control characters.");
571
1260
  }
572
- options;
573
- async collectKeysWithPrefix(prefix) {
574
- const { tagIndex } = this.options;
575
- const matches = new Set(
576
- tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
577
- );
578
- await Promise.all(
579
- this.options.layers.map(async (layer) => {
580
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
581
- return;
582
- }
583
- try {
584
- const keys = await layer.keys();
585
- for (const key of keys) {
586
- if (key.startsWith(prefix)) {
587
- matches.add(key);
588
- }
589
- }
590
- } catch (error) {
591
- await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
592
- }
593
- })
594
- );
595
- return [...matches];
1261
+ if (/[\uD800-\uDFFF]/.test(tag)) {
1262
+ throw new Error("Cache tag contains unsupported surrogate code points.");
596
1263
  }
597
- async collectKeysMatchingPattern(pattern) {
598
- const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
599
- await Promise.all(
600
- this.options.layers.map(async (layer) => {
601
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
602
- return;
603
- }
604
- try {
605
- const keys = await layer.keys();
606
- for (const key of keys) {
607
- if (PatternMatcher.matches(pattern, key)) {
608
- matches.add(key);
609
- }
610
- }
611
- } catch (error) {
612
- await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
613
- }
614
- })
615
- );
616
- return [...matches];
1264
+ return tag;
1265
+ }
1266
+ function validateTags(tags) {
1267
+ if (!tags) {
1268
+ return;
617
1269
  }
618
- };
1270
+ if (tags.length > MAX_TAGS_PER_OPERATION) {
1271
+ throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
1272
+ }
1273
+ for (const tag of tags) {
1274
+ validateTag(tag);
1275
+ }
1276
+ }
1277
+ function validatePattern(pattern) {
1278
+ if (pattern.length === 0) {
1279
+ throw new Error("Pattern must not be empty.");
1280
+ }
1281
+ if (pattern.length > MAX_PATTERN_LENGTH) {
1282
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
1283
+ }
1284
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
1285
+ throw new Error("Pattern contains unsupported control characters.");
1286
+ }
1287
+ }
1288
+ function validateTtlPolicy(name, policy) {
1289
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
1290
+ return;
1291
+ }
1292
+ if ("alignTo" in policy) {
1293
+ validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
1294
+ return;
1295
+ }
1296
+ throw new Error(`${name} is invalid.`);
1297
+ }
1298
+ function validateAdaptiveTtlOptions(options) {
1299
+ if (!options || options === true) {
1300
+ return;
1301
+ }
1302
+ validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
1303
+ validateLayerNumberOption("adaptiveTtl.step", options.step);
1304
+ validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
1305
+ }
1306
+ function validateCircuitBreakerOptions(options) {
1307
+ if (!options) {
1308
+ return;
1309
+ }
1310
+ validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
1311
+ validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
1312
+ }
619
1313
 
620
1314
  // ../../src/internal/CircuitBreakerManager.ts
621
1315
  var CircuitBreakerManager = class {
@@ -646,7 +1340,6 @@ var CircuitBreakerManager = class {
646
1340
  if (!options) {
647
1341
  return;
648
1342
  }
649
- this.pruneIfNeeded();
650
1343
  const failureThreshold = options.failureThreshold ?? 3;
651
1344
  const cooldownMs = options.cooldownMs ?? 3e4;
652
1345
  const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
@@ -655,6 +1348,7 @@ var CircuitBreakerManager = class {
655
1348
  state.openUntil = Date.now() + cooldownMs;
656
1349
  }
657
1350
  this.breakers.set(key, state);
1351
+ this.pruneIfNeeded();
658
1352
  }
659
1353
  recordSuccess(key) {
660
1354
  this.breakers.delete(key);
@@ -969,158 +1663,34 @@ var MetricsCollector = class {
969
1663
  for (const layer of allLayers) {
970
1664
  const h = this.data.hitsByLayer[layer] ?? 0;
971
1665
  const m = this.data.missesByLayer[layer] ?? 0;
972
- byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
973
- }
974
- return { overall, byLayer };
975
- }
976
- empty() {
977
- return {
978
- hits: 0,
979
- misses: 0,
980
- fetches: 0,
981
- sets: 0,
982
- deletes: 0,
983
- backfills: 0,
984
- invalidations: 0,
985
- staleHits: 0,
986
- refreshes: 0,
987
- refreshErrors: 0,
988
- writeFailures: 0,
989
- singleFlightWaits: 0,
990
- negativeCacheHits: 0,
991
- circuitBreakerTrips: 0,
992
- degradedOperations: 0,
993
- hitsByLayer: {},
994
- missesByLayer: {},
995
- latencyByLayer: {},
996
- resetAt: Date.now()
997
- };
998
- }
999
- };
1000
-
1001
- // ../../src/internal/StoredValue.ts
1002
- function isStoredValueEnvelope(value) {
1003
- if (typeof value !== "object" || value === null) {
1004
- return false;
1005
- }
1006
- const v = value;
1007
- if (v.__layercache !== 1) {
1008
- return false;
1009
- }
1010
- if (v.kind !== "value" && v.kind !== "empty") {
1011
- return false;
1012
- }
1013
- if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
1014
- return false;
1015
- }
1016
- if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
1017
- return false;
1018
- }
1019
- if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
1020
- return false;
1021
- }
1022
- const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
1023
- if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
1024
- return false;
1025
- }
1026
- return true;
1027
- }
1028
- function createStoredValueEnvelope(options) {
1029
- const now = options.now ?? Date.now();
1030
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
1031
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
1032
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
1033
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
1034
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
1035
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
1036
- return {
1037
- __layercache: 1,
1038
- kind: options.kind,
1039
- value: options.value,
1040
- freshUntil,
1041
- staleUntil,
1042
- errorUntil,
1043
- freshTtlSeconds: freshTtlSeconds ?? null,
1044
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
1045
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
1046
- };
1047
- }
1048
- function resolveStoredValue(stored, now = Date.now()) {
1049
- if (!isStoredValueEnvelope(stored)) {
1050
- return { state: "fresh", value: stored, stored };
1051
- }
1052
- if (stored.freshUntil === null || stored.freshUntil > now) {
1053
- return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
1054
- }
1055
- if (stored.staleUntil !== null && stored.staleUntil > now) {
1056
- return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
1057
- }
1058
- if (stored.errorUntil !== null && stored.errorUntil > now) {
1059
- return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
1060
- }
1061
- return { state: "expired", value: null, stored, envelope: stored };
1062
- }
1063
- function unwrapStoredValue(stored) {
1064
- if (!isStoredValueEnvelope(stored)) {
1065
- return stored;
1066
- }
1067
- if (stored.kind === "empty") {
1068
- return null;
1069
- }
1070
- return stored.value ?? null;
1071
- }
1072
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
1073
- if (!isStoredValueEnvelope(stored)) {
1074
- return void 0;
1075
- }
1076
- const expiry = maxExpiry(stored);
1077
- if (expiry === null) {
1078
- return void 0;
1079
- }
1080
- const remainingMs = expiry - now;
1081
- if (remainingMs <= 0) {
1082
- return 1;
1083
- }
1084
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1085
- }
1086
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
1087
- if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
1088
- return void 0;
1089
- }
1090
- const remainingMs = stored.freshUntil - now;
1091
- if (remainingMs <= 0) {
1092
- return 0;
1093
- }
1094
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1095
- }
1096
- function refreshStoredEnvelope(stored, now = Date.now()) {
1097
- if (!isStoredValueEnvelope(stored)) {
1098
- return stored;
1099
- }
1100
- return createStoredValueEnvelope({
1101
- kind: stored.kind,
1102
- value: stored.value,
1103
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1104
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1105
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1106
- now
1107
- });
1108
- }
1109
- function maxExpiry(stored) {
1110
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1111
- (value) => value !== null
1112
- );
1113
- if (values.length === 0) {
1114
- return null;
1115
- }
1116
- return Math.max(...values);
1117
- }
1118
- function normalizePositiveSeconds(value) {
1119
- if (!value || value <= 0) {
1120
- return void 0;
1666
+ byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
1667
+ }
1668
+ return { overall, byLayer };
1121
1669
  }
1122
- return value;
1123
- }
1670
+ empty() {
1671
+ return {
1672
+ hits: 0,
1673
+ misses: 0,
1674
+ fetches: 0,
1675
+ sets: 0,
1676
+ deletes: 0,
1677
+ backfills: 0,
1678
+ invalidations: 0,
1679
+ staleHits: 0,
1680
+ refreshes: 0,
1681
+ refreshErrors: 0,
1682
+ writeFailures: 0,
1683
+ singleFlightWaits: 0,
1684
+ negativeCacheHits: 0,
1685
+ circuitBreakerTrips: 0,
1686
+ degradedOperations: 0,
1687
+ hitsByLayer: {},
1688
+ missesByLayer: {},
1689
+ latencyByLayer: {},
1690
+ resetAt: Date.now()
1691
+ };
1692
+ }
1693
+ };
1124
1694
 
1125
1695
  // ../../src/internal/TtlResolver.ts
1126
1696
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
@@ -1275,6 +1845,11 @@ var TagIndex = class {
1275
1845
  async keysForTag(tag) {
1276
1846
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
1277
1847
  }
1848
+ async forEachKeyForTag(tag, visitor) {
1849
+ for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
1850
+ await visitor(key);
1851
+ }
1852
+ }
1278
1853
  async keysForPrefix(prefix) {
1279
1854
  const node = this.findNode(prefix);
1280
1855
  if (!node) {
@@ -1284,6 +1859,13 @@ var TagIndex = class {
1284
1859
  this.collectFromNode(node, prefix, matches);
1285
1860
  return matches;
1286
1861
  }
1862
+ async forEachKeyForPrefix(prefix, visitor) {
1863
+ const node = this.findNode(prefix);
1864
+ if (!node) {
1865
+ return;
1866
+ }
1867
+ await this.visitFromNode(node, prefix, visitor);
1868
+ }
1287
1869
  async tagsForKey(key) {
1288
1870
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
1289
1871
  }
@@ -1292,6 +1874,12 @@ var TagIndex = class {
1292
1874
  this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1293
1875
  return [...matches];
1294
1876
  }
1877
+ async forEachKeyMatchingPattern(pattern, visitor) {
1878
+ const matches = await this.matchPattern(pattern);
1879
+ for (const key of matches) {
1880
+ await visitor(key);
1881
+ }
1882
+ }
1295
1883
  async clear() {
1296
1884
  this.tagToKeys.clear();
1297
1885
  this.keyToTags.clear();
@@ -1341,6 +1929,14 @@ var TagIndex = class {
1341
1929
  this.collectFromNode(child, `${prefix}${character}`, matches);
1342
1930
  }
1343
1931
  }
1932
+ async visitFromNode(node, prefix, visitor) {
1933
+ if (node.terminal) {
1934
+ await visitor(prefix);
1935
+ }
1936
+ for (const [character, child] of node.children) {
1937
+ await this.visitFromNode(child, `${prefix}${character}`, visitor);
1938
+ }
1939
+ }
1344
1940
  collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
1345
1941
  if (depth > MAX_PATTERN_RECURSION_DEPTH) {
1346
1942
  return;
@@ -1458,22 +2054,27 @@ var TagIndex = class {
1458
2054
 
1459
2055
  // ../../src/serialization/JsonSerializer.ts
1460
2056
  var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2057
+ var MAX_SANITIZE_NODES = 1e4;
1461
2058
  var JsonSerializer = class {
1462
2059
  serialize(value) {
1463
2060
  return JSON.stringify(value);
1464
2061
  }
1465
2062
  deserialize(payload) {
1466
2063
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1467
- return sanitizeJsonValue(JSON.parse(normalized), 0);
2064
+ return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
1468
2065
  }
1469
2066
  };
1470
2067
  var MAX_SANITIZE_DEPTH = 200;
1471
- function sanitizeJsonValue(value, depth) {
2068
+ function sanitizeJsonValue(value, depth, state) {
2069
+ state.count += 1;
2070
+ if (state.count > MAX_SANITIZE_NODES) {
2071
+ throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
2072
+ }
1472
2073
  if (depth > MAX_SANITIZE_DEPTH) {
1473
- return value;
2074
+ throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
1474
2075
  }
1475
2076
  if (Array.isArray(value)) {
1476
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
2077
+ return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
1477
2078
  }
1478
2079
  if (!isPlainObject(value)) {
1479
2080
  return value;
@@ -1483,7 +2084,7 @@ function sanitizeJsonValue(value, depth) {
1483
2084
  if (DANGEROUS_JSON_KEYS.has(key)) {
1484
2085
  continue;
1485
2086
  }
1486
- sanitized[key] = sanitizeJsonValue(entry, depth + 1);
2087
+ sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
1487
2088
  }
1488
2089
  return sanitized;
1489
2090
  }
@@ -1532,10 +2133,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1532
2133
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1533
2134
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1534
2135
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1535
- var MAX_CACHE_KEY_LENGTH = 1024;
1536
- var MAX_PATTERN_LENGTH = 1024;
2136
+ var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
2137
+ var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
2138
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
2139
+ var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
1537
2140
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1538
- var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1539
2141
  var DebugLogger = class {
1540
2142
  enabled;
1541
2143
  constructor(enabled) {
@@ -1622,13 +2224,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1622
2224
  snapshotSerializer = new JsonSerializer();
1623
2225
  backgroundRefreshes = /* @__PURE__ */ new Map();
1624
2226
  layerDegradedUntil = /* @__PURE__ */ new Map();
2227
+ maintenance = new CacheStackMaintenance();
1625
2228
  ttlResolver;
1626
2229
  circuitBreakerManager;
1627
2230
  currentGeneration;
1628
- writeBehindQueue = [];
1629
- writeBehindTimer;
1630
- writeBehindFlushPromise;
1631
- generationCleanupPromise;
1632
2231
  isDisconnecting = false;
1633
2232
  disconnectPromise;
1634
2233
  /**
@@ -1638,7 +2237,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1638
2237
  * and no `fetcher` is provided.
1639
2238
  */
1640
2239
  async get(key, fetcher, options) {
1641
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2240
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1642
2241
  this.validateWriteOptions(options);
1643
2242
  await this.awaitStartup("get");
1644
2243
  return this.getPrepared(normalizedKey, fetcher, options);
@@ -1708,7 +2307,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1708
2307
  * Returns true if the given key exists and is not expired in any layer.
1709
2308
  */
1710
2309
  async has(key) {
1711
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2310
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1712
2311
  await this.awaitStartup("has");
1713
2312
  for (const layer of this.layers) {
1714
2313
  if (this.shouldSkipLayer(layer)) {
@@ -1741,7 +2340,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1741
2340
  * that has it, or null if the key is not found / has no TTL.
1742
2341
  */
1743
2342
  async ttl(key) {
1744
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2343
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1745
2344
  await this.awaitStartup("ttl");
1746
2345
  for (const layer of this.layers) {
1747
2346
  if (this.shouldSkipLayer(layer)) {
@@ -1763,7 +2362,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1763
2362
  * Stores a value in all cache layers. Overwrites any existing value.
1764
2363
  */
1765
2364
  async set(key, value, options) {
1766
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2365
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1767
2366
  this.validateWriteOptions(options);
1768
2367
  await this.awaitStartup("set");
1769
2368
  await this.storeEntry(normalizedKey, "value", value, options);
@@ -1772,7 +2371,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1772
2371
  * Deletes the key from all layers and publishes an invalidation message.
1773
2372
  */
1774
2373
  async delete(key) {
1775
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2374
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1776
2375
  await this.awaitStartup("delete");
1777
2376
  await this.deleteKeys([normalizedKey]);
1778
2377
  await this.publishInvalidation({
@@ -1784,6 +2383,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1784
2383
  }
1785
2384
  async clear() {
1786
2385
  await this.awaitStartup("clear");
2386
+ this.maintenance.beginClearEpoch();
1787
2387
  await Promise.all(this.layers.map((layer) => layer.clear()));
1788
2388
  await this.tagIndex.clear();
1789
2389
  this.ttlResolver.clearProfiles();
@@ -1800,7 +2400,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1800
2400
  return;
1801
2401
  }
1802
2402
  await this.awaitStartup("mdelete");
1803
- const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
2403
+ const normalizedKeys = keys.map((k) => validateCacheKey(k));
1804
2404
  const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1805
2405
  await this.deleteKeys(cacheKeys);
1806
2406
  await this.publishInvalidation({
@@ -1817,7 +2417,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1817
2417
  }
1818
2418
  const normalizedEntries = entries.map((entry) => ({
1819
2419
  ...entry,
1820
- key: this.qualifyKey(this.validateCacheKey(entry.key))
2420
+ key: this.qualifyKey(validateCacheKey(entry.key))
1821
2421
  }));
1822
2422
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1823
2423
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1826,7 +2426,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1826
2426
  const pendingReads = /* @__PURE__ */ new Map();
1827
2427
  return Promise.all(
1828
2428
  normalizedEntries.map((entry) => {
1829
- const optionsSignature = this.serializeOptions(entry.options);
2429
+ const optionsSignature = serializeOptions(entry.options);
1830
2430
  const existing = pendingReads.get(entry.key);
1831
2431
  if (!existing) {
1832
2432
  const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
@@ -1895,7 +2495,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1895
2495
  this.assertActive("mset");
1896
2496
  const normalizedEntries = entries.map((entry) => ({
1897
2497
  ...entry,
1898
- key: this.qualifyKey(this.validateCacheKey(entry.key))
2498
+ key: this.qualifyKey(validateCacheKey(entry.key))
1899
2499
  }));
1900
2500
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1901
2501
  await this.awaitStartup("mset");
@@ -1938,7 +2538,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1938
2538
  */
1939
2539
  wrap(prefix, fetcher, options = {}) {
1940
2540
  return (...args) => {
1941
- const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
2541
+ const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
1942
2542
  const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
1943
2543
  return this.get(key, () => fetcher(...args), options);
1944
2544
  };
@@ -1948,11 +2548,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1948
2548
  * `prefix:`. Useful for multi-tenant or module-level isolation.
1949
2549
  */
1950
2550
  namespace(prefix) {
2551
+ validateNamespaceKey(prefix);
1951
2552
  return new CacheNamespace(this, prefix);
1952
2553
  }
1953
2554
  async invalidateByTag(tag) {
2555
+ validateTag(tag);
1954
2556
  await this.awaitStartup("invalidateByTag");
1955
- const keys = await this.tagIndex.keysForTag(tag);
2557
+ const keys = await this.collectKeysForTag(tag);
1956
2558
  await this.deleteKeys(keys);
1957
2559
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1958
2560
  }
@@ -1960,23 +2562,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1960
2562
  if (tags.length === 0) {
1961
2563
  return;
1962
2564
  }
2565
+ validateTags(tags);
1963
2566
  await this.awaitStartup("invalidateByTags");
1964
- const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
2567
+ const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
1965
2568
  const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2569
+ this.assertWithinInvalidationKeyLimit(keys.length);
1966
2570
  await this.deleteKeys(keys);
1967
2571
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1968
2572
  }
1969
2573
  async invalidateByPattern(pattern) {
1970
- this.validatePattern(pattern);
2574
+ validatePattern(pattern);
1971
2575
  await this.awaitStartup("invalidateByPattern");
1972
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
2576
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2577
+ this.qualifyPattern(pattern),
2578
+ this.invalidationMaxKeys()
2579
+ );
1973
2580
  await this.deleteKeys(keys);
1974
2581
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1975
2582
  }
1976
2583
  async invalidateByPrefix(prefix) {
1977
2584
  await this.awaitStartup("invalidateByPrefix");
1978
- const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1979
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
2585
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2586
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
1980
2587
  await this.deleteKeys(keys);
1981
2588
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1982
2589
  }
@@ -2034,9 +2641,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2034
2641
  bumpGeneration(nextGeneration) {
2035
2642
  const current = this.currentGeneration ?? 0;
2036
2643
  const previousGeneration = this.currentGeneration;
2037
- this.currentGeneration = nextGeneration ?? current + 1;
2038
- if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
2039
- this.scheduleGenerationCleanup(previousGeneration);
2644
+ const updatedGeneration = nextGeneration ?? current + 1;
2645
+ const generationToCleanup = resolveGenerationCleanupTarget({
2646
+ previousGeneration,
2647
+ nextGeneration: updatedGeneration,
2648
+ generationCleanup: this.options.generationCleanup
2649
+ });
2650
+ this.currentGeneration = updatedGeneration;
2651
+ if (generationToCleanup !== null) {
2652
+ this.scheduleGenerationCleanup(generationToCleanup);
2040
2653
  }
2041
2654
  return this.currentGeneration;
2042
2655
  }
@@ -2046,7 +2659,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2046
2659
  * Returns `null` if the key does not exist in any layer.
2047
2660
  */
2048
2661
  async inspect(key) {
2049
- const userKey = this.validateCacheKey(key);
2662
+ const userKey = validateCacheKey(key);
2050
2663
  const normalizedKey = this.qualifyKey(userKey);
2051
2664
  await this.awaitStartup("inspect");
2052
2665
  const foundInLayers = [];
@@ -2083,50 +2696,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
2083
2696
  }
2084
2697
  async exportState() {
2085
2698
  await this.awaitStartup("exportState");
2086
- const exported = /* @__PURE__ */ new Map();
2087
- for (const layer of this.layers) {
2088
- if (!layer.keys) {
2089
- continue;
2090
- }
2091
- const keys = await layer.keys();
2092
- for (const key of keys) {
2093
- const exportedKey = this.stripQualifiedKey(key);
2094
- if (exported.has(exportedKey)) {
2095
- continue;
2096
- }
2097
- const stored = await this.readLayerEntry(layer, key);
2098
- if (stored === null) {
2099
- continue;
2100
- }
2101
- exported.set(exportedKey, {
2102
- key: exportedKey,
2103
- value: stored,
2104
- ttl: remainingStoredTtlSeconds(stored)
2105
- });
2106
- }
2107
- }
2108
- return [...exported.values()];
2699
+ const entries = [];
2700
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2701
+ entries.push(entry);
2702
+ });
2703
+ return entries;
2109
2704
  }
2110
2705
  async importState(entries) {
2111
2706
  await this.awaitStartup("importState");
2112
- await Promise.all(
2113
- entries.map(async (entry) => {
2114
- const qualifiedKey = this.qualifyKey(entry.key);
2115
- await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
2116
- await this.tagIndex.touch(qualifiedKey);
2117
- })
2118
- );
2707
+ const normalizedEntries = entries.map((entry) => ({
2708
+ key: this.qualifyKey(validateCacheKey(entry.key)),
2709
+ value: entry.value,
2710
+ ttl: entry.ttl
2711
+ }));
2712
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
2713
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
2714
+ await Promise.all(
2715
+ batch.map(async (entry) => {
2716
+ await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
2717
+ await this.tagIndex.touch(entry.key);
2718
+ })
2719
+ );
2720
+ }
2119
2721
  }
2120
2722
  async persistToFile(filePath) {
2121
2723
  this.assertActive("persistToFile");
2122
- const snapshot = await this.exportState();
2123
2724
  const { promises: fs } = await import("fs");
2124
- await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
2725
+ const path = await import("path");
2726
+ const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
2727
+ const tempPath = path.join(
2728
+ path.dirname(targetPath),
2729
+ `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
2730
+ );
2731
+ let handle;
2732
+ try {
2733
+ handle = await fs.open(tempPath, "wx");
2734
+ const openedHandle = handle;
2735
+ await openedHandle.writeFile("[", "utf8");
2736
+ let wroteAny = false;
2737
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2738
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
2739
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
2740
+ wroteAny = true;
2741
+ });
2742
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
2743
+ await openedHandle.close();
2744
+ handle = void 0;
2745
+ await fs.rename(tempPath, targetPath);
2746
+ } catch (error) {
2747
+ await handle?.close().catch(() => void 0);
2748
+ await fs.unlink(tempPath).catch(() => void 0);
2749
+ throw error;
2750
+ }
2125
2751
  }
2126
2752
  async restoreFromFile(filePath) {
2127
2753
  this.assertActive("restoreFromFile");
2128
- const { promises: fs } = await import("fs");
2129
- const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
2754
+ const { promises: fs, constants } = await import("fs");
2755
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
2756
+ const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2757
+ const snapshotMaxBytes = this.snapshotMaxBytes();
2758
+ let raw;
2759
+ try {
2760
+ if (snapshotMaxBytes !== false) {
2761
+ const stat = await handle.stat();
2762
+ if (stat.size > snapshotMaxBytes) {
2763
+ throw new Error(
2764
+ `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
2765
+ );
2766
+ }
2767
+ }
2768
+ raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
2769
+ } finally {
2770
+ await handle.close();
2771
+ }
2130
2772
  let parsed;
2131
2773
  try {
2132
2774
  parsed = JSON.parse(raw);
@@ -2151,12 +2793,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2151
2793
  await this.startup;
2152
2794
  await this.unsubscribeInvalidation?.();
2153
2795
  await this.flushWriteBehindQueue();
2154
- await this.generationCleanupPromise;
2796
+ await this.maintenance.waitForGenerationCleanup();
2155
2797
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
2156
- if (this.writeBehindTimer) {
2157
- clearInterval(this.writeBehindTimer);
2158
- this.writeBehindTimer = void 0;
2159
- }
2798
+ this.maintenance.disposeWriteBehindTimer();
2160
2799
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2161
2800
  })();
2162
2801
  }
@@ -2170,14 +2809,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
2170
2809
  await this.handleInvalidationMessage(message);
2171
2810
  });
2172
2811
  }
2173
- async fetchWithGuards(key, fetcher, options) {
2812
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2174
2813
  const fetchTask = async () => {
2175
2814
  const secondHit = await this.readFromLayers(key, options, "fresh-only");
2176
2815
  if (secondHit.found) {
2177
2816
  this.metricsCollector.increment("hits");
2178
2817
  return secondHit.value;
2179
2818
  }
2180
- return this.fetchAndPopulate(key, fetcher, options);
2819
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2181
2820
  };
2182
2821
  const singleFlightTask = async () => {
2183
2822
  if (!this.options.singleFlightCoordinator) {
@@ -2187,7 +2826,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2187
2826
  key,
2188
2827
  this.resolveSingleFlightOptions(),
2189
2828
  fetchTask,
2190
- () => this.waitForFreshValue(key, fetcher, options)
2829
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
2191
2830
  );
2192
2831
  };
2193
2832
  if (this.options.stampedePrevention === false) {
@@ -2195,7 +2834,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2195
2834
  }
2196
2835
  return this.stampedeGuard.execute(key, singleFlightTask);
2197
2836
  }
2198
- async waitForFreshValue(key, fetcher, options) {
2837
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2199
2838
  const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
2200
2839
  const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
2201
2840
  const deadline = Date.now() + timeoutMs;
@@ -2209,9 +2848,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2209
2848
  }
2210
2849
  await this.sleep(pollIntervalMs);
2211
2850
  }
2212
- return this.fetchAndPopulate(key, fetcher, options);
2851
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2213
2852
  }
2214
- async fetchAndPopulate(key, fetcher, options) {
2853
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2215
2854
  this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
2216
2855
  this.metricsCollector.increment("fetches");
2217
2856
  const fetchStart = Date.now();
@@ -2232,6 +2871,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2232
2871
  if (!this.shouldNegativeCache(options)) {
2233
2872
  return null;
2234
2873
  }
2874
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2875
+ this.logger.debug?.("skip-negative-store-after-invalidation", {
2876
+ key,
2877
+ expectedClearEpoch,
2878
+ clearEpoch: this.maintenance.currentClearEpoch(),
2879
+ expectedKeyEpoch,
2880
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2881
+ });
2882
+ return null;
2883
+ }
2235
2884
  await this.storeEntry(key, "empty", null, options);
2236
2885
  return null;
2237
2886
  }
@@ -2244,11 +2893,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
2244
2893
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2245
2894
  }
2246
2895
  }
2896
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2897
+ this.logger.debug?.("skip-store-after-invalidation", {
2898
+ key,
2899
+ expectedClearEpoch,
2900
+ clearEpoch: this.maintenance.currentClearEpoch(),
2901
+ expectedKeyEpoch,
2902
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2903
+ });
2904
+ return fetched;
2905
+ }
2247
2906
  await this.storeEntry(key, "value", fetched, options);
2248
2907
  return fetched;
2249
2908
  }
2250
2909
  async storeEntry(key, kind, value, options) {
2910
+ const clearEpoch = this.maintenance.currentClearEpoch();
2911
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2251
2912
  await this.writeAcrossLayers(key, kind, value, options);
2913
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2914
+ return;
2915
+ }
2252
2916
  if (options?.tags) {
2253
2917
  await this.tagIndex.track(key, options.tags);
2254
2918
  } else {
@@ -2263,6 +2927,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
2263
2927
  }
2264
2928
  async writeBatch(entries) {
2265
2929
  const now = Date.now();
2930
+ const clearEpoch = this.maintenance.currentClearEpoch();
2931
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2266
2932
  const entriesByLayer = /* @__PURE__ */ new Map();
2267
2933
  const immediateOperations = [];
2268
2934
  const deferredOperations = [];
@@ -2279,12 +2945,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
2279
2945
  }
2280
2946
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
2281
2947
  const operation = async () => {
2948
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2949
+ return;
2950
+ }
2951
+ const activeEntries = layerEntries.filter(
2952
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2953
+ );
2954
+ if (activeEntries.length === 0) {
2955
+ return;
2956
+ }
2282
2957
  try {
2283
2958
  if (layer.setMany) {
2284
- await layer.setMany(layerEntries);
2959
+ await layer.setMany(activeEntries);
2285
2960
  return;
2286
2961
  }
2287
- await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2962
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2288
2963
  } catch (error) {
2289
2964
  await this.handleLayerFailure(layer, "write", error);
2290
2965
  }
@@ -2297,7 +2972,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2297
2972
  }
2298
2973
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2299
2974
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2975
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2976
+ return;
2977
+ }
2300
2978
  for (const entry of entries) {
2979
+ if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2980
+ continue;
2981
+ }
2301
2982
  if (entry.options?.tags) {
2302
2983
  await this.tagIndex.track(entry.key, entry.options.tags);
2303
2984
  } else {
@@ -2399,10 +3080,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2399
3080
  }
2400
3081
  async writeAcrossLayers(key, kind, value, options) {
2401
3082
  const now = Date.now();
3083
+ const clearEpoch = this.maintenance.currentClearEpoch();
3084
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2402
3085
  const immediateOperations = [];
2403
3086
  const deferredOperations = [];
2404
3087
  for (const layer of this.layers) {
2405
3088
  const operation = async () => {
3089
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3090
+ return;
3091
+ }
2406
3092
  if (this.shouldSkipLayer(layer)) {
2407
3093
  return;
2408
3094
  }
@@ -2463,13 +3149,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
2463
3149
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
2464
3150
  }
2465
3151
  scheduleBackgroundRefresh(key, fetcher, options) {
2466
- if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
3152
+ if (!shouldStartBackgroundRefresh({
3153
+ isDisconnecting: this.isDisconnecting,
3154
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
3155
+ })) {
2467
3156
  return;
2468
3157
  }
3158
+ const clearEpoch = this.maintenance.currentClearEpoch();
3159
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2469
3160
  const refresh = (async () => {
2470
3161
  this.metricsCollector.increment("refreshes");
2471
3162
  try {
2472
- await this.runBackgroundRefresh(key, fetcher, options);
3163
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
2473
3164
  } catch (error) {
2474
3165
  this.metricsCollector.increment("refreshErrors");
2475
3166
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
@@ -2479,14 +3170,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2479
3170
  })();
2480
3171
  this.backgroundRefreshes.set(key, refresh);
2481
3172
  }
2482
- async runBackgroundRefresh(key, fetcher, options) {
3173
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2483
3174
  const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
2484
3175
  await this.fetchWithGuards(
2485
3176
  key,
2486
3177
  () => this.withTimeout(fetcher(), timeoutMs, () => {
2487
3178
  return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
2488
3179
  }),
2489
- options
3180
+ options,
3181
+ expectedClearEpoch,
3182
+ expectedKeyEpoch
2490
3183
  );
2491
3184
  }
2492
3185
  resolveSingleFlightOptions() {
@@ -2501,6 +3194,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2501
3194
  if (keys.length === 0) {
2502
3195
  return;
2503
3196
  }
3197
+ this.maintenance.bumpKeyEpochs(keys);
2504
3198
  await this.deleteKeysFromLayers(this.layers, keys);
2505
3199
  for (const key of keys) {
2506
3200
  await this.tagIndex.remove(key);
@@ -2523,21 +3217,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
2523
3217
  return;
2524
3218
  }
2525
3219
  const localLayers = this.layers.filter((layer) => layer.isLocal);
2526
- if (localLayers.length === 0) {
2527
- return;
2528
- }
2529
3220
  if (message.scope === "clear") {
3221
+ this.maintenance.beginClearEpoch();
2530
3222
  await Promise.all(localLayers.map((layer) => layer.clear()));
2531
3223
  await this.tagIndex.clear();
2532
3224
  this.ttlResolver.clearProfiles();
3225
+ this.circuitBreakerManager.clear();
2533
3226
  return;
2534
3227
  }
2535
3228
  const keys = message.keys ?? [];
3229
+ this.maintenance.bumpKeyEpochs(keys);
2536
3230
  await this.deleteKeysFromLayers(localLayers, keys);
2537
3231
  if (message.operation !== "write") {
2538
3232
  for (const key of keys) {
2539
3233
  await this.tagIndex.remove(key);
2540
3234
  this.ttlResolver.deleteProfile(key);
3235
+ this.circuitBreakerManager.delete(key);
2541
3236
  }
2542
3237
  }
2543
3238
  }
@@ -2589,35 +3284,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
2589
3284
  shouldBroadcastL1Invalidation() {
2590
3285
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
2591
3286
  }
2592
- shouldCleanupGenerations() {
2593
- return Boolean(this.options.generationCleanup);
2594
- }
2595
- generationCleanupBatchSize() {
2596
- const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
2597
- return configured ?? 500;
2598
- }
2599
3287
  scheduleGenerationCleanup(generation) {
2600
- const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
2601
- this.logger.warn?.("generation-cleanup-error", {
2602
- generation,
2603
- error: this.formatError(error)
2604
- });
2605
- });
2606
- this.generationCleanupPromise = task.finally(() => {
2607
- if (this.generationCleanupPromise === task) {
2608
- this.generationCleanupPromise = void 0;
3288
+ this.maintenance.scheduleGenerationCleanup(
3289
+ generation,
3290
+ async (generationToClean) => this.cleanupGeneration(generationToClean),
3291
+ (failedGeneration, error) => {
3292
+ this.logger.warn?.("generation-cleanup-error", {
3293
+ generation: failedGeneration,
3294
+ error: this.formatError(error)
3295
+ });
2609
3296
  }
2610
- });
3297
+ );
2611
3298
  }
2612
3299
  async cleanupGeneration(generation) {
2613
3300
  const prefix = `v${generation}:`;
2614
3301
  const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
2615
- if (keys.length === 0) {
2616
- return;
2617
- }
2618
- const batchSize = this.generationCleanupBatchSize();
2619
- for (let index = 0; index < keys.length; index += batchSize) {
2620
- const batch = keys.slice(index, index + batchSize);
3302
+ for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
2621
3303
  await this.deleteKeys(batch);
2622
3304
  await this.publishInvalidation({
2623
3305
  scope: "keys",
@@ -2628,58 +3310,34 @@ var CacheStack = class extends import_node_events.EventEmitter {
2628
3310
  }
2629
3311
  }
2630
3312
  initializeWriteBehind(options) {
2631
- if (this.options.writeStrategy !== "write-behind") {
2632
- return;
2633
- }
2634
- const flushIntervalMs = options?.flushIntervalMs;
2635
- if (!flushIntervalMs || flushIntervalMs <= 0) {
2636
- return;
2637
- }
2638
- this.writeBehindTimer = setInterval(() => {
2639
- void this.flushWriteBehindQueue();
2640
- }, flushIntervalMs);
2641
- this.writeBehindTimer.unref?.();
3313
+ this.maintenance.initializeWriteBehindTimer(
3314
+ this.options.writeStrategy,
3315
+ options,
3316
+ this.flushWriteBehindQueue.bind(this)
3317
+ );
2642
3318
  }
2643
3319
  shouldWriteBehind(layer) {
2644
3320
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2645
3321
  }
2646
3322
  async enqueueWriteBehind(operation) {
2647
- this.writeBehindQueue.push(operation);
2648
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
2649
- const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2650
- if (this.writeBehindQueue.length >= batchSize) {
2651
- await this.flushWriteBehindQueue();
2652
- return;
2653
- }
2654
- if (this.writeBehindQueue.length >= maxQueueSize) {
2655
- await this.flushWriteBehindQueue();
2656
- }
3323
+ await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
2657
3324
  }
2658
3325
  async flushWriteBehindQueue() {
2659
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2660
- await this.writeBehindFlushPromise;
3326
+ await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3327
+ }
3328
+ async runWriteBehindBatch(batch) {
3329
+ const results = await Promise.allSettled(batch.map((operation) => operation()));
3330
+ const failures = results.filter((result) => result.status === "rejected");
3331
+ if (failures.length === 0) {
2661
3332
  return;
2662
3333
  }
2663
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
2664
- const batch = this.writeBehindQueue.splice(0, batchSize);
2665
- this.writeBehindFlushPromise = (async () => {
2666
- const results = await Promise.allSettled(batch.map((operation) => operation()));
2667
- const failures = results.filter((result) => result.status === "rejected");
2668
- if (failures.length > 0) {
2669
- this.metricsCollector.increment("writeFailures", failures.length);
2670
- this.logger.error?.("write-behind-flush-failure", {
2671
- failed: failures.length,
2672
- total: batch.length,
2673
- errors: failures.map((failure) => this.formatError(failure.reason))
2674
- });
2675
- this.emitError("write-behind", { failed: failures.length, total: batch.length });
2676
- }
2677
- })();
2678
- await this.writeBehindFlushPromise;
2679
- this.writeBehindFlushPromise = void 0;
2680
- if (this.writeBehindQueue.length > 0) {
2681
- await this.flushWriteBehindQueue();
2682
- }
3334
+ this.metricsCollector.increment("writeFailures", failures.length);
3335
+ this.logger.error?.("write-behind-flush-failure", {
3336
+ failed: failures.length,
3337
+ total: batch.length,
3338
+ errors: failures.map((failure) => this.formatError(failure.reason))
3339
+ });
3340
+ this.emitError("write-behind", { failed: failures.length, total: batch.length });
2683
3341
  }
2684
3342
  buildLayerSetEntry(layer, key, kind, value, options, now) {
2685
3343
  const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
@@ -2709,32 +3367,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
2709
3367
  return [];
2710
3368
  }
2711
3369
  const [firstGroup, ...rest] = groups;
2712
- if (!firstGroup) {
2713
- return [];
2714
- }
2715
3370
  const restSets = rest.map((group) => new Set(group));
2716
3371
  return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2717
3372
  }
2718
3373
  qualifyKey(key) {
2719
- const prefix = this.generationPrefix();
2720
- return prefix ? `${prefix}${key}` : key;
3374
+ return qualifyGenerationKey(key, this.currentGeneration);
2721
3375
  }
2722
3376
  qualifyPattern(pattern) {
2723
- const prefix = this.generationPrefix();
2724
- return prefix ? `${prefix}${pattern}` : pattern;
3377
+ return qualifyGenerationPattern(pattern, this.currentGeneration);
2725
3378
  }
2726
3379
  stripQualifiedKey(key) {
2727
- const prefix = this.generationPrefix();
2728
- if (!prefix || !key.startsWith(prefix)) {
2729
- return key;
2730
- }
2731
- return key.slice(prefix.length);
2732
- }
2733
- generationPrefix() {
2734
- if (this.currentGeneration === void 0) {
2735
- return "";
2736
- }
2737
- return `v${this.currentGeneration}:`;
3380
+ return stripGenerationPrefix(key, this.currentGeneration);
2738
3381
  }
2739
3382
  async deleteKeysFromLayers(layers, keys) {
2740
3383
  await Promise.all(
@@ -2769,118 +3412,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
2769
3412
  if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
2770
3413
  throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
2771
3414
  }
2772
- this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
2773
- this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
2774
- this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
2775
- this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
2776
- this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
2777
- this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
2778
- this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
2779
- this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2780
- this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2781
- this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
2782
- this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
2783
- this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
2784
- this.validateCircuitBreakerOptions(this.options.circuitBreaker);
3415
+ validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
3416
+ validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
3417
+ validateLayerNumberOption("staleIfError", this.options.staleIfError);
3418
+ validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
3419
+ validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
3420
+ validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
3421
+ validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
3422
+ validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
3423
+ validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
3424
+ validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
3425
+ if (this.options.snapshotMaxBytes !== false) {
3426
+ validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
3427
+ }
3428
+ if (this.options.snapshotMaxEntries !== false) {
3429
+ validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
3430
+ }
3431
+ if (this.options.invalidationMaxKeys !== false) {
3432
+ validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
3433
+ }
3434
+ validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
3435
+ validateAdaptiveTtlOptions(this.options.adaptiveTtl);
3436
+ validateCircuitBreakerOptions(this.options.circuitBreaker);
2785
3437
  if (typeof this.options.generationCleanup === "object") {
2786
- this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
3438
+ validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
2787
3439
  }
2788
3440
  if (this.options.generation !== void 0) {
2789
- this.validateNonNegativeNumber("generation", this.options.generation);
3441
+ validateNonNegativeNumber("generation", this.options.generation);
2790
3442
  }
2791
3443
  }
2792
3444
  validateWriteOptions(options) {
2793
3445
  if (!options) {
2794
3446
  return;
2795
3447
  }
2796
- this.validateLayerNumberOption("options.ttl", options.ttl);
2797
- this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
2798
- this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
2799
- this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
2800
- this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
2801
- this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2802
- this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
2803
- this.validateAdaptiveTtlOptions(options.adaptiveTtl);
2804
- this.validateCircuitBreakerOptions(options.circuitBreaker);
2805
- this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
2806
- }
2807
- validateLayerNumberOption(name, value) {
2808
- if (value === void 0) {
2809
- return;
2810
- }
2811
- if (typeof value === "number") {
2812
- this.validateNonNegativeNumber(name, value);
2813
- return;
2814
- }
2815
- for (const [layerName, layerValue] of Object.entries(value)) {
2816
- if (layerValue === void 0) {
2817
- continue;
2818
- }
2819
- this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
2820
- }
2821
- }
2822
- validatePositiveNumber(name, value) {
2823
- if (value === void 0) {
2824
- return;
2825
- }
2826
- if (!Number.isFinite(value) || value <= 0) {
2827
- throw new Error(`${name} must be a positive finite number.`);
2828
- }
2829
- }
2830
- validateRateLimitOptions(name, options) {
2831
- if (!options) {
2832
- return;
2833
- }
2834
- this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2835
- this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2836
- this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2837
- if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2838
- throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2839
- }
2840
- if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2841
- throw new Error(`${name}.bucketKey must not be empty.`);
2842
- }
2843
- }
2844
- validateNonNegativeNumber(name, value) {
2845
- if (!Number.isFinite(value) || value < 0) {
2846
- throw new Error(`${name} must be a non-negative finite number.`);
2847
- }
2848
- }
2849
- validateCacheKey(key) {
2850
- if (key.length === 0) {
2851
- throw new Error("Cache key must not be empty.");
2852
- }
2853
- if (key.length > MAX_CACHE_KEY_LENGTH) {
2854
- throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
2855
- }
2856
- if (/[\u0000-\u001F\u007F]/.test(key)) {
2857
- throw new Error("Cache key contains unsupported control characters.");
2858
- }
2859
- if (/[\uD800-\uDFFF]/.test(key)) {
2860
- throw new Error("Cache key contains unsupported surrogate code points.");
2861
- }
2862
- return key;
2863
- }
2864
- validatePattern(pattern) {
2865
- if (pattern.length === 0) {
2866
- throw new Error("Pattern must not be empty.");
2867
- }
2868
- if (pattern.length > MAX_PATTERN_LENGTH) {
2869
- throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
2870
- }
2871
- if (/[\u0000-\u001F\u007F]/.test(pattern)) {
2872
- throw new Error("Pattern contains unsupported control characters.");
2873
- }
2874
- }
2875
- validateTtlPolicy(name, policy) {
2876
- if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2877
- return;
2878
- }
2879
- if ("alignTo" in policy) {
2880
- this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2881
- return;
2882
- }
2883
- throw new Error(`${name} is invalid.`);
3448
+ validateLayerNumberOption("options.ttl", options.ttl);
3449
+ validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
3450
+ validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
3451
+ validateLayerNumberOption("options.staleIfError", options.staleIfError);
3452
+ validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
3453
+ validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
3454
+ validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
3455
+ validateAdaptiveTtlOptions(options.adaptiveTtl);
3456
+ validateCircuitBreakerOptions(options.circuitBreaker);
3457
+ validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
3458
+ validateTags(options.tags);
2884
3459
  }
2885
3460
  assertActive(operation) {
2886
3461
  if (this.isDisconnecting) {
@@ -2892,56 +3467,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
2892
3467
  await this.startup;
2893
3468
  this.assertActive(operation);
2894
3469
  }
2895
- serializeOptions(options) {
2896
- return JSON.stringify(this.normalizeForSerialization(options) ?? null);
2897
- }
2898
- validateAdaptiveTtlOptions(options) {
2899
- if (!options || options === true) {
2900
- return;
2901
- }
2902
- this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
2903
- this.validateLayerNumberOption("adaptiveTtl.step", options.step);
2904
- this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
2905
- }
2906
- validateCircuitBreakerOptions(options) {
2907
- if (!options) {
2908
- return;
2909
- }
2910
- this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
2911
- this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
2912
- }
2913
3470
  async applyFreshReadPolicies(key, hit, options, fetcher) {
2914
- const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
2915
- const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
2916
- if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
2917
- const refreshed = refreshStoredEnvelope(hit.stored);
2918
- const ttl = remainingStoredTtlSeconds(refreshed);
3471
+ const plan = planFreshReadPolicies({
3472
+ stored: hit.stored,
3473
+ hasFetcher: Boolean(fetcher),
3474
+ slidingTtl: options?.slidingTtl ?? false,
3475
+ refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3476
+ });
3477
+ if (plan.refreshedStored) {
2919
3478
  for (let index = 0; index <= hit.layerIndex; index += 1) {
2920
3479
  const layer = this.layers[index];
2921
3480
  if (!layer || this.shouldSkipLayer(layer)) {
2922
3481
  continue;
2923
3482
  }
2924
3483
  try {
2925
- await layer.set(key, refreshed, ttl);
3484
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
2926
3485
  } catch (error) {
2927
3486
  await this.handleLayerFailure(layer, "sliding-ttl", error);
2928
3487
  }
2929
3488
  }
2930
3489
  }
2931
- if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
3490
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
2932
3491
  this.scheduleBackgroundRefresh(key, fetcher, options);
2933
3492
  }
2934
3493
  }
2935
3494
  shouldSkipLayer(layer) {
2936
- const degradedUntil = this.layerDegradedUntil.get(layer.name);
2937
- return degradedUntil !== void 0 && degradedUntil > Date.now();
3495
+ return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
2938
3496
  }
2939
3497
  async handleLayerFailure(layer, operation, error) {
2940
- if (!this.isGracefulDegradationEnabled()) {
3498
+ const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
3499
+ if (!recovery.degrade) {
2941
3500
  throw error;
2942
3501
  }
2943
- const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
2944
- this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
3502
+ this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
2945
3503
  this.metricsCollector.increment("degradedOperations");
2946
3504
  this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
2947
3505
  this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
@@ -2977,18 +3535,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2977
3535
  this.emit("error", { operation, ...context });
2978
3536
  }
2979
3537
  }
2980
- serializeKeyPart(value) {
2981
- if (typeof value === "string") {
2982
- return `s:${value}`;
2983
- }
2984
- if (typeof value === "number") {
2985
- return `n:${value}`;
2986
- }
2987
- if (typeof value === "boolean") {
2988
- return `b:${value}`;
2989
- }
2990
- return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
2991
- }
2992
3538
  isCacheSnapshotEntries(value) {
2993
3539
  return Array.isArray(value) && value.every((entry) => {
2994
3540
  if (!entry || typeof entry !== "object") {
@@ -3001,54 +3547,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
3001
3547
  sanitizeSnapshotValue(value) {
3002
3548
  return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
3003
3549
  }
3004
- async validateSnapshotFilePath(filePath) {
3005
- if (filePath.length === 0) {
3006
- throw new Error("filePath must not be empty.");
3007
- }
3008
- if (filePath.includes("\0")) {
3009
- throw new Error("filePath must not contain null bytes.");
3550
+ snapshotMaxBytes() {
3551
+ return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3552
+ }
3553
+ snapshotMaxEntries() {
3554
+ return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
3555
+ }
3556
+ invalidationMaxKeys() {
3557
+ return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3558
+ }
3559
+ async collectKeysForTag(tag) {
3560
+ const keys = /* @__PURE__ */ new Set();
3561
+ if (this.tagIndex.forEachKeyForTag) {
3562
+ await this.tagIndex.forEachKeyForTag(tag, async (key) => {
3563
+ keys.add(key);
3564
+ this.assertWithinInvalidationKeyLimit(keys.size);
3565
+ });
3566
+ return [...keys];
3010
3567
  }
3011
- const path = await import("path");
3012
- const resolved = path.resolve(filePath);
3013
- const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
3014
- if (baseDir !== false) {
3015
- const relative = path.relative(baseDir, resolved);
3016
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
3017
- throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
3018
- }
3568
+ for (const key of await this.tagIndex.keysForTag(tag)) {
3569
+ keys.add(key);
3570
+ this.assertWithinInvalidationKeyLimit(keys.size);
3019
3571
  }
3020
- return resolved;
3572
+ return [...keys];
3021
3573
  }
3022
- normalizeForSerialization(value) {
3023
- if (Array.isArray(value)) {
3024
- return value.map((entry) => this.normalizeForSerialization(entry));
3574
+ assertWithinInvalidationKeyLimit(size) {
3575
+ const maxKeys = this.invalidationMaxKeys();
3576
+ if (maxKeys !== false && size > maxKeys) {
3577
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
3025
3578
  }
3026
- if (value && typeof value === "object") {
3027
- return Object.keys(value).sort().reduce((normalized, key) => {
3028
- if (DANGEROUS_OBJECT_KEYS.has(key)) {
3029
- return normalized;
3579
+ }
3580
+ async visitExportEntries(maxEntries, visitor) {
3581
+ const exported = /* @__PURE__ */ new Set();
3582
+ for (const layer of this.layers) {
3583
+ if (!layer.keys && !layer.forEachKey) {
3584
+ continue;
3585
+ }
3586
+ const visitKey = async (key) => {
3587
+ const exportedKey = this.stripQualifiedKey(key);
3588
+ if (exported.has(exportedKey)) {
3589
+ return;
3030
3590
  }
3031
- normalized[key] = this.normalizeForSerialization(value[key]);
3032
- return normalized;
3033
- }, {});
3591
+ const stored = await this.readLayerEntry(layer, key);
3592
+ if (stored === null) {
3593
+ return;
3594
+ }
3595
+ exported.add(exportedKey);
3596
+ if (maxEntries !== false && exported.size > maxEntries) {
3597
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
3598
+ }
3599
+ await visitor({
3600
+ key: exportedKey,
3601
+ value: stored,
3602
+ ttl: remainingStoredTtlSeconds(stored)
3603
+ });
3604
+ };
3605
+ if (layer.forEachKey) {
3606
+ await layer.forEachKey(visitKey);
3607
+ continue;
3608
+ }
3609
+ const keys = await layer.keys?.();
3610
+ for (const key of keys ?? []) {
3611
+ await visitKey(key);
3612
+ }
3034
3613
  }
3035
- return value;
3036
3614
  }
3037
3615
  };
3038
- function createInstanceId() {
3039
- if (globalThis.crypto?.randomUUID) {
3040
- return globalThis.crypto.randomUUID();
3041
- }
3042
- const bytes = new Uint8Array(16);
3043
- if (globalThis.crypto?.getRandomValues) {
3044
- globalThis.crypto.getRandomValues(bytes);
3045
- } else {
3046
- for (let i = 0; i < bytes.length; i++) {
3047
- bytes[i] = Math.floor(Math.random() * 256);
3048
- }
3049
- }
3050
- return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
3051
- }
3052
3616
 
3053
3617
  // src/module.ts
3054
3618
  var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);