layercache 1.2.5 → 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 -912
- package/dist/{chunk-7V7XAB74.js → chunk-4PPBOOXT.js} +37 -3
- package/dist/{chunk-QHWG7QS5.js → chunk-BQLL6IM5.js} +47 -1
- package/dist/{chunk-JC26W3KK.js → chunk-GJBKCFE6.js} +38 -3
- package/dist/cli.cjs +83 -3
- package/dist/cli.js +2 -2
- package/dist/{edge-P07GCO2Y.d.ts → edge-DLstcDMn.d.cts} +32 -14
- package/dist/{edge-P07GCO2Y.d.cts → edge-DLstcDMn.d.ts} +32 -14
- package/dist/edge.cjs +74 -5
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +1070 -352
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +950 -347
- package/package.json +29 -3
- package/packages/nestjs/dist/index.cjs +722 -272
- package/packages/nestjs/dist/index.d.cts +23 -13
- package/packages/nestjs/dist/index.d.ts +23 -13
- package/packages/nestjs/dist/index.js +722 -272
|
@@ -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
|
);
|
|
@@ -380,6 +396,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
380
396
|
qualify(key) {
|
|
381
397
|
return `${this.prefix}:${key}`;
|
|
382
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
|
+
}
|
|
383
417
|
async trackMetrics(operation) {
|
|
384
418
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
385
419
|
const before = this.cache.getMetrics();
|
|
@@ -514,6 +548,9 @@ function validateNamespaceKey(key) {
|
|
|
514
548
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
515
549
|
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
516
550
|
}
|
|
551
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
552
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
553
|
+
}
|
|
517
554
|
}
|
|
518
555
|
|
|
519
556
|
// ../../src/invalidation/PatternMatcher.ts
|
|
@@ -570,21 +607,41 @@ var CacheKeyDiscovery = class {
|
|
|
570
607
|
this.options = options;
|
|
571
608
|
}
|
|
572
609
|
options;
|
|
573
|
-
async collectKeysWithPrefix(prefix) {
|
|
610
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
574
611
|
const { tagIndex } = this.options;
|
|
575
|
-
const matches = new Set(
|
|
576
|
-
|
|
577
|
-
|
|
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
|
+
}
|
|
578
625
|
await Promise.all(
|
|
579
626
|
this.options.layers.map(async (layer) => {
|
|
580
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
627
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
581
628
|
return;
|
|
582
629
|
}
|
|
583
630
|
try {
|
|
584
|
-
|
|
585
|
-
|
|
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 ?? []) {
|
|
586
642
|
if (key.startsWith(prefix)) {
|
|
587
643
|
matches.add(key);
|
|
644
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
588
645
|
}
|
|
589
646
|
}
|
|
590
647
|
} catch (error) {
|
|
@@ -594,18 +651,39 @@ var CacheKeyDiscovery = class {
|
|
|
594
651
|
);
|
|
595
652
|
return [...matches];
|
|
596
653
|
}
|
|
597
|
-
async collectKeysMatchingPattern(pattern) {
|
|
598
|
-
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
|
+
}
|
|
599
667
|
await Promise.all(
|
|
600
668
|
this.options.layers.map(async (layer) => {
|
|
601
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
669
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
602
670
|
return;
|
|
603
671
|
}
|
|
604
672
|
try {
|
|
605
|
-
|
|
606
|
-
|
|
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 ?? []) {
|
|
607
684
|
if (PatternMatcher.matches(pattern, key)) {
|
|
608
685
|
matches.add(key);
|
|
686
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
609
687
|
}
|
|
610
688
|
}
|
|
611
689
|
} catch (error) {
|
|
@@ -615,8 +693,280 @@ var CacheKeyDiscovery = class {
|
|
|
615
693
|
);
|
|
616
694
|
return [...matches];
|
|
617
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
|
+
}
|
|
618
701
|
};
|
|
619
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
|
+
|
|
620
970
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
621
971
|
var CircuitBreakerManager = class {
|
|
622
972
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -1010,19 +1360,47 @@ function isStoredValueEnvelope(value) {
|
|
|
1010
1360
|
if (v.kind !== "value" && v.kind !== "empty") {
|
|
1011
1361
|
return false;
|
|
1012
1362
|
}
|
|
1013
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
1363
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
1014
1364
|
return false;
|
|
1015
1365
|
}
|
|
1016
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
1366
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
1017
1367
|
return false;
|
|
1018
1368
|
}
|
|
1019
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
1369
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
1020
1370
|
return false;
|
|
1021
1371
|
}
|
|
1022
1372
|
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
1023
1373
|
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
1024
1374
|
return false;
|
|
1025
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
|
+
}
|
|
1026
1404
|
return true;
|
|
1027
1405
|
}
|
|
1028
1406
|
function createStoredValueEnvelope(options) {
|
|
@@ -1121,6 +1499,12 @@ function normalizePositiveSeconds(value) {
|
|
|
1121
1499
|
}
|
|
1122
1500
|
return value;
|
|
1123
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
|
+
}
|
|
1124
1508
|
|
|
1125
1509
|
// ../../src/internal/TtlResolver.ts
|
|
1126
1510
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1275,6 +1659,11 @@ var TagIndex = class {
|
|
|
1275
1659
|
async keysForTag(tag) {
|
|
1276
1660
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1277
1661
|
}
|
|
1662
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1663
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1664
|
+
await visitor(key);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1278
1667
|
async keysForPrefix(prefix) {
|
|
1279
1668
|
const node = this.findNode(prefix);
|
|
1280
1669
|
if (!node) {
|
|
@@ -1284,6 +1673,13 @@ var TagIndex = class {
|
|
|
1284
1673
|
this.collectFromNode(node, prefix, matches);
|
|
1285
1674
|
return matches;
|
|
1286
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
|
+
}
|
|
1287
1683
|
async tagsForKey(key) {
|
|
1288
1684
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1289
1685
|
}
|
|
@@ -1292,6 +1688,12 @@ var TagIndex = class {
|
|
|
1292
1688
|
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1293
1689
|
return [...matches];
|
|
1294
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
|
+
}
|
|
1295
1697
|
async clear() {
|
|
1296
1698
|
this.tagToKeys.clear();
|
|
1297
1699
|
this.keyToTags.clear();
|
|
@@ -1341,6 +1743,14 @@ var TagIndex = class {
|
|
|
1341
1743
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1342
1744
|
}
|
|
1343
1745
|
}
|
|
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
|
+
}
|
|
1344
1754
|
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1345
1755
|
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1346
1756
|
return;
|
|
@@ -1458,22 +1868,27 @@ var TagIndex = class {
|
|
|
1458
1868
|
|
|
1459
1869
|
// ../../src/serialization/JsonSerializer.ts
|
|
1460
1870
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1871
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1461
1872
|
var JsonSerializer = class {
|
|
1462
1873
|
serialize(value) {
|
|
1463
1874
|
return JSON.stringify(value);
|
|
1464
1875
|
}
|
|
1465
1876
|
deserialize(payload) {
|
|
1466
1877
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1467
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1878
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1468
1879
|
}
|
|
1469
1880
|
};
|
|
1470
1881
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1471
|
-
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
|
+
}
|
|
1472
1887
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1473
|
-
|
|
1888
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1474
1889
|
}
|
|
1475
1890
|
if (Array.isArray(value)) {
|
|
1476
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1891
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1477
1892
|
}
|
|
1478
1893
|
if (!isPlainObject(value)) {
|
|
1479
1894
|
return value;
|
|
@@ -1483,7 +1898,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1483
1898
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1484
1899
|
continue;
|
|
1485
1900
|
}
|
|
1486
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1901
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1487
1902
|
}
|
|
1488
1903
|
return sanitized;
|
|
1489
1904
|
}
|
|
@@ -1532,10 +1947,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1532
1947
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1533
1948
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1534
1949
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1535
|
-
var
|
|
1536
|
-
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;
|
|
1537
1954
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1538
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1539
1955
|
var DebugLogger = class {
|
|
1540
1956
|
enabled;
|
|
1541
1957
|
constructor(enabled) {
|
|
@@ -1622,6 +2038,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1622
2038
|
snapshotSerializer = new JsonSerializer();
|
|
1623
2039
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1624
2040
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2041
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1625
2042
|
ttlResolver;
|
|
1626
2043
|
circuitBreakerManager;
|
|
1627
2044
|
currentGeneration;
|
|
@@ -1629,6 +2046,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1629
2046
|
writeBehindTimer;
|
|
1630
2047
|
writeBehindFlushPromise;
|
|
1631
2048
|
generationCleanupPromise;
|
|
2049
|
+
clearEpoch = 0;
|
|
1632
2050
|
isDisconnecting = false;
|
|
1633
2051
|
disconnectPromise;
|
|
1634
2052
|
/**
|
|
@@ -1638,7 +2056,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1638
2056
|
* and no `fetcher` is provided.
|
|
1639
2057
|
*/
|
|
1640
2058
|
async get(key, fetcher, options) {
|
|
1641
|
-
const normalizedKey = this.qualifyKey(
|
|
2059
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1642
2060
|
this.validateWriteOptions(options);
|
|
1643
2061
|
await this.awaitStartup("get");
|
|
1644
2062
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1708,7 +2126,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1708
2126
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1709
2127
|
*/
|
|
1710
2128
|
async has(key) {
|
|
1711
|
-
const normalizedKey = this.qualifyKey(
|
|
2129
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1712
2130
|
await this.awaitStartup("has");
|
|
1713
2131
|
for (const layer of this.layers) {
|
|
1714
2132
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1741,7 +2159,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1741
2159
|
* that has it, or null if the key is not found / has no TTL.
|
|
1742
2160
|
*/
|
|
1743
2161
|
async ttl(key) {
|
|
1744
|
-
const normalizedKey = this.qualifyKey(
|
|
2162
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1745
2163
|
await this.awaitStartup("ttl");
|
|
1746
2164
|
for (const layer of this.layers) {
|
|
1747
2165
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1763,7 +2181,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1763
2181
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1764
2182
|
*/
|
|
1765
2183
|
async set(key, value, options) {
|
|
1766
|
-
const normalizedKey = this.qualifyKey(
|
|
2184
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1767
2185
|
this.validateWriteOptions(options);
|
|
1768
2186
|
await this.awaitStartup("set");
|
|
1769
2187
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1772,7 +2190,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1772
2190
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1773
2191
|
*/
|
|
1774
2192
|
async delete(key) {
|
|
1775
|
-
const normalizedKey = this.qualifyKey(
|
|
2193
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1776
2194
|
await this.awaitStartup("delete");
|
|
1777
2195
|
await this.deleteKeys([normalizedKey]);
|
|
1778
2196
|
await this.publishInvalidation({
|
|
@@ -1784,6 +2202,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1784
2202
|
}
|
|
1785
2203
|
async clear() {
|
|
1786
2204
|
await this.awaitStartup("clear");
|
|
2205
|
+
this.beginClearEpoch();
|
|
1787
2206
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1788
2207
|
await this.tagIndex.clear();
|
|
1789
2208
|
this.ttlResolver.clearProfiles();
|
|
@@ -1800,7 +2219,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1800
2219
|
return;
|
|
1801
2220
|
}
|
|
1802
2221
|
await this.awaitStartup("mdelete");
|
|
1803
|
-
const normalizedKeys = keys.map((k) =>
|
|
2222
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1804
2223
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1805
2224
|
await this.deleteKeys(cacheKeys);
|
|
1806
2225
|
await this.publishInvalidation({
|
|
@@ -1817,7 +2236,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1817
2236
|
}
|
|
1818
2237
|
const normalizedEntries = entries.map((entry) => ({
|
|
1819
2238
|
...entry,
|
|
1820
|
-
key: this.qualifyKey(
|
|
2239
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1821
2240
|
}));
|
|
1822
2241
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1823
2242
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1826,7 +2245,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1826
2245
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1827
2246
|
return Promise.all(
|
|
1828
2247
|
normalizedEntries.map((entry) => {
|
|
1829
|
-
const optionsSignature =
|
|
2248
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1830
2249
|
const existing = pendingReads.get(entry.key);
|
|
1831
2250
|
if (!existing) {
|
|
1832
2251
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1895,7 +2314,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1895
2314
|
this.assertActive("mset");
|
|
1896
2315
|
const normalizedEntries = entries.map((entry) => ({
|
|
1897
2316
|
...entry,
|
|
1898
|
-
key: this.qualifyKey(
|
|
2317
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1899
2318
|
}));
|
|
1900
2319
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1901
2320
|
await this.awaitStartup("mset");
|
|
@@ -1938,7 +2357,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1938
2357
|
*/
|
|
1939
2358
|
wrap(prefix, fetcher, options = {}) {
|
|
1940
2359
|
return (...args) => {
|
|
1941
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2360
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1942
2361
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1943
2362
|
return this.get(key, () => fetcher(...args), options);
|
|
1944
2363
|
};
|
|
@@ -1948,11 +2367,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1948
2367
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1949
2368
|
*/
|
|
1950
2369
|
namespace(prefix) {
|
|
2370
|
+
validateNamespaceKey(prefix);
|
|
1951
2371
|
return new CacheNamespace(this, prefix);
|
|
1952
2372
|
}
|
|
1953
2373
|
async invalidateByTag(tag) {
|
|
2374
|
+
validateTag(tag);
|
|
1954
2375
|
await this.awaitStartup("invalidateByTag");
|
|
1955
|
-
const keys = await this.
|
|
2376
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1956
2377
|
await this.deleteKeys(keys);
|
|
1957
2378
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1958
2379
|
}
|
|
@@ -1960,23 +2381,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1960
2381
|
if (tags.length === 0) {
|
|
1961
2382
|
return;
|
|
1962
2383
|
}
|
|
2384
|
+
validateTags(tags);
|
|
1963
2385
|
await this.awaitStartup("invalidateByTags");
|
|
1964
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2386
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1965
2387
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2388
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1966
2389
|
await this.deleteKeys(keys);
|
|
1967
2390
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1968
2391
|
}
|
|
1969
2392
|
async invalidateByPattern(pattern) {
|
|
1970
|
-
|
|
2393
|
+
validatePattern(pattern);
|
|
1971
2394
|
await this.awaitStartup("invalidateByPattern");
|
|
1972
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2395
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2396
|
+
this.qualifyPattern(pattern),
|
|
2397
|
+
this.invalidationMaxKeys()
|
|
2398
|
+
);
|
|
1973
2399
|
await this.deleteKeys(keys);
|
|
1974
2400
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1975
2401
|
}
|
|
1976
2402
|
async invalidateByPrefix(prefix) {
|
|
1977
2403
|
await this.awaitStartup("invalidateByPrefix");
|
|
1978
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1979
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2404
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2405
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1980
2406
|
await this.deleteKeys(keys);
|
|
1981
2407
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1982
2408
|
}
|
|
@@ -2046,7 +2472,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2046
2472
|
* Returns `null` if the key does not exist in any layer.
|
|
2047
2473
|
*/
|
|
2048
2474
|
async inspect(key) {
|
|
2049
|
-
const userKey =
|
|
2475
|
+
const userKey = validateCacheKey(key);
|
|
2050
2476
|
const normalizedKey = this.qualifyKey(userKey);
|
|
2051
2477
|
await this.awaitStartup("inspect");
|
|
2052
2478
|
const foundInLayers = [];
|
|
@@ -2083,50 +2509,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2083
2509
|
}
|
|
2084
2510
|
async exportState() {
|
|
2085
2511
|
await this.awaitStartup("exportState");
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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()];
|
|
2512
|
+
const entries = [];
|
|
2513
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2514
|
+
entries.push(entry);
|
|
2515
|
+
});
|
|
2516
|
+
return entries;
|
|
2109
2517
|
}
|
|
2110
2518
|
async importState(entries) {
|
|
2111
2519
|
await this.awaitStartup("importState");
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
+
}
|
|
2119
2534
|
}
|
|
2120
2535
|
async persistToFile(filePath) {
|
|
2121
2536
|
this.assertActive("persistToFile");
|
|
2122
|
-
const snapshot = await this.exportState();
|
|
2123
2537
|
const { promises: fs } = await import("fs");
|
|
2124
|
-
|
|
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
|
+
}
|
|
2125
2564
|
}
|
|
2126
2565
|
async restoreFromFile(filePath) {
|
|
2127
2566
|
this.assertActive("restoreFromFile");
|
|
2128
|
-
const { promises: fs } = await import("fs");
|
|
2129
|
-
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
|
+
}
|
|
2130
2585
|
let parsed;
|
|
2131
2586
|
try {
|
|
2132
2587
|
parsed = JSON.parse(raw);
|
|
@@ -2170,14 +2625,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2170
2625
|
await this.handleInvalidationMessage(message);
|
|
2171
2626
|
});
|
|
2172
2627
|
}
|
|
2173
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2628
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2174
2629
|
const fetchTask = async () => {
|
|
2175
2630
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2176
2631
|
if (secondHit.found) {
|
|
2177
2632
|
this.metricsCollector.increment("hits");
|
|
2178
2633
|
return secondHit.value;
|
|
2179
2634
|
}
|
|
2180
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2635
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2181
2636
|
};
|
|
2182
2637
|
const singleFlightTask = async () => {
|
|
2183
2638
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -2187,7 +2642,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2187
2642
|
key,
|
|
2188
2643
|
this.resolveSingleFlightOptions(),
|
|
2189
2644
|
fetchTask,
|
|
2190
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2645
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2191
2646
|
);
|
|
2192
2647
|
};
|
|
2193
2648
|
if (this.options.stampedePrevention === false) {
|
|
@@ -2195,7 +2650,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2195
2650
|
}
|
|
2196
2651
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2197
2652
|
}
|
|
2198
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2653
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2199
2654
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2200
2655
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2201
2656
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2209,9 +2664,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2209
2664
|
}
|
|
2210
2665
|
await this.sleep(pollIntervalMs);
|
|
2211
2666
|
}
|
|
2212
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2667
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2213
2668
|
}
|
|
2214
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2669
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2215
2670
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2216
2671
|
this.metricsCollector.increment("fetches");
|
|
2217
2672
|
const fetchStart = Date.now();
|
|
@@ -2232,6 +2687,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2232
2687
|
if (!this.shouldNegativeCache(options)) {
|
|
2233
2688
|
return null;
|
|
2234
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
|
+
}
|
|
2235
2700
|
await this.storeEntry(key, "empty", null, options);
|
|
2236
2701
|
return null;
|
|
2237
2702
|
}
|
|
@@ -2244,11 +2709,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2244
2709
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2245
2710
|
}
|
|
2246
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
|
+
}
|
|
2247
2722
|
await this.storeEntry(key, "value", fetched, options);
|
|
2248
2723
|
return fetched;
|
|
2249
2724
|
}
|
|
2250
2725
|
async storeEntry(key, kind, value, options) {
|
|
2726
|
+
const clearEpoch = this.clearEpoch;
|
|
2727
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2251
2728
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2729
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2252
2732
|
if (options?.tags) {
|
|
2253
2733
|
await this.tagIndex.track(key, options.tags);
|
|
2254
2734
|
} else {
|
|
@@ -2263,6 +2743,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2263
2743
|
}
|
|
2264
2744
|
async writeBatch(entries) {
|
|
2265
2745
|
const now = Date.now();
|
|
2746
|
+
const clearEpoch = this.clearEpoch;
|
|
2747
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2266
2748
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2267
2749
|
const immediateOperations = [];
|
|
2268
2750
|
const deferredOperations = [];
|
|
@@ -2279,12 +2761,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2279
2761
|
}
|
|
2280
2762
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2281
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
|
+
}
|
|
2282
2773
|
try {
|
|
2283
2774
|
if (layer.setMany) {
|
|
2284
|
-
await layer.setMany(
|
|
2775
|
+
await layer.setMany(activeEntries);
|
|
2285
2776
|
return;
|
|
2286
2777
|
}
|
|
2287
|
-
await Promise.all(
|
|
2778
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2288
2779
|
} catch (error) {
|
|
2289
2780
|
await this.handleLayerFailure(layer, "write", error);
|
|
2290
2781
|
}
|
|
@@ -2297,7 +2788,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2297
2788
|
}
|
|
2298
2789
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2299
2790
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2791
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2300
2794
|
for (const entry of entries) {
|
|
2795
|
+
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2301
2798
|
if (entry.options?.tags) {
|
|
2302
2799
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2303
2800
|
} else {
|
|
@@ -2399,10 +2896,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2399
2896
|
}
|
|
2400
2897
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2401
2898
|
const now = Date.now();
|
|
2899
|
+
const clearEpoch = this.clearEpoch;
|
|
2900
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2402
2901
|
const immediateOperations = [];
|
|
2403
2902
|
const deferredOperations = [];
|
|
2404
2903
|
for (const layer of this.layers) {
|
|
2405
2904
|
const operation = async () => {
|
|
2905
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2406
2908
|
if (this.shouldSkipLayer(layer)) {
|
|
2407
2909
|
return;
|
|
2408
2910
|
}
|
|
@@ -2466,10 +2968,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2466
2968
|
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
2467
2969
|
return;
|
|
2468
2970
|
}
|
|
2971
|
+
const clearEpoch = this.clearEpoch;
|
|
2972
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2469
2973
|
const refresh = (async () => {
|
|
2470
2974
|
this.metricsCollector.increment("refreshes");
|
|
2471
2975
|
try {
|
|
2472
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2976
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2473
2977
|
} catch (error) {
|
|
2474
2978
|
this.metricsCollector.increment("refreshErrors");
|
|
2475
2979
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2479,14 +2983,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2479
2983
|
})();
|
|
2480
2984
|
this.backgroundRefreshes.set(key, refresh);
|
|
2481
2985
|
}
|
|
2482
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2986
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2483
2987
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2484
2988
|
await this.fetchWithGuards(
|
|
2485
2989
|
key,
|
|
2486
2990
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2487
2991
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2488
2992
|
}),
|
|
2489
|
-
options
|
|
2993
|
+
options,
|
|
2994
|
+
expectedClearEpoch,
|
|
2995
|
+
expectedKeyEpoch
|
|
2490
2996
|
);
|
|
2491
2997
|
}
|
|
2492
2998
|
resolveSingleFlightOptions() {
|
|
@@ -2501,6 +3007,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2501
3007
|
if (keys.length === 0) {
|
|
2502
3008
|
return;
|
|
2503
3009
|
}
|
|
3010
|
+
this.bumpKeyEpochs(keys);
|
|
2504
3011
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2505
3012
|
for (const key of keys) {
|
|
2506
3013
|
await this.tagIndex.remove(key);
|
|
@@ -2523,21 +3030,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2523
3030
|
return;
|
|
2524
3031
|
}
|
|
2525
3032
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2526
|
-
if (localLayers.length === 0) {
|
|
2527
|
-
return;
|
|
2528
|
-
}
|
|
2529
3033
|
if (message.scope === "clear") {
|
|
3034
|
+
this.beginClearEpoch();
|
|
2530
3035
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2531
3036
|
await this.tagIndex.clear();
|
|
2532
3037
|
this.ttlResolver.clearProfiles();
|
|
3038
|
+
this.circuitBreakerManager.clear();
|
|
2533
3039
|
return;
|
|
2534
3040
|
}
|
|
2535
3041
|
const keys = message.keys ?? [];
|
|
3042
|
+
this.bumpKeyEpochs(keys);
|
|
2536
3043
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2537
3044
|
if (message.operation !== "write") {
|
|
2538
3045
|
for (const key of keys) {
|
|
2539
3046
|
await this.tagIndex.remove(key);
|
|
2540
3047
|
this.ttlResolver.deleteProfile(key);
|
|
3048
|
+
this.circuitBreakerManager.delete(key);
|
|
2541
3049
|
}
|
|
2542
3050
|
}
|
|
2543
3051
|
}
|
|
@@ -2643,6 +3151,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2643
3151
|
shouldWriteBehind(layer) {
|
|
2644
3152
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2645
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
|
+
}
|
|
2646
3176
|
async enqueueWriteBehind(operation) {
|
|
2647
3177
|
this.writeBehindQueue.push(operation);
|
|
2648
3178
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
@@ -2769,118 +3299,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2769
3299
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2770
3300
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2771
3301
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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);
|
|
2785
3324
|
if (typeof this.options.generationCleanup === "object") {
|
|
2786
|
-
|
|
3325
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2787
3326
|
}
|
|
2788
3327
|
if (this.options.generation !== void 0) {
|
|
2789
|
-
|
|
3328
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2790
3329
|
}
|
|
2791
3330
|
}
|
|
2792
3331
|
validateWriteOptions(options) {
|
|
2793
3332
|
if (!options) {
|
|
2794
3333
|
return;
|
|
2795
3334
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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.`);
|
|
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);
|
|
2884
3346
|
}
|
|
2885
3347
|
assertActive(operation) {
|
|
2886
3348
|
if (this.isDisconnecting) {
|
|
@@ -2892,24 +3354,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2892
3354
|
await this.startup;
|
|
2893
3355
|
this.assertActive(operation);
|
|
2894
3356
|
}
|
|
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
3357
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2914
3358
|
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
2915
3359
|
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
@@ -2977,18 +3421,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2977
3421
|
this.emit("error", { operation, ...context });
|
|
2978
3422
|
}
|
|
2979
3423
|
}
|
|
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
3424
|
isCacheSnapshotEntries(value) {
|
|
2993
3425
|
return Array.isArray(value) && value.every((entry) => {
|
|
2994
3426
|
if (!entry || typeof entry !== "object") {
|
|
@@ -3001,54 +3433,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3001
3433
|
sanitizeSnapshotValue(value) {
|
|
3002
3434
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3003
3435
|
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
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];
|
|
3010
3453
|
}
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
|
|
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
|
-
}
|
|
3454
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3455
|
+
keys.add(key);
|
|
3456
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3019
3457
|
}
|
|
3020
|
-
return
|
|
3458
|
+
return [...keys];
|
|
3021
3459
|
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
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}).`);
|
|
3025
3464
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
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;
|
|
3030
3476
|
}
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
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
|
+
}
|
|
3034
3499
|
}
|
|
3035
|
-
return value;
|
|
3036
3500
|
}
|
|
3037
3501
|
};
|
|
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
3502
|
|
|
3053
3503
|
// src/module.ts
|
|
3054
3504
|
var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);
|