layercache 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -21
- package/README.md +254 -906
- package/dist/{chunk-7V7XAB74.js → chunk-4PPBOOXT.js} +37 -3
- package/dist/{chunk-QHWG7QS5.js → chunk-BQLL6IM5.js} +47 -1
- package/dist/{chunk-KOYGHLVP.js → chunk-GJBKCFE6.js} +65 -10
- package/dist/cli.cjs +85 -19
- package/dist/cli.js +4 -18
- package/dist/{edge-Dw97n89L.d.ts → edge-DLstcDMn.d.cts} +32 -13
- package/dist/{edge-Dw97n89L.d.cts → edge-DLstcDMn.d.ts} +32 -13
- package/dist/edge.cjs +101 -12
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +1160 -350
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +1017 -342
- package/package.json +29 -3
- package/packages/nestjs/dist/index.cjs +793 -270
- package/packages/nestjs/dist/index.d.cts +23 -12
- package/packages/nestjs/dist/index.d.ts +23 -12
- package/packages/nestjs/dist/index.js +793 -270
|
@@ -264,22 +264,23 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
264
264
|
constructor(cache, prefix) {
|
|
265
265
|
this.cache = cache;
|
|
266
266
|
this.prefix = prefix;
|
|
267
|
+
validateNamespaceKey(prefix);
|
|
267
268
|
}
|
|
268
269
|
cache;
|
|
269
270
|
prefix;
|
|
270
271
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
271
272
|
metrics = emptyMetrics();
|
|
272
273
|
async get(key, fetcher, options) {
|
|
273
|
-
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
274
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
274
275
|
}
|
|
275
276
|
async getOrSet(key, fetcher, options) {
|
|
276
|
-
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
277
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
277
278
|
}
|
|
278
279
|
/**
|
|
279
280
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
280
281
|
*/
|
|
281
282
|
async getOrThrow(key, fetcher, options) {
|
|
282
|
-
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
283
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
283
284
|
}
|
|
284
285
|
async has(key) {
|
|
285
286
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
@@ -288,7 +289,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
288
289
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
289
290
|
}
|
|
290
291
|
async set(key, value, options) {
|
|
291
|
-
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
292
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
292
293
|
}
|
|
293
294
|
async delete(key) {
|
|
294
295
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
@@ -304,7 +305,8 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
304
305
|
() => this.cache.mget(
|
|
305
306
|
entries.map((entry) => ({
|
|
306
307
|
...entry,
|
|
307
|
-
key: this.qualify(entry.key)
|
|
308
|
+
key: this.qualify(entry.key),
|
|
309
|
+
options: this.qualifyGetOptions(entry.options)
|
|
308
310
|
}))
|
|
309
311
|
)
|
|
310
312
|
);
|
|
@@ -314,16 +316,22 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
314
316
|
() => this.cache.mset(
|
|
315
317
|
entries.map((entry) => ({
|
|
316
318
|
...entry,
|
|
317
|
-
key: this.qualify(entry.key)
|
|
319
|
+
key: this.qualify(entry.key),
|
|
320
|
+
options: this.qualifyWriteOptions(entry.options)
|
|
318
321
|
}))
|
|
319
322
|
)
|
|
320
323
|
);
|
|
321
324
|
}
|
|
322
325
|
async invalidateByTag(tag) {
|
|
323
|
-
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
326
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
324
327
|
}
|
|
325
328
|
async invalidateByTags(tags, mode = "any") {
|
|
326
|
-
await this.trackMetrics(
|
|
329
|
+
await this.trackMetrics(
|
|
330
|
+
() => this.cache.invalidateByTags(
|
|
331
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
332
|
+
mode
|
|
333
|
+
)
|
|
334
|
+
);
|
|
327
335
|
}
|
|
328
336
|
async invalidateByPattern(pattern) {
|
|
329
337
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
@@ -335,16 +343,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
335
343
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
336
344
|
*/
|
|
337
345
|
async inspect(key) {
|
|
338
|
-
|
|
346
|
+
const result = await this.cache.inspect(this.qualify(key));
|
|
347
|
+
if (result === null) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
...result,
|
|
352
|
+
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
353
|
+
};
|
|
339
354
|
}
|
|
340
355
|
wrap(keyPrefix, fetcher, options) {
|
|
341
|
-
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
356
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
342
357
|
}
|
|
343
358
|
warm(entries, options) {
|
|
344
359
|
return this.cache.warm(
|
|
345
360
|
entries.map((entry) => ({
|
|
346
361
|
...entry,
|
|
347
|
-
key: this.qualify(entry.key)
|
|
362
|
+
key: this.qualify(entry.key),
|
|
363
|
+
options: this.qualifyGetOptions(entry.options)
|
|
348
364
|
})),
|
|
349
365
|
options
|
|
350
366
|
);
|
|
@@ -374,11 +390,30 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
374
390
|
* ```
|
|
375
391
|
*/
|
|
376
392
|
namespace(childPrefix) {
|
|
393
|
+
validateNamespaceKey(childPrefix);
|
|
377
394
|
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
378
395
|
}
|
|
379
396
|
qualify(key) {
|
|
380
397
|
return `${this.prefix}:${key}`;
|
|
381
398
|
}
|
|
399
|
+
qualifyTag(tag) {
|
|
400
|
+
return `${this.prefix}:${tag}`;
|
|
401
|
+
}
|
|
402
|
+
qualifyGetOptions(options) {
|
|
403
|
+
return this.qualifyWriteOptions(options);
|
|
404
|
+
}
|
|
405
|
+
qualifyWrapOptions(options) {
|
|
406
|
+
return this.qualifyWriteOptions(options);
|
|
407
|
+
}
|
|
408
|
+
qualifyWriteOptions(options) {
|
|
409
|
+
if (!options?.tags || options.tags.length === 0) {
|
|
410
|
+
return options;
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
...options,
|
|
414
|
+
tags: options.tags.map((tag) => this.qualifyTag(tag))
|
|
415
|
+
};
|
|
416
|
+
}
|
|
382
417
|
async trackMetrics(operation) {
|
|
383
418
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
384
419
|
const before = this.cache.getMetrics();
|
|
@@ -503,6 +538,20 @@ function addMap(base, delta) {
|
|
|
503
538
|
}
|
|
504
539
|
return result;
|
|
505
540
|
}
|
|
541
|
+
function validateNamespaceKey(key) {
|
|
542
|
+
if (key.length === 0) {
|
|
543
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
544
|
+
}
|
|
545
|
+
if (key.length > 256) {
|
|
546
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
547
|
+
}
|
|
548
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
549
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
550
|
+
}
|
|
551
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
552
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
506
555
|
|
|
507
556
|
// ../../src/invalidation/PatternMatcher.ts
|
|
508
557
|
var PatternMatcher = class _PatternMatcher {
|
|
@@ -558,21 +607,41 @@ var CacheKeyDiscovery = class {
|
|
|
558
607
|
this.options = options;
|
|
559
608
|
}
|
|
560
609
|
options;
|
|
561
|
-
async collectKeysWithPrefix(prefix) {
|
|
610
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
562
611
|
const { tagIndex } = this.options;
|
|
563
|
-
const matches = new Set(
|
|
564
|
-
|
|
565
|
-
|
|
612
|
+
const matches = /* @__PURE__ */ new Set();
|
|
613
|
+
if (tagIndex.forEachKeyForPrefix) {
|
|
614
|
+
await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
|
|
615
|
+
matches.add(key);
|
|
616
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
617
|
+
});
|
|
618
|
+
} else {
|
|
619
|
+
const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
|
|
620
|
+
for (const key of initialMatches) {
|
|
621
|
+
matches.add(key);
|
|
622
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
566
625
|
await Promise.all(
|
|
567
626
|
this.options.layers.map(async (layer) => {
|
|
568
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
627
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
569
628
|
return;
|
|
570
629
|
}
|
|
571
630
|
try {
|
|
572
|
-
|
|
573
|
-
|
|
631
|
+
if (layer.forEachKey) {
|
|
632
|
+
await layer.forEachKey(async (key) => {
|
|
633
|
+
if (key.startsWith(prefix)) {
|
|
634
|
+
matches.add(key);
|
|
635
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const keys = await layer.keys?.();
|
|
641
|
+
for (const key of keys ?? []) {
|
|
574
642
|
if (key.startsWith(prefix)) {
|
|
575
643
|
matches.add(key);
|
|
644
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
576
645
|
}
|
|
577
646
|
}
|
|
578
647
|
} catch (error) {
|
|
@@ -582,18 +651,39 @@ var CacheKeyDiscovery = class {
|
|
|
582
651
|
);
|
|
583
652
|
return [...matches];
|
|
584
653
|
}
|
|
585
|
-
async collectKeysMatchingPattern(pattern) {
|
|
586
|
-
const matches = new Set(
|
|
654
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
655
|
+
const matches = /* @__PURE__ */ new Set();
|
|
656
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
657
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
658
|
+
matches.add(key);
|
|
659
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
660
|
+
});
|
|
661
|
+
} else {
|
|
662
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
663
|
+
matches.add(key);
|
|
664
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
587
667
|
await Promise.all(
|
|
588
668
|
this.options.layers.map(async (layer) => {
|
|
589
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
669
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
590
670
|
return;
|
|
591
671
|
}
|
|
592
672
|
try {
|
|
593
|
-
|
|
594
|
-
|
|
673
|
+
if (layer.forEachKey) {
|
|
674
|
+
await layer.forEachKey(async (key) => {
|
|
675
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
676
|
+
matches.add(key);
|
|
677
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const keys = await layer.keys?.();
|
|
683
|
+
for (const key of keys ?? []) {
|
|
595
684
|
if (PatternMatcher.matches(pattern, key)) {
|
|
596
685
|
matches.add(key);
|
|
686
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
597
687
|
}
|
|
598
688
|
}
|
|
599
689
|
} catch (error) {
|
|
@@ -603,8 +693,280 @@ var CacheKeyDiscovery = class {
|
|
|
603
693
|
);
|
|
604
694
|
return [...matches];
|
|
605
695
|
}
|
|
696
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
697
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
698
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
606
701
|
};
|
|
607
702
|
|
|
703
|
+
// ../../src/internal/CacheKeySerialization.ts
|
|
704
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
705
|
+
function normalizeForSerialization(value) {
|
|
706
|
+
if (Array.isArray(value)) {
|
|
707
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
708
|
+
}
|
|
709
|
+
if (value && typeof value === "object") {
|
|
710
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
711
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
712
|
+
return normalized;
|
|
713
|
+
}
|
|
714
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
715
|
+
return normalized;
|
|
716
|
+
}, {});
|
|
717
|
+
}
|
|
718
|
+
return value;
|
|
719
|
+
}
|
|
720
|
+
function serializeKeyPart(value) {
|
|
721
|
+
if (typeof value === "string") {
|
|
722
|
+
return `s:${value}`;
|
|
723
|
+
}
|
|
724
|
+
if (typeof value === "number") {
|
|
725
|
+
return `n:${value}`;
|
|
726
|
+
}
|
|
727
|
+
if (typeof value === "boolean") {
|
|
728
|
+
return `b:${value}`;
|
|
729
|
+
}
|
|
730
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
731
|
+
}
|
|
732
|
+
function serializeOptions(options) {
|
|
733
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
734
|
+
}
|
|
735
|
+
function createInstanceId() {
|
|
736
|
+
if (globalThis.crypto?.randomUUID) {
|
|
737
|
+
return globalThis.crypto.randomUUID();
|
|
738
|
+
}
|
|
739
|
+
const bytes = new Uint8Array(16);
|
|
740
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
741
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
742
|
+
} else {
|
|
743
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
744
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
751
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
752
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
753
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
754
|
+
}
|
|
755
|
+
async function findExistingAncestor(directory, fs, path) {
|
|
756
|
+
let current = directory;
|
|
757
|
+
while (true) {
|
|
758
|
+
try {
|
|
759
|
+
await fs.lstat(current);
|
|
760
|
+
return current;
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (error.code !== "ENOENT") {
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const parent = path.dirname(current);
|
|
767
|
+
if (parent === current) {
|
|
768
|
+
return current;
|
|
769
|
+
}
|
|
770
|
+
current = parent;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
774
|
+
if (filePath.length === 0) {
|
|
775
|
+
throw new Error("filePath must not be empty.");
|
|
776
|
+
}
|
|
777
|
+
if (filePath.includes("\0")) {
|
|
778
|
+
throw new Error("filePath must not contain null bytes.");
|
|
779
|
+
}
|
|
780
|
+
const { promises: fs } = await import("fs");
|
|
781
|
+
const path = await import("path");
|
|
782
|
+
const resolved = path.resolve(filePath);
|
|
783
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
784
|
+
if (baseDir === false) {
|
|
785
|
+
return resolved;
|
|
786
|
+
}
|
|
787
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
788
|
+
const realBaseDir = await fs.realpath(baseDir);
|
|
789
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
790
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
791
|
+
}
|
|
792
|
+
if (mode === "read") {
|
|
793
|
+
const realTarget = await fs.realpath(resolved);
|
|
794
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
795
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
796
|
+
}
|
|
797
|
+
return realTarget;
|
|
798
|
+
}
|
|
799
|
+
const parentDir = path.dirname(resolved);
|
|
800
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs, path);
|
|
801
|
+
const realExistingAncestor = await fs.realpath(existingAncestor);
|
|
802
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
803
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
804
|
+
}
|
|
805
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
806
|
+
const realParentDir = await fs.realpath(parentDir);
|
|
807
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
808
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
809
|
+
}
|
|
810
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
811
|
+
try {
|
|
812
|
+
const existing = await fs.lstat(targetPath);
|
|
813
|
+
if (existing.isSymbolicLink()) {
|
|
814
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
815
|
+
}
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (error.code !== "ENOENT") {
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return targetPath;
|
|
822
|
+
}
|
|
823
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
824
|
+
if (byteLimit === false) {
|
|
825
|
+
return handle.readFile({ encoding: "utf8" });
|
|
826
|
+
}
|
|
827
|
+
const chunks = [];
|
|
828
|
+
let totalBytes = 0;
|
|
829
|
+
let position = 0;
|
|
830
|
+
while (true) {
|
|
831
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
832
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
833
|
+
if (bytesRead === 0) {
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
totalBytes += bytesRead;
|
|
837
|
+
if (totalBytes > byteLimit) {
|
|
838
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
839
|
+
}
|
|
840
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
841
|
+
position += bytesRead;
|
|
842
|
+
}
|
|
843
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ../../src/internal/CacheStackValidation.ts
|
|
847
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
848
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
849
|
+
var MAX_TAGS_PER_OPERATION = 128;
|
|
850
|
+
function validatePositiveNumber(name, value) {
|
|
851
|
+
if (value === void 0) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
855
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function validateNonNegativeNumber(name, value) {
|
|
859
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
860
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function validateLayerNumberOption(name, value) {
|
|
864
|
+
if (value === void 0) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (typeof value === "number") {
|
|
868
|
+
validateNonNegativeNumber(name, value);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
872
|
+
if (layerValue === void 0) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function validateRateLimitOptions(name, options) {
|
|
879
|
+
if (!options) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
883
|
+
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
884
|
+
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
885
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
886
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
887
|
+
}
|
|
888
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
889
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function validateCacheKey(key) {
|
|
893
|
+
if (key.length === 0) {
|
|
894
|
+
throw new Error("Cache key must not be empty.");
|
|
895
|
+
}
|
|
896
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
897
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
898
|
+
}
|
|
899
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
900
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
901
|
+
}
|
|
902
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
903
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
904
|
+
}
|
|
905
|
+
return key;
|
|
906
|
+
}
|
|
907
|
+
function validateTag(tag) {
|
|
908
|
+
if (tag.length === 0) {
|
|
909
|
+
throw new Error("Cache tag must not be empty.");
|
|
910
|
+
}
|
|
911
|
+
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
912
|
+
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
913
|
+
}
|
|
914
|
+
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
915
|
+
throw new Error("Cache tag contains unsupported control characters.");
|
|
916
|
+
}
|
|
917
|
+
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
918
|
+
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
919
|
+
}
|
|
920
|
+
return tag;
|
|
921
|
+
}
|
|
922
|
+
function validateTags(tags) {
|
|
923
|
+
if (!tags) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
927
|
+
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
928
|
+
}
|
|
929
|
+
for (const tag of tags) {
|
|
930
|
+
validateTag(tag);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function validatePattern(pattern) {
|
|
934
|
+
if (pattern.length === 0) {
|
|
935
|
+
throw new Error("Pattern must not be empty.");
|
|
936
|
+
}
|
|
937
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
938
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
939
|
+
}
|
|
940
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
941
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function validateTtlPolicy(name, policy) {
|
|
945
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if ("alignTo" in policy) {
|
|
949
|
+
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
throw new Error(`${name} is invalid.`);
|
|
953
|
+
}
|
|
954
|
+
function validateAdaptiveTtlOptions(options) {
|
|
955
|
+
if (!options || options === true) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
959
|
+
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
960
|
+
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
961
|
+
}
|
|
962
|
+
function validateCircuitBreakerOptions(options) {
|
|
963
|
+
if (!options) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
967
|
+
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
968
|
+
}
|
|
969
|
+
|
|
608
970
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
609
971
|
var CircuitBreakerManager = class {
|
|
610
972
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -623,9 +985,7 @@ var CircuitBreakerManager = class {
|
|
|
623
985
|
}
|
|
624
986
|
const now = Date.now();
|
|
625
987
|
if (state.openUntil <= now) {
|
|
626
|
-
|
|
627
|
-
state.failures = 0;
|
|
628
|
-
this.breakers.set(key, state);
|
|
988
|
+
this.breakers.delete(key);
|
|
629
989
|
return;
|
|
630
990
|
}
|
|
631
991
|
const remainingMs = state.openUntil - now;
|
|
@@ -636,15 +996,15 @@ var CircuitBreakerManager = class {
|
|
|
636
996
|
if (!options) {
|
|
637
997
|
return;
|
|
638
998
|
}
|
|
999
|
+
this.pruneIfNeeded();
|
|
639
1000
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
640
1001
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
641
|
-
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
1002
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
642
1003
|
state.failures += 1;
|
|
643
1004
|
if (state.failures >= failureThreshold) {
|
|
644
1005
|
state.openUntil = Date.now() + cooldownMs;
|
|
645
1006
|
}
|
|
646
1007
|
this.breakers.set(key, state);
|
|
647
|
-
this.pruneIfNeeded();
|
|
648
1008
|
}
|
|
649
1009
|
recordSuccess(key) {
|
|
650
1010
|
this.breakers.delete(key);
|
|
@@ -655,8 +1015,7 @@ var CircuitBreakerManager = class {
|
|
|
655
1015
|
return false;
|
|
656
1016
|
}
|
|
657
1017
|
if (state.openUntil <= Date.now()) {
|
|
658
|
-
|
|
659
|
-
state.failures = 0;
|
|
1018
|
+
this.breakers.delete(key);
|
|
660
1019
|
return false;
|
|
661
1020
|
}
|
|
662
1021
|
return true;
|
|
@@ -680,15 +1039,20 @@ var CircuitBreakerManager = class {
|
|
|
680
1039
|
if (this.breakers.size <= this.maxEntries) {
|
|
681
1040
|
return;
|
|
682
1041
|
}
|
|
1042
|
+
const now = Date.now();
|
|
683
1043
|
for (const [key, state] of this.breakers.entries()) {
|
|
684
1044
|
if (this.breakers.size <= this.maxEntries) {
|
|
685
|
-
|
|
1045
|
+
return;
|
|
686
1046
|
}
|
|
687
|
-
if (!state.openUntil || state.openUntil <=
|
|
1047
|
+
if (!state.openUntil || state.openUntil <= now) {
|
|
688
1048
|
this.breakers.delete(key);
|
|
689
1049
|
}
|
|
690
1050
|
}
|
|
691
|
-
|
|
1051
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
1055
|
+
for (const [key] of sorted) {
|
|
692
1056
|
if (this.breakers.size <= this.maxEntries) {
|
|
693
1057
|
break;
|
|
694
1058
|
}
|
|
@@ -698,6 +1062,7 @@ var CircuitBreakerManager = class {
|
|
|
698
1062
|
};
|
|
699
1063
|
|
|
700
1064
|
// ../../src/internal/FetchRateLimiter.ts
|
|
1065
|
+
var MAX_BUCKETS = 1e4;
|
|
701
1066
|
var FetchRateLimiter = class {
|
|
702
1067
|
buckets = /* @__PURE__ */ new Map();
|
|
703
1068
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -863,10 +1228,25 @@ var FetchRateLimiter = class {
|
|
|
863
1228
|
if (existing) {
|
|
864
1229
|
return existing;
|
|
865
1230
|
}
|
|
1231
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1232
|
+
this.evictIdleBuckets();
|
|
1233
|
+
}
|
|
866
1234
|
const bucket = { active: 0, startedAt: [] };
|
|
867
1235
|
this.buckets.set(bucketKey, bucket);
|
|
868
1236
|
return bucket;
|
|
869
1237
|
}
|
|
1238
|
+
evictIdleBuckets() {
|
|
1239
|
+
for (const [key, bucket] of this.buckets.entries()) {
|
|
1240
|
+
if (this.buckets.size <= MAX_BUCKETS * 0.9) {
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
|
|
1244
|
+
this.buckets.delete(key);
|
|
1245
|
+
this.queuesByBucket.delete(key);
|
|
1246
|
+
this.pendingBuckets.delete(key);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
870
1250
|
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
871
1251
|
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
872
1252
|
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
@@ -980,19 +1360,47 @@ function isStoredValueEnvelope(value) {
|
|
|
980
1360
|
if (v.kind !== "value" && v.kind !== "empty") {
|
|
981
1361
|
return false;
|
|
982
1362
|
}
|
|
983
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
1363
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
984
1364
|
return false;
|
|
985
1365
|
}
|
|
986
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
1366
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
987
1367
|
return false;
|
|
988
1368
|
}
|
|
989
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
1369
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
990
1370
|
return false;
|
|
991
1371
|
}
|
|
992
1372
|
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
993
1373
|
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
994
1374
|
return false;
|
|
995
1375
|
}
|
|
1376
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1392
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
996
1404
|
return true;
|
|
997
1405
|
}
|
|
998
1406
|
function createStoredValueEnvelope(options) {
|
|
@@ -1091,6 +1499,12 @@ function normalizePositiveSeconds(value) {
|
|
|
1091
1499
|
}
|
|
1092
1500
|
return value;
|
|
1093
1501
|
}
|
|
1502
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1503
|
+
if (value == null) {
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1506
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1507
|
+
}
|
|
1094
1508
|
|
|
1095
1509
|
// ../../src/internal/TtlResolver.ts
|
|
1096
1510
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1193,18 +1607,18 @@ var TtlResolver = class {
|
|
|
1193
1607
|
return;
|
|
1194
1608
|
}
|
|
1195
1609
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
1196
|
-
|
|
1197
|
-
for (
|
|
1198
|
-
|
|
1199
|
-
|
|
1610
|
+
const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
1611
|
+
for (let i = 0; i < toRemove && i < sorted.length; i++) {
|
|
1612
|
+
const entry = sorted[i];
|
|
1613
|
+
if (entry) {
|
|
1614
|
+
this.accessProfiles.delete(entry[0]);
|
|
1200
1615
|
}
|
|
1201
|
-
this.accessProfiles.delete(key);
|
|
1202
|
-
removed += 1;
|
|
1203
1616
|
}
|
|
1204
1617
|
}
|
|
1205
1618
|
};
|
|
1206
1619
|
|
|
1207
1620
|
// ../../src/invalidation/TagIndex.ts
|
|
1621
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
1208
1622
|
var TagIndex = class {
|
|
1209
1623
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
1210
1624
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -1245,6 +1659,11 @@ var TagIndex = class {
|
|
|
1245
1659
|
async keysForTag(tag) {
|
|
1246
1660
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1247
1661
|
}
|
|
1662
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1663
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1664
|
+
await visitor(key);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1248
1667
|
async keysForPrefix(prefix) {
|
|
1249
1668
|
const node = this.findNode(prefix);
|
|
1250
1669
|
if (!node) {
|
|
@@ -1254,14 +1673,27 @@ var TagIndex = class {
|
|
|
1254
1673
|
this.collectFromNode(node, prefix, matches);
|
|
1255
1674
|
return matches;
|
|
1256
1675
|
}
|
|
1676
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
1677
|
+
const node = this.findNode(prefix);
|
|
1678
|
+
if (!node) {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
await this.visitFromNode(node, prefix, visitor);
|
|
1682
|
+
}
|
|
1257
1683
|
async tagsForKey(key) {
|
|
1258
1684
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1259
1685
|
}
|
|
1260
1686
|
async matchPattern(pattern) {
|
|
1261
1687
|
const matches = /* @__PURE__ */ new Set();
|
|
1262
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1688
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1263
1689
|
return [...matches];
|
|
1264
1690
|
}
|
|
1691
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
1692
|
+
const matches = await this.matchPattern(pattern);
|
|
1693
|
+
for (const key of matches) {
|
|
1694
|
+
await visitor(key);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1265
1697
|
async clear() {
|
|
1266
1698
|
this.tagToKeys.clear();
|
|
1267
1699
|
this.keyToTags.clear();
|
|
@@ -1311,7 +1743,18 @@ var TagIndex = class {
|
|
|
1311
1743
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1312
1744
|
}
|
|
1313
1745
|
}
|
|
1314
|
-
|
|
1746
|
+
async visitFromNode(node, prefix, visitor) {
|
|
1747
|
+
if (node.terminal) {
|
|
1748
|
+
await visitor(prefix);
|
|
1749
|
+
}
|
|
1750
|
+
for (const [character, child] of node.children) {
|
|
1751
|
+
await this.visitFromNode(child, `${prefix}${character}`, visitor);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1755
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1315
1758
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
1316
1759
|
if (visited.has(stateKey)) {
|
|
1317
1760
|
return;
|
|
@@ -1328,21 +1771,37 @@ var TagIndex = class {
|
|
|
1328
1771
|
return;
|
|
1329
1772
|
}
|
|
1330
1773
|
if (patternChar === "*") {
|
|
1331
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1774
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
1332
1775
|
for (const [character, child2] of node.children) {
|
|
1333
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1776
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
1334
1777
|
}
|
|
1335
1778
|
return;
|
|
1336
1779
|
}
|
|
1337
1780
|
if (patternChar === "?") {
|
|
1338
1781
|
for (const [character, child2] of node.children) {
|
|
1339
|
-
this.collectPatternMatches(
|
|
1782
|
+
this.collectPatternMatches(
|
|
1783
|
+
child2,
|
|
1784
|
+
`${prefix}${character}`,
|
|
1785
|
+
pattern,
|
|
1786
|
+
patternIndex + 1,
|
|
1787
|
+
matches,
|
|
1788
|
+
visited,
|
|
1789
|
+
depth + 1
|
|
1790
|
+
);
|
|
1340
1791
|
}
|
|
1341
1792
|
return;
|
|
1342
1793
|
}
|
|
1343
1794
|
const child = node.children.get(patternChar);
|
|
1344
1795
|
if (child) {
|
|
1345
|
-
this.collectPatternMatches(
|
|
1796
|
+
this.collectPatternMatches(
|
|
1797
|
+
child,
|
|
1798
|
+
`${prefix}${patternChar}`,
|
|
1799
|
+
pattern,
|
|
1800
|
+
patternIndex + 1,
|
|
1801
|
+
matches,
|
|
1802
|
+
visited,
|
|
1803
|
+
depth + 1
|
|
1804
|
+
);
|
|
1346
1805
|
}
|
|
1347
1806
|
}
|
|
1348
1807
|
pruneKnownKeysIfNeeded() {
|
|
@@ -1409,22 +1868,27 @@ var TagIndex = class {
|
|
|
1409
1868
|
|
|
1410
1869
|
// ../../src/serialization/JsonSerializer.ts
|
|
1411
1870
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1871
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1412
1872
|
var JsonSerializer = class {
|
|
1413
1873
|
serialize(value) {
|
|
1414
1874
|
return JSON.stringify(value);
|
|
1415
1875
|
}
|
|
1416
1876
|
deserialize(payload) {
|
|
1417
1877
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1418
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1878
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1419
1879
|
}
|
|
1420
1880
|
};
|
|
1421
1881
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1422
|
-
function sanitizeJsonValue(value, depth) {
|
|
1882
|
+
function sanitizeJsonValue(value, depth, state) {
|
|
1883
|
+
state.count += 1;
|
|
1884
|
+
if (state.count > MAX_SANITIZE_NODES) {
|
|
1885
|
+
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
1886
|
+
}
|
|
1423
1887
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1424
|
-
|
|
1888
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1425
1889
|
}
|
|
1426
1890
|
if (Array.isArray(value)) {
|
|
1427
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1891
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1428
1892
|
}
|
|
1429
1893
|
if (!isPlainObject(value)) {
|
|
1430
1894
|
return value;
|
|
@@ -1434,7 +1898,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1434
1898
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1435
1899
|
continue;
|
|
1436
1900
|
}
|
|
1437
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1901
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1438
1902
|
}
|
|
1439
1903
|
return sanitized;
|
|
1440
1904
|
}
|
|
@@ -1483,9 +1947,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1483
1947
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1484
1948
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1485
1949
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1486
|
-
var
|
|
1950
|
+
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1951
|
+
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1952
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1953
|
+
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1487
1954
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1488
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1489
1955
|
var DebugLogger = class {
|
|
1490
1956
|
enabled;
|
|
1491
1957
|
constructor(enabled) {
|
|
@@ -1572,6 +2038,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1572
2038
|
snapshotSerializer = new JsonSerializer();
|
|
1573
2039
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1574
2040
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2041
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1575
2042
|
ttlResolver;
|
|
1576
2043
|
circuitBreakerManager;
|
|
1577
2044
|
currentGeneration;
|
|
@@ -1579,6 +2046,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1579
2046
|
writeBehindTimer;
|
|
1580
2047
|
writeBehindFlushPromise;
|
|
1581
2048
|
generationCleanupPromise;
|
|
2049
|
+
clearEpoch = 0;
|
|
1582
2050
|
isDisconnecting = false;
|
|
1583
2051
|
disconnectPromise;
|
|
1584
2052
|
/**
|
|
@@ -1588,7 +2056,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1588
2056
|
* and no `fetcher` is provided.
|
|
1589
2057
|
*/
|
|
1590
2058
|
async get(key, fetcher, options) {
|
|
1591
|
-
const normalizedKey = this.qualifyKey(
|
|
2059
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1592
2060
|
this.validateWriteOptions(options);
|
|
1593
2061
|
await this.awaitStartup("get");
|
|
1594
2062
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1658,7 +2126,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1658
2126
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1659
2127
|
*/
|
|
1660
2128
|
async has(key) {
|
|
1661
|
-
const normalizedKey = this.qualifyKey(
|
|
2129
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1662
2130
|
await this.awaitStartup("has");
|
|
1663
2131
|
for (const layer of this.layers) {
|
|
1664
2132
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1691,7 +2159,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1691
2159
|
* that has it, or null if the key is not found / has no TTL.
|
|
1692
2160
|
*/
|
|
1693
2161
|
async ttl(key) {
|
|
1694
|
-
const normalizedKey = this.qualifyKey(
|
|
2162
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1695
2163
|
await this.awaitStartup("ttl");
|
|
1696
2164
|
for (const layer of this.layers) {
|
|
1697
2165
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1713,7 +2181,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1713
2181
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1714
2182
|
*/
|
|
1715
2183
|
async set(key, value, options) {
|
|
1716
|
-
const normalizedKey = this.qualifyKey(
|
|
2184
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1717
2185
|
this.validateWriteOptions(options);
|
|
1718
2186
|
await this.awaitStartup("set");
|
|
1719
2187
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1722,7 +2190,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1722
2190
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1723
2191
|
*/
|
|
1724
2192
|
async delete(key) {
|
|
1725
|
-
const normalizedKey = this.qualifyKey(
|
|
2193
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1726
2194
|
await this.awaitStartup("delete");
|
|
1727
2195
|
await this.deleteKeys([normalizedKey]);
|
|
1728
2196
|
await this.publishInvalidation({
|
|
@@ -1734,6 +2202,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1734
2202
|
}
|
|
1735
2203
|
async clear() {
|
|
1736
2204
|
await this.awaitStartup("clear");
|
|
2205
|
+
this.beginClearEpoch();
|
|
1737
2206
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1738
2207
|
await this.tagIndex.clear();
|
|
1739
2208
|
this.ttlResolver.clearProfiles();
|
|
@@ -1750,7 +2219,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1750
2219
|
return;
|
|
1751
2220
|
}
|
|
1752
2221
|
await this.awaitStartup("mdelete");
|
|
1753
|
-
const normalizedKeys = keys.map((k) =>
|
|
2222
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1754
2223
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1755
2224
|
await this.deleteKeys(cacheKeys);
|
|
1756
2225
|
await this.publishInvalidation({
|
|
@@ -1767,7 +2236,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1767
2236
|
}
|
|
1768
2237
|
const normalizedEntries = entries.map((entry) => ({
|
|
1769
2238
|
...entry,
|
|
1770
|
-
key: this.qualifyKey(
|
|
2239
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1771
2240
|
}));
|
|
1772
2241
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1773
2242
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1776,7 +2245,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1776
2245
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1777
2246
|
return Promise.all(
|
|
1778
2247
|
normalizedEntries.map((entry) => {
|
|
1779
|
-
const optionsSignature =
|
|
2248
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1780
2249
|
const existing = pendingReads.get(entry.key);
|
|
1781
2250
|
if (!existing) {
|
|
1782
2251
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1845,7 +2314,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1845
2314
|
this.assertActive("mset");
|
|
1846
2315
|
const normalizedEntries = entries.map((entry) => ({
|
|
1847
2316
|
...entry,
|
|
1848
|
-
key: this.qualifyKey(
|
|
2317
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1849
2318
|
}));
|
|
1850
2319
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1851
2320
|
await this.awaitStartup("mset");
|
|
@@ -1888,7 +2357,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1888
2357
|
*/
|
|
1889
2358
|
wrap(prefix, fetcher, options = {}) {
|
|
1890
2359
|
return (...args) => {
|
|
1891
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2360
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1892
2361
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1893
2362
|
return this.get(key, () => fetcher(...args), options);
|
|
1894
2363
|
};
|
|
@@ -1898,11 +2367,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1898
2367
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1899
2368
|
*/
|
|
1900
2369
|
namespace(prefix) {
|
|
2370
|
+
validateNamespaceKey(prefix);
|
|
1901
2371
|
return new CacheNamespace(this, prefix);
|
|
1902
2372
|
}
|
|
1903
2373
|
async invalidateByTag(tag) {
|
|
2374
|
+
validateTag(tag);
|
|
1904
2375
|
await this.awaitStartup("invalidateByTag");
|
|
1905
|
-
const keys = await this.
|
|
2376
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1906
2377
|
await this.deleteKeys(keys);
|
|
1907
2378
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1908
2379
|
}
|
|
@@ -1910,22 +2381,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1910
2381
|
if (tags.length === 0) {
|
|
1911
2382
|
return;
|
|
1912
2383
|
}
|
|
2384
|
+
validateTags(tags);
|
|
1913
2385
|
await this.awaitStartup("invalidateByTags");
|
|
1914
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2386
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1915
2387
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2388
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1916
2389
|
await this.deleteKeys(keys);
|
|
1917
2390
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1918
2391
|
}
|
|
1919
2392
|
async invalidateByPattern(pattern) {
|
|
2393
|
+
validatePattern(pattern);
|
|
1920
2394
|
await this.awaitStartup("invalidateByPattern");
|
|
1921
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2395
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2396
|
+
this.qualifyPattern(pattern),
|
|
2397
|
+
this.invalidationMaxKeys()
|
|
2398
|
+
);
|
|
1922
2399
|
await this.deleteKeys(keys);
|
|
1923
2400
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1924
2401
|
}
|
|
1925
2402
|
async invalidateByPrefix(prefix) {
|
|
1926
2403
|
await this.awaitStartup("invalidateByPrefix");
|
|
1927
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1928
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2404
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2405
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1929
2406
|
await this.deleteKeys(keys);
|
|
1930
2407
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1931
2408
|
}
|
|
@@ -1995,7 +2472,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1995
2472
|
* Returns `null` if the key does not exist in any layer.
|
|
1996
2473
|
*/
|
|
1997
2474
|
async inspect(key) {
|
|
1998
|
-
const userKey =
|
|
2475
|
+
const userKey = validateCacheKey(key);
|
|
1999
2476
|
const normalizedKey = this.qualifyKey(userKey);
|
|
2000
2477
|
await this.awaitStartup("inspect");
|
|
2001
2478
|
const foundInLayers = [];
|
|
@@ -2032,50 +2509,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2032
2509
|
}
|
|
2033
2510
|
async exportState() {
|
|
2034
2511
|
await this.awaitStartup("exportState");
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const keys = await layer.keys();
|
|
2041
|
-
for (const key of keys) {
|
|
2042
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
2043
|
-
if (exported.has(exportedKey)) {
|
|
2044
|
-
continue;
|
|
2045
|
-
}
|
|
2046
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2047
|
-
if (stored === null) {
|
|
2048
|
-
continue;
|
|
2049
|
-
}
|
|
2050
|
-
exported.set(exportedKey, {
|
|
2051
|
-
key: exportedKey,
|
|
2052
|
-
value: stored,
|
|
2053
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
return [...exported.values()];
|
|
2512
|
+
const entries = [];
|
|
2513
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2514
|
+
entries.push(entry);
|
|
2515
|
+
});
|
|
2516
|
+
return entries;
|
|
2058
2517
|
}
|
|
2059
2518
|
async importState(entries) {
|
|
2060
2519
|
await this.awaitStartup("importState");
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2520
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2521
|
+
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2522
|
+
value: entry.value,
|
|
2523
|
+
ttl: entry.ttl
|
|
2524
|
+
}));
|
|
2525
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2526
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2527
|
+
await Promise.all(
|
|
2528
|
+
batch.map(async (entry) => {
|
|
2529
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2530
|
+
await this.tagIndex.touch(entry.key);
|
|
2531
|
+
})
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2068
2534
|
}
|
|
2069
2535
|
async persistToFile(filePath) {
|
|
2070
2536
|
this.assertActive("persistToFile");
|
|
2071
|
-
const snapshot = await this.exportState();
|
|
2072
2537
|
const { promises: fs } = await import("fs");
|
|
2073
|
-
|
|
2538
|
+
const path = await import("path");
|
|
2539
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2540
|
+
const tempPath = path.join(
|
|
2541
|
+
path.dirname(targetPath),
|
|
2542
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2543
|
+
);
|
|
2544
|
+
let handle;
|
|
2545
|
+
try {
|
|
2546
|
+
handle = await fs.open(tempPath, "wx");
|
|
2547
|
+
const openedHandle = handle;
|
|
2548
|
+
await openedHandle.writeFile("[", "utf8");
|
|
2549
|
+
let wroteAny = false;
|
|
2550
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2551
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2552
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2553
|
+
wroteAny = true;
|
|
2554
|
+
});
|
|
2555
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2556
|
+
await openedHandle.close();
|
|
2557
|
+
handle = void 0;
|
|
2558
|
+
await fs.rename(tempPath, targetPath);
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
await handle?.close().catch(() => void 0);
|
|
2561
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
2562
|
+
throw error;
|
|
2563
|
+
}
|
|
2074
2564
|
}
|
|
2075
2565
|
async restoreFromFile(filePath) {
|
|
2076
2566
|
this.assertActive("restoreFromFile");
|
|
2077
|
-
const { promises: fs } = await import("fs");
|
|
2078
|
-
const
|
|
2567
|
+
const { promises: fs, constants } = await import("fs");
|
|
2568
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2569
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2570
|
+
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2571
|
+
let raw;
|
|
2572
|
+
try {
|
|
2573
|
+
if (snapshotMaxBytes !== false) {
|
|
2574
|
+
const stat = await handle.stat();
|
|
2575
|
+
if (stat.size > snapshotMaxBytes) {
|
|
2576
|
+
throw new Error(
|
|
2577
|
+
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2582
|
+
} finally {
|
|
2583
|
+
await handle.close();
|
|
2584
|
+
}
|
|
2079
2585
|
let parsed;
|
|
2080
2586
|
try {
|
|
2081
2587
|
parsed = JSON.parse(raw);
|
|
@@ -2119,14 +2625,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2119
2625
|
await this.handleInvalidationMessage(message);
|
|
2120
2626
|
});
|
|
2121
2627
|
}
|
|
2122
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2628
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2123
2629
|
const fetchTask = async () => {
|
|
2124
2630
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2125
2631
|
if (secondHit.found) {
|
|
2126
2632
|
this.metricsCollector.increment("hits");
|
|
2127
2633
|
return secondHit.value;
|
|
2128
2634
|
}
|
|
2129
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2635
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2130
2636
|
};
|
|
2131
2637
|
const singleFlightTask = async () => {
|
|
2132
2638
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -2136,7 +2642,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2136
2642
|
key,
|
|
2137
2643
|
this.resolveSingleFlightOptions(),
|
|
2138
2644
|
fetchTask,
|
|
2139
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2645
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2140
2646
|
);
|
|
2141
2647
|
};
|
|
2142
2648
|
if (this.options.stampedePrevention === false) {
|
|
@@ -2144,7 +2650,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2144
2650
|
}
|
|
2145
2651
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2146
2652
|
}
|
|
2147
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2653
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2148
2654
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2149
2655
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2150
2656
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2158,9 +2664,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2158
2664
|
}
|
|
2159
2665
|
await this.sleep(pollIntervalMs);
|
|
2160
2666
|
}
|
|
2161
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2667
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2162
2668
|
}
|
|
2163
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2669
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2164
2670
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2165
2671
|
this.metricsCollector.increment("fetches");
|
|
2166
2672
|
const fetchStart = Date.now();
|
|
@@ -2181,6 +2687,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2181
2687
|
if (!this.shouldNegativeCache(options)) {
|
|
2182
2688
|
return null;
|
|
2183
2689
|
}
|
|
2690
|
+
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2691
|
+
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2692
|
+
key,
|
|
2693
|
+
expectedClearEpoch,
|
|
2694
|
+
clearEpoch: this.clearEpoch,
|
|
2695
|
+
expectedKeyEpoch,
|
|
2696
|
+
keyEpoch: this.currentKeyEpoch(key)
|
|
2697
|
+
});
|
|
2698
|
+
return null;
|
|
2699
|
+
}
|
|
2184
2700
|
await this.storeEntry(key, "empty", null, options);
|
|
2185
2701
|
return null;
|
|
2186
2702
|
}
|
|
@@ -2193,11 +2709,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2193
2709
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2194
2710
|
}
|
|
2195
2711
|
}
|
|
2712
|
+
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2713
|
+
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2714
|
+
key,
|
|
2715
|
+
expectedClearEpoch,
|
|
2716
|
+
clearEpoch: this.clearEpoch,
|
|
2717
|
+
expectedKeyEpoch,
|
|
2718
|
+
keyEpoch: this.currentKeyEpoch(key)
|
|
2719
|
+
});
|
|
2720
|
+
return fetched;
|
|
2721
|
+
}
|
|
2196
2722
|
await this.storeEntry(key, "value", fetched, options);
|
|
2197
2723
|
return fetched;
|
|
2198
2724
|
}
|
|
2199
2725
|
async storeEntry(key, kind, value, options) {
|
|
2726
|
+
const clearEpoch = this.clearEpoch;
|
|
2727
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2200
2728
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2729
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2201
2732
|
if (options?.tags) {
|
|
2202
2733
|
await this.tagIndex.track(key, options.tags);
|
|
2203
2734
|
} else {
|
|
@@ -2212,6 +2743,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2212
2743
|
}
|
|
2213
2744
|
async writeBatch(entries) {
|
|
2214
2745
|
const now = Date.now();
|
|
2746
|
+
const clearEpoch = this.clearEpoch;
|
|
2747
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2215
2748
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2216
2749
|
const immediateOperations = [];
|
|
2217
2750
|
const deferredOperations = [];
|
|
@@ -2228,12 +2761,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2228
2761
|
}
|
|
2229
2762
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2230
2763
|
const operation = async () => {
|
|
2764
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
const activeEntries = layerEntries.filter(
|
|
2768
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
|
|
2769
|
+
);
|
|
2770
|
+
if (activeEntries.length === 0) {
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2231
2773
|
try {
|
|
2232
2774
|
if (layer.setMany) {
|
|
2233
|
-
await layer.setMany(
|
|
2775
|
+
await layer.setMany(activeEntries);
|
|
2234
2776
|
return;
|
|
2235
2777
|
}
|
|
2236
|
-
await Promise.all(
|
|
2778
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2237
2779
|
} catch (error) {
|
|
2238
2780
|
await this.handleLayerFailure(layer, "write", error);
|
|
2239
2781
|
}
|
|
@@ -2246,7 +2788,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2246
2788
|
}
|
|
2247
2789
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2248
2790
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2791
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2249
2794
|
for (const entry of entries) {
|
|
2795
|
+
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2250
2798
|
if (entry.options?.tags) {
|
|
2251
2799
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2252
2800
|
} else {
|
|
@@ -2348,10 +2896,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2348
2896
|
}
|
|
2349
2897
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2350
2898
|
const now = Date.now();
|
|
2899
|
+
const clearEpoch = this.clearEpoch;
|
|
2900
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2351
2901
|
const immediateOperations = [];
|
|
2352
2902
|
const deferredOperations = [];
|
|
2353
2903
|
for (const layer of this.layers) {
|
|
2354
2904
|
const operation = async () => {
|
|
2905
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2355
2908
|
if (this.shouldSkipLayer(layer)) {
|
|
2356
2909
|
return;
|
|
2357
2910
|
}
|
|
@@ -2415,10 +2968,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2415
2968
|
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
2416
2969
|
return;
|
|
2417
2970
|
}
|
|
2971
|
+
const clearEpoch = this.clearEpoch;
|
|
2972
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2418
2973
|
const refresh = (async () => {
|
|
2419
2974
|
this.metricsCollector.increment("refreshes");
|
|
2420
2975
|
try {
|
|
2421
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2976
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2422
2977
|
} catch (error) {
|
|
2423
2978
|
this.metricsCollector.increment("refreshErrors");
|
|
2424
2979
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2428,14 +2983,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2428
2983
|
})();
|
|
2429
2984
|
this.backgroundRefreshes.set(key, refresh);
|
|
2430
2985
|
}
|
|
2431
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2986
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2432
2987
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2433
2988
|
await this.fetchWithGuards(
|
|
2434
2989
|
key,
|
|
2435
2990
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2436
2991
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2437
2992
|
}),
|
|
2438
|
-
options
|
|
2993
|
+
options,
|
|
2994
|
+
expectedClearEpoch,
|
|
2995
|
+
expectedKeyEpoch
|
|
2439
2996
|
);
|
|
2440
2997
|
}
|
|
2441
2998
|
resolveSingleFlightOptions() {
|
|
@@ -2450,6 +3007,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2450
3007
|
if (keys.length === 0) {
|
|
2451
3008
|
return;
|
|
2452
3009
|
}
|
|
3010
|
+
this.bumpKeyEpochs(keys);
|
|
2453
3011
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2454
3012
|
for (const key of keys) {
|
|
2455
3013
|
await this.tagIndex.remove(key);
|
|
@@ -2472,21 +3030,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2472
3030
|
return;
|
|
2473
3031
|
}
|
|
2474
3032
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2475
|
-
if (localLayers.length === 0) {
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
3033
|
if (message.scope === "clear") {
|
|
3034
|
+
this.beginClearEpoch();
|
|
2479
3035
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2480
3036
|
await this.tagIndex.clear();
|
|
2481
3037
|
this.ttlResolver.clearProfiles();
|
|
3038
|
+
this.circuitBreakerManager.clear();
|
|
2482
3039
|
return;
|
|
2483
3040
|
}
|
|
2484
3041
|
const keys = message.keys ?? [];
|
|
3042
|
+
this.bumpKeyEpochs(keys);
|
|
2485
3043
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2486
3044
|
if (message.operation !== "write") {
|
|
2487
3045
|
for (const key of keys) {
|
|
2488
3046
|
await this.tagIndex.remove(key);
|
|
2489
3047
|
this.ttlResolver.deleteProfile(key);
|
|
3048
|
+
this.circuitBreakerManager.delete(key);
|
|
2490
3049
|
}
|
|
2491
3050
|
}
|
|
2492
3051
|
}
|
|
@@ -2592,6 +3151,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2592
3151
|
shouldWriteBehind(layer) {
|
|
2593
3152
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2594
3153
|
}
|
|
3154
|
+
beginClearEpoch() {
|
|
3155
|
+
this.clearEpoch += 1;
|
|
3156
|
+
this.keyEpochs.clear();
|
|
3157
|
+
this.writeBehindQueue.length = 0;
|
|
3158
|
+
}
|
|
3159
|
+
currentKeyEpoch(key) {
|
|
3160
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
3161
|
+
}
|
|
3162
|
+
bumpKeyEpochs(keys) {
|
|
3163
|
+
for (const key of keys) {
|
|
3164
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
3168
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
3169
|
+
return true;
|
|
3170
|
+
}
|
|
3171
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
3172
|
+
return true;
|
|
3173
|
+
}
|
|
3174
|
+
return false;
|
|
3175
|
+
}
|
|
2595
3176
|
async enqueueWriteBehind(operation) {
|
|
2596
3177
|
this.writeBehindQueue.push(operation);
|
|
2597
3178
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
@@ -2718,107 +3299,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2718
3299
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2719
3300
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2720
3301
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
3302
|
+
validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
3303
|
+
validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
3304
|
+
validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
3305
|
+
validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
3306
|
+
validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
3307
|
+
validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
3308
|
+
validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
3309
|
+
validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
3310
|
+
validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
3311
|
+
validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
3312
|
+
if (this.options.snapshotMaxBytes !== false) {
|
|
3313
|
+
validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
|
|
3314
|
+
}
|
|
3315
|
+
if (this.options.snapshotMaxEntries !== false) {
|
|
3316
|
+
validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
|
|
3317
|
+
}
|
|
3318
|
+
if (this.options.invalidationMaxKeys !== false) {
|
|
3319
|
+
validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
|
|
3320
|
+
}
|
|
3321
|
+
validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
3322
|
+
validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
3323
|
+
validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2734
3324
|
if (typeof this.options.generationCleanup === "object") {
|
|
2735
|
-
|
|
3325
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2736
3326
|
}
|
|
2737
3327
|
if (this.options.generation !== void 0) {
|
|
2738
|
-
|
|
3328
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2739
3329
|
}
|
|
2740
3330
|
}
|
|
2741
3331
|
validateWriteOptions(options) {
|
|
2742
3332
|
if (!options) {
|
|
2743
3333
|
return;
|
|
2744
3334
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
validateLayerNumberOption(name, value) {
|
|
2757
|
-
if (value === void 0) {
|
|
2758
|
-
return;
|
|
2759
|
-
}
|
|
2760
|
-
if (typeof value === "number") {
|
|
2761
|
-
this.validateNonNegativeNumber(name, value);
|
|
2762
|
-
return;
|
|
2763
|
-
}
|
|
2764
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2765
|
-
if (layerValue === void 0) {
|
|
2766
|
-
continue;
|
|
2767
|
-
}
|
|
2768
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
validatePositiveNumber(name, value) {
|
|
2772
|
-
if (value === void 0) {
|
|
2773
|
-
return;
|
|
2774
|
-
}
|
|
2775
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2776
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
validateRateLimitOptions(name, options) {
|
|
2780
|
-
if (!options) {
|
|
2781
|
-
return;
|
|
2782
|
-
}
|
|
2783
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2784
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2785
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2786
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2787
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2788
|
-
}
|
|
2789
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2790
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
validateNonNegativeNumber(name, value) {
|
|
2794
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2795
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
validateCacheKey(key) {
|
|
2799
|
-
if (key.length === 0) {
|
|
2800
|
-
throw new Error("Cache key must not be empty.");
|
|
2801
|
-
}
|
|
2802
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2803
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2804
|
-
}
|
|
2805
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2806
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2807
|
-
}
|
|
2808
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2809
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2810
|
-
}
|
|
2811
|
-
return key;
|
|
2812
|
-
}
|
|
2813
|
-
validateTtlPolicy(name, policy) {
|
|
2814
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2815
|
-
return;
|
|
2816
|
-
}
|
|
2817
|
-
if ("alignTo" in policy) {
|
|
2818
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2819
|
-
return;
|
|
2820
|
-
}
|
|
2821
|
-
throw new Error(`${name} is invalid.`);
|
|
3335
|
+
validateLayerNumberOption("options.ttl", options.ttl);
|
|
3336
|
+
validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
3337
|
+
validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
3338
|
+
validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
3339
|
+
validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
3340
|
+
validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
3341
|
+
validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
3342
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
3343
|
+
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3344
|
+
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3345
|
+
validateTags(options.tags);
|
|
2822
3346
|
}
|
|
2823
3347
|
assertActive(operation) {
|
|
2824
3348
|
if (this.isDisconnecting) {
|
|
@@ -2830,24 +3354,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2830
3354
|
await this.startup;
|
|
2831
3355
|
this.assertActive(operation);
|
|
2832
3356
|
}
|
|
2833
|
-
serializeOptions(options) {
|
|
2834
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2835
|
-
}
|
|
2836
|
-
validateAdaptiveTtlOptions(options) {
|
|
2837
|
-
if (!options || options === true) {
|
|
2838
|
-
return;
|
|
2839
|
-
}
|
|
2840
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2841
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2842
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2843
|
-
}
|
|
2844
|
-
validateCircuitBreakerOptions(options) {
|
|
2845
|
-
if (!options) {
|
|
2846
|
-
return;
|
|
2847
|
-
}
|
|
2848
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2849
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2850
|
-
}
|
|
2851
3357
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2852
3358
|
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
2853
3359
|
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
@@ -2915,18 +3421,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2915
3421
|
this.emit("error", { operation, ...context });
|
|
2916
3422
|
}
|
|
2917
3423
|
}
|
|
2918
|
-
serializeKeyPart(value) {
|
|
2919
|
-
if (typeof value === "string") {
|
|
2920
|
-
return `s:${value}`;
|
|
2921
|
-
}
|
|
2922
|
-
if (typeof value === "number") {
|
|
2923
|
-
return `n:${value}`;
|
|
2924
|
-
}
|
|
2925
|
-
if (typeof value === "boolean") {
|
|
2926
|
-
return `b:${value}`;
|
|
2927
|
-
}
|
|
2928
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2929
|
-
}
|
|
2930
3424
|
isCacheSnapshotEntries(value) {
|
|
2931
3425
|
return Array.isArray(value) && value.every((entry) => {
|
|
2932
3426
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2939,43 +3433,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2939
3433
|
sanitizeSnapshotValue(value) {
|
|
2940
3434
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2941
3435
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3436
|
+
snapshotMaxBytes() {
|
|
3437
|
+
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3438
|
+
}
|
|
3439
|
+
snapshotMaxEntries() {
|
|
3440
|
+
return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
|
|
3441
|
+
}
|
|
3442
|
+
invalidationMaxKeys() {
|
|
3443
|
+
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3444
|
+
}
|
|
3445
|
+
async collectKeysForTag(tag) {
|
|
3446
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3447
|
+
if (this.tagIndex.forEachKeyForTag) {
|
|
3448
|
+
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3449
|
+
keys.add(key);
|
|
3450
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3451
|
+
});
|
|
3452
|
+
return [...keys];
|
|
2948
3453
|
}
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
if (baseDir !== false) {
|
|
2953
|
-
const relative = path.relative(baseDir, resolved);
|
|
2954
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2955
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2956
|
-
}
|
|
3454
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3455
|
+
keys.add(key);
|
|
3456
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2957
3457
|
}
|
|
2958
|
-
return
|
|
3458
|
+
return [...keys];
|
|
2959
3459
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3460
|
+
assertWithinInvalidationKeyLimit(size) {
|
|
3461
|
+
const maxKeys = this.invalidationMaxKeys();
|
|
3462
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
3463
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2963
3464
|
}
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3465
|
+
}
|
|
3466
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
3467
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3468
|
+
for (const layer of this.layers) {
|
|
3469
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
3470
|
+
continue;
|
|
3471
|
+
}
|
|
3472
|
+
const visitKey = async (key) => {
|
|
3473
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
3474
|
+
if (exported.has(exportedKey)) {
|
|
3475
|
+
return;
|
|
2968
3476
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3477
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
3478
|
+
if (stored === null) {
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
exported.add(exportedKey);
|
|
3482
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3483
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3484
|
+
}
|
|
3485
|
+
await visitor({
|
|
3486
|
+
key: exportedKey,
|
|
3487
|
+
value: stored,
|
|
3488
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
3489
|
+
});
|
|
3490
|
+
};
|
|
3491
|
+
if (layer.forEachKey) {
|
|
3492
|
+
await layer.forEachKey(visitKey);
|
|
3493
|
+
continue;
|
|
3494
|
+
}
|
|
3495
|
+
const keys = await layer.keys?.();
|
|
3496
|
+
for (const key of keys ?? []) {
|
|
3497
|
+
await visitKey(key);
|
|
3498
|
+
}
|
|
2972
3499
|
}
|
|
2973
|
-
return value;
|
|
2974
3500
|
}
|
|
2975
3501
|
};
|
|
2976
|
-
function createInstanceId() {
|
|
2977
|
-
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2978
|
-
}
|
|
2979
3502
|
|
|
2980
3503
|
// src/module.ts
|
|
2981
3504
|
var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);
|