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
|
@@ -228,22 +228,23 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
228
228
|
constructor(cache, prefix) {
|
|
229
229
|
this.cache = cache;
|
|
230
230
|
this.prefix = prefix;
|
|
231
|
+
validateNamespaceKey(prefix);
|
|
231
232
|
}
|
|
232
233
|
cache;
|
|
233
234
|
prefix;
|
|
234
235
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
235
236
|
metrics = emptyMetrics();
|
|
236
237
|
async get(key, fetcher, options) {
|
|
237
|
-
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
238
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
238
239
|
}
|
|
239
240
|
async getOrSet(key, fetcher, options) {
|
|
240
|
-
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
241
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
241
242
|
}
|
|
242
243
|
/**
|
|
243
244
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
244
245
|
*/
|
|
245
246
|
async getOrThrow(key, fetcher, options) {
|
|
246
|
-
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
247
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
247
248
|
}
|
|
248
249
|
async has(key) {
|
|
249
250
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
@@ -252,7 +253,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
252
253
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
253
254
|
}
|
|
254
255
|
async set(key, value, options) {
|
|
255
|
-
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
256
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
256
257
|
}
|
|
257
258
|
async delete(key) {
|
|
258
259
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
@@ -268,7 +269,8 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
268
269
|
() => this.cache.mget(
|
|
269
270
|
entries.map((entry) => ({
|
|
270
271
|
...entry,
|
|
271
|
-
key: this.qualify(entry.key)
|
|
272
|
+
key: this.qualify(entry.key),
|
|
273
|
+
options: this.qualifyGetOptions(entry.options)
|
|
272
274
|
}))
|
|
273
275
|
)
|
|
274
276
|
);
|
|
@@ -278,16 +280,22 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
278
280
|
() => this.cache.mset(
|
|
279
281
|
entries.map((entry) => ({
|
|
280
282
|
...entry,
|
|
281
|
-
key: this.qualify(entry.key)
|
|
283
|
+
key: this.qualify(entry.key),
|
|
284
|
+
options: this.qualifyWriteOptions(entry.options)
|
|
282
285
|
}))
|
|
283
286
|
)
|
|
284
287
|
);
|
|
285
288
|
}
|
|
286
289
|
async invalidateByTag(tag) {
|
|
287
|
-
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
290
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
288
291
|
}
|
|
289
292
|
async invalidateByTags(tags, mode = "any") {
|
|
290
|
-
await this.trackMetrics(
|
|
293
|
+
await this.trackMetrics(
|
|
294
|
+
() => this.cache.invalidateByTags(
|
|
295
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
296
|
+
mode
|
|
297
|
+
)
|
|
298
|
+
);
|
|
291
299
|
}
|
|
292
300
|
async invalidateByPattern(pattern) {
|
|
293
301
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
@@ -299,16 +307,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
299
307
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
300
308
|
*/
|
|
301
309
|
async inspect(key) {
|
|
302
|
-
|
|
310
|
+
const result = await this.cache.inspect(this.qualify(key));
|
|
311
|
+
if (result === null) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
...result,
|
|
316
|
+
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
317
|
+
};
|
|
303
318
|
}
|
|
304
319
|
wrap(keyPrefix, fetcher, options) {
|
|
305
|
-
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
320
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
306
321
|
}
|
|
307
322
|
warm(entries, options) {
|
|
308
323
|
return this.cache.warm(
|
|
309
324
|
entries.map((entry) => ({
|
|
310
325
|
...entry,
|
|
311
|
-
key: this.qualify(entry.key)
|
|
326
|
+
key: this.qualify(entry.key),
|
|
327
|
+
options: this.qualifyGetOptions(entry.options)
|
|
312
328
|
})),
|
|
313
329
|
options
|
|
314
330
|
);
|
|
@@ -344,6 +360,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
344
360
|
qualify(key) {
|
|
345
361
|
return `${this.prefix}:${key}`;
|
|
346
362
|
}
|
|
363
|
+
qualifyTag(tag) {
|
|
364
|
+
return `${this.prefix}:${tag}`;
|
|
365
|
+
}
|
|
366
|
+
qualifyGetOptions(options) {
|
|
367
|
+
return this.qualifyWriteOptions(options);
|
|
368
|
+
}
|
|
369
|
+
qualifyWrapOptions(options) {
|
|
370
|
+
return this.qualifyWriteOptions(options);
|
|
371
|
+
}
|
|
372
|
+
qualifyWriteOptions(options) {
|
|
373
|
+
if (!options?.tags || options.tags.length === 0) {
|
|
374
|
+
return options;
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
...options,
|
|
378
|
+
tags: options.tags.map((tag) => this.qualifyTag(tag))
|
|
379
|
+
};
|
|
380
|
+
}
|
|
347
381
|
async trackMetrics(operation) {
|
|
348
382
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
349
383
|
const before = this.cache.getMetrics();
|
|
@@ -478,6 +512,9 @@ function validateNamespaceKey(key) {
|
|
|
478
512
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
479
513
|
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
480
514
|
}
|
|
515
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
516
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
517
|
+
}
|
|
481
518
|
}
|
|
482
519
|
|
|
483
520
|
// ../../src/invalidation/PatternMatcher.ts
|
|
@@ -534,21 +571,41 @@ var CacheKeyDiscovery = class {
|
|
|
534
571
|
this.options = options;
|
|
535
572
|
}
|
|
536
573
|
options;
|
|
537
|
-
async collectKeysWithPrefix(prefix) {
|
|
574
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
538
575
|
const { tagIndex } = this.options;
|
|
539
|
-
const matches = new Set(
|
|
540
|
-
|
|
541
|
-
|
|
576
|
+
const matches = /* @__PURE__ */ new Set();
|
|
577
|
+
if (tagIndex.forEachKeyForPrefix) {
|
|
578
|
+
await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
|
|
579
|
+
matches.add(key);
|
|
580
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
581
|
+
});
|
|
582
|
+
} else {
|
|
583
|
+
const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
|
|
584
|
+
for (const key of initialMatches) {
|
|
585
|
+
matches.add(key);
|
|
586
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
542
589
|
await Promise.all(
|
|
543
590
|
this.options.layers.map(async (layer) => {
|
|
544
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
591
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
545
592
|
return;
|
|
546
593
|
}
|
|
547
594
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
595
|
+
if (layer.forEachKey) {
|
|
596
|
+
await layer.forEachKey(async (key) => {
|
|
597
|
+
if (key.startsWith(prefix)) {
|
|
598
|
+
matches.add(key);
|
|
599
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const keys = await layer.keys?.();
|
|
605
|
+
for (const key of keys ?? []) {
|
|
550
606
|
if (key.startsWith(prefix)) {
|
|
551
607
|
matches.add(key);
|
|
608
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
552
609
|
}
|
|
553
610
|
}
|
|
554
611
|
} catch (error) {
|
|
@@ -558,18 +615,39 @@ var CacheKeyDiscovery = class {
|
|
|
558
615
|
);
|
|
559
616
|
return [...matches];
|
|
560
617
|
}
|
|
561
|
-
async collectKeysMatchingPattern(pattern) {
|
|
562
|
-
const matches = new Set(
|
|
618
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
619
|
+
const matches = /* @__PURE__ */ new Set();
|
|
620
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
621
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
622
|
+
matches.add(key);
|
|
623
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
624
|
+
});
|
|
625
|
+
} else {
|
|
626
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
627
|
+
matches.add(key);
|
|
628
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
563
631
|
await Promise.all(
|
|
564
632
|
this.options.layers.map(async (layer) => {
|
|
565
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
633
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
566
634
|
return;
|
|
567
635
|
}
|
|
568
636
|
try {
|
|
569
|
-
|
|
570
|
-
|
|
637
|
+
if (layer.forEachKey) {
|
|
638
|
+
await layer.forEachKey(async (key) => {
|
|
639
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
640
|
+
matches.add(key);
|
|
641
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const keys = await layer.keys?.();
|
|
647
|
+
for (const key of keys ?? []) {
|
|
571
648
|
if (PatternMatcher.matches(pattern, key)) {
|
|
572
649
|
matches.add(key);
|
|
650
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
573
651
|
}
|
|
574
652
|
}
|
|
575
653
|
} catch (error) {
|
|
@@ -579,8 +657,280 @@ var CacheKeyDiscovery = class {
|
|
|
579
657
|
);
|
|
580
658
|
return [...matches];
|
|
581
659
|
}
|
|
660
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
661
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
662
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
582
665
|
};
|
|
583
666
|
|
|
667
|
+
// ../../src/internal/CacheKeySerialization.ts
|
|
668
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
669
|
+
function normalizeForSerialization(value) {
|
|
670
|
+
if (Array.isArray(value)) {
|
|
671
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
672
|
+
}
|
|
673
|
+
if (value && typeof value === "object") {
|
|
674
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
675
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
676
|
+
return normalized;
|
|
677
|
+
}
|
|
678
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
679
|
+
return normalized;
|
|
680
|
+
}, {});
|
|
681
|
+
}
|
|
682
|
+
return value;
|
|
683
|
+
}
|
|
684
|
+
function serializeKeyPart(value) {
|
|
685
|
+
if (typeof value === "string") {
|
|
686
|
+
return `s:${value}`;
|
|
687
|
+
}
|
|
688
|
+
if (typeof value === "number") {
|
|
689
|
+
return `n:${value}`;
|
|
690
|
+
}
|
|
691
|
+
if (typeof value === "boolean") {
|
|
692
|
+
return `b:${value}`;
|
|
693
|
+
}
|
|
694
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
695
|
+
}
|
|
696
|
+
function serializeOptions(options) {
|
|
697
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
698
|
+
}
|
|
699
|
+
function createInstanceId() {
|
|
700
|
+
if (globalThis.crypto?.randomUUID) {
|
|
701
|
+
return globalThis.crypto.randomUUID();
|
|
702
|
+
}
|
|
703
|
+
const bytes = new Uint8Array(16);
|
|
704
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
705
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
706
|
+
} else {
|
|
707
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
708
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
715
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
716
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
717
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
718
|
+
}
|
|
719
|
+
async function findExistingAncestor(directory, fs, path) {
|
|
720
|
+
let current = directory;
|
|
721
|
+
while (true) {
|
|
722
|
+
try {
|
|
723
|
+
await fs.lstat(current);
|
|
724
|
+
return current;
|
|
725
|
+
} catch (error) {
|
|
726
|
+
if (error.code !== "ENOENT") {
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const parent = path.dirname(current);
|
|
731
|
+
if (parent === current) {
|
|
732
|
+
return current;
|
|
733
|
+
}
|
|
734
|
+
current = parent;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
738
|
+
if (filePath.length === 0) {
|
|
739
|
+
throw new Error("filePath must not be empty.");
|
|
740
|
+
}
|
|
741
|
+
if (filePath.includes("\0")) {
|
|
742
|
+
throw new Error("filePath must not contain null bytes.");
|
|
743
|
+
}
|
|
744
|
+
const { promises: fs } = await import("fs");
|
|
745
|
+
const path = await import("path");
|
|
746
|
+
const resolved = path.resolve(filePath);
|
|
747
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
748
|
+
if (baseDir === false) {
|
|
749
|
+
return resolved;
|
|
750
|
+
}
|
|
751
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
752
|
+
const realBaseDir = await fs.realpath(baseDir);
|
|
753
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
754
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
755
|
+
}
|
|
756
|
+
if (mode === "read") {
|
|
757
|
+
const realTarget = await fs.realpath(resolved);
|
|
758
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
759
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
760
|
+
}
|
|
761
|
+
return realTarget;
|
|
762
|
+
}
|
|
763
|
+
const parentDir = path.dirname(resolved);
|
|
764
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs, path);
|
|
765
|
+
const realExistingAncestor = await fs.realpath(existingAncestor);
|
|
766
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
767
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
768
|
+
}
|
|
769
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
770
|
+
const realParentDir = await fs.realpath(parentDir);
|
|
771
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
772
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
773
|
+
}
|
|
774
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
775
|
+
try {
|
|
776
|
+
const existing = await fs.lstat(targetPath);
|
|
777
|
+
if (existing.isSymbolicLink()) {
|
|
778
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
779
|
+
}
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (error.code !== "ENOENT") {
|
|
782
|
+
throw error;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return targetPath;
|
|
786
|
+
}
|
|
787
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
788
|
+
if (byteLimit === false) {
|
|
789
|
+
return handle.readFile({ encoding: "utf8" });
|
|
790
|
+
}
|
|
791
|
+
const chunks = [];
|
|
792
|
+
let totalBytes = 0;
|
|
793
|
+
let position = 0;
|
|
794
|
+
while (true) {
|
|
795
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
796
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
797
|
+
if (bytesRead === 0) {
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
totalBytes += bytesRead;
|
|
801
|
+
if (totalBytes > byteLimit) {
|
|
802
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
803
|
+
}
|
|
804
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
805
|
+
position += bytesRead;
|
|
806
|
+
}
|
|
807
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ../../src/internal/CacheStackValidation.ts
|
|
811
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
812
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
813
|
+
var MAX_TAGS_PER_OPERATION = 128;
|
|
814
|
+
function validatePositiveNumber(name, value) {
|
|
815
|
+
if (value === void 0) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
819
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function validateNonNegativeNumber(name, value) {
|
|
823
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
824
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function validateLayerNumberOption(name, value) {
|
|
828
|
+
if (value === void 0) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (typeof value === "number") {
|
|
832
|
+
validateNonNegativeNumber(name, value);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
836
|
+
if (layerValue === void 0) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function validateRateLimitOptions(name, options) {
|
|
843
|
+
if (!options) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
847
|
+
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
848
|
+
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
849
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
850
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
851
|
+
}
|
|
852
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
853
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function validateCacheKey(key) {
|
|
857
|
+
if (key.length === 0) {
|
|
858
|
+
throw new Error("Cache key must not be empty.");
|
|
859
|
+
}
|
|
860
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
861
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
862
|
+
}
|
|
863
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
864
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
865
|
+
}
|
|
866
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
867
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
868
|
+
}
|
|
869
|
+
return key;
|
|
870
|
+
}
|
|
871
|
+
function validateTag(tag) {
|
|
872
|
+
if (tag.length === 0) {
|
|
873
|
+
throw new Error("Cache tag must not be empty.");
|
|
874
|
+
}
|
|
875
|
+
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
876
|
+
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
877
|
+
}
|
|
878
|
+
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
879
|
+
throw new Error("Cache tag contains unsupported control characters.");
|
|
880
|
+
}
|
|
881
|
+
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
882
|
+
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
883
|
+
}
|
|
884
|
+
return tag;
|
|
885
|
+
}
|
|
886
|
+
function validateTags(tags) {
|
|
887
|
+
if (!tags) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
891
|
+
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
892
|
+
}
|
|
893
|
+
for (const tag of tags) {
|
|
894
|
+
validateTag(tag);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function validatePattern(pattern) {
|
|
898
|
+
if (pattern.length === 0) {
|
|
899
|
+
throw new Error("Pattern must not be empty.");
|
|
900
|
+
}
|
|
901
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
902
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
903
|
+
}
|
|
904
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
905
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function validateTtlPolicy(name, policy) {
|
|
909
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if ("alignTo" in policy) {
|
|
913
|
+
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
throw new Error(`${name} is invalid.`);
|
|
917
|
+
}
|
|
918
|
+
function validateAdaptiveTtlOptions(options) {
|
|
919
|
+
if (!options || options === true) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
923
|
+
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
924
|
+
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
925
|
+
}
|
|
926
|
+
function validateCircuitBreakerOptions(options) {
|
|
927
|
+
if (!options) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
931
|
+
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
932
|
+
}
|
|
933
|
+
|
|
584
934
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
585
935
|
var CircuitBreakerManager = class {
|
|
586
936
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -974,19 +1324,47 @@ function isStoredValueEnvelope(value) {
|
|
|
974
1324
|
if (v.kind !== "value" && v.kind !== "empty") {
|
|
975
1325
|
return false;
|
|
976
1326
|
}
|
|
977
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
1327
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
978
1328
|
return false;
|
|
979
1329
|
}
|
|
980
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
1330
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
981
1331
|
return false;
|
|
982
1332
|
}
|
|
983
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
1333
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
984
1334
|
return false;
|
|
985
1335
|
}
|
|
986
1336
|
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
987
1337
|
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
988
1338
|
return false;
|
|
989
1339
|
}
|
|
1340
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
1350
|
+
return false;
|
|
1351
|
+
}
|
|
1352
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1356
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
990
1368
|
return true;
|
|
991
1369
|
}
|
|
992
1370
|
function createStoredValueEnvelope(options) {
|
|
@@ -1085,6 +1463,12 @@ function normalizePositiveSeconds(value) {
|
|
|
1085
1463
|
}
|
|
1086
1464
|
return value;
|
|
1087
1465
|
}
|
|
1466
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1467
|
+
if (value == null) {
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1471
|
+
}
|
|
1088
1472
|
|
|
1089
1473
|
// ../../src/internal/TtlResolver.ts
|
|
1090
1474
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1239,6 +1623,11 @@ var TagIndex = class {
|
|
|
1239
1623
|
async keysForTag(tag) {
|
|
1240
1624
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1241
1625
|
}
|
|
1626
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1627
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1628
|
+
await visitor(key);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1242
1631
|
async keysForPrefix(prefix) {
|
|
1243
1632
|
const node = this.findNode(prefix);
|
|
1244
1633
|
if (!node) {
|
|
@@ -1248,6 +1637,13 @@ var TagIndex = class {
|
|
|
1248
1637
|
this.collectFromNode(node, prefix, matches);
|
|
1249
1638
|
return matches;
|
|
1250
1639
|
}
|
|
1640
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
1641
|
+
const node = this.findNode(prefix);
|
|
1642
|
+
if (!node) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
await this.visitFromNode(node, prefix, visitor);
|
|
1646
|
+
}
|
|
1251
1647
|
async tagsForKey(key) {
|
|
1252
1648
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1253
1649
|
}
|
|
@@ -1256,6 +1652,12 @@ var TagIndex = class {
|
|
|
1256
1652
|
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1257
1653
|
return [...matches];
|
|
1258
1654
|
}
|
|
1655
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
1656
|
+
const matches = await this.matchPattern(pattern);
|
|
1657
|
+
for (const key of matches) {
|
|
1658
|
+
await visitor(key);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1259
1661
|
async clear() {
|
|
1260
1662
|
this.tagToKeys.clear();
|
|
1261
1663
|
this.keyToTags.clear();
|
|
@@ -1305,6 +1707,14 @@ var TagIndex = class {
|
|
|
1305
1707
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1306
1708
|
}
|
|
1307
1709
|
}
|
|
1710
|
+
async visitFromNode(node, prefix, visitor) {
|
|
1711
|
+
if (node.terminal) {
|
|
1712
|
+
await visitor(prefix);
|
|
1713
|
+
}
|
|
1714
|
+
for (const [character, child] of node.children) {
|
|
1715
|
+
await this.visitFromNode(child, `${prefix}${character}`, visitor);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1308
1718
|
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1309
1719
|
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1310
1720
|
return;
|
|
@@ -1422,22 +1832,27 @@ var TagIndex = class {
|
|
|
1422
1832
|
|
|
1423
1833
|
// ../../src/serialization/JsonSerializer.ts
|
|
1424
1834
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1835
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1425
1836
|
var JsonSerializer = class {
|
|
1426
1837
|
serialize(value) {
|
|
1427
1838
|
return JSON.stringify(value);
|
|
1428
1839
|
}
|
|
1429
1840
|
deserialize(payload) {
|
|
1430
1841
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1431
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1842
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1432
1843
|
}
|
|
1433
1844
|
};
|
|
1434
1845
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1435
|
-
function sanitizeJsonValue(value, depth) {
|
|
1846
|
+
function sanitizeJsonValue(value, depth, state) {
|
|
1847
|
+
state.count += 1;
|
|
1848
|
+
if (state.count > MAX_SANITIZE_NODES) {
|
|
1849
|
+
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
1850
|
+
}
|
|
1436
1851
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1437
|
-
|
|
1852
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1438
1853
|
}
|
|
1439
1854
|
if (Array.isArray(value)) {
|
|
1440
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1855
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1441
1856
|
}
|
|
1442
1857
|
if (!isPlainObject(value)) {
|
|
1443
1858
|
return value;
|
|
@@ -1447,7 +1862,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1447
1862
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1448
1863
|
continue;
|
|
1449
1864
|
}
|
|
1450
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1865
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1451
1866
|
}
|
|
1452
1867
|
return sanitized;
|
|
1453
1868
|
}
|
|
@@ -1496,10 +1911,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1496
1911
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1497
1912
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1498
1913
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1499
|
-
var
|
|
1500
|
-
var
|
|
1914
|
+
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1915
|
+
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1916
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1917
|
+
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1501
1918
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1502
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1503
1919
|
var DebugLogger = class {
|
|
1504
1920
|
enabled;
|
|
1505
1921
|
constructor(enabled) {
|
|
@@ -1586,6 +2002,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1586
2002
|
snapshotSerializer = new JsonSerializer();
|
|
1587
2003
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1588
2004
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2005
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1589
2006
|
ttlResolver;
|
|
1590
2007
|
circuitBreakerManager;
|
|
1591
2008
|
currentGeneration;
|
|
@@ -1593,6 +2010,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1593
2010
|
writeBehindTimer;
|
|
1594
2011
|
writeBehindFlushPromise;
|
|
1595
2012
|
generationCleanupPromise;
|
|
2013
|
+
clearEpoch = 0;
|
|
1596
2014
|
isDisconnecting = false;
|
|
1597
2015
|
disconnectPromise;
|
|
1598
2016
|
/**
|
|
@@ -1602,7 +2020,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1602
2020
|
* and no `fetcher` is provided.
|
|
1603
2021
|
*/
|
|
1604
2022
|
async get(key, fetcher, options) {
|
|
1605
|
-
const normalizedKey = this.qualifyKey(
|
|
2023
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1606
2024
|
this.validateWriteOptions(options);
|
|
1607
2025
|
await this.awaitStartup("get");
|
|
1608
2026
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1672,7 +2090,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1672
2090
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1673
2091
|
*/
|
|
1674
2092
|
async has(key) {
|
|
1675
|
-
const normalizedKey = this.qualifyKey(
|
|
2093
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1676
2094
|
await this.awaitStartup("has");
|
|
1677
2095
|
for (const layer of this.layers) {
|
|
1678
2096
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1705,7 +2123,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1705
2123
|
* that has it, or null if the key is not found / has no TTL.
|
|
1706
2124
|
*/
|
|
1707
2125
|
async ttl(key) {
|
|
1708
|
-
const normalizedKey = this.qualifyKey(
|
|
2126
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1709
2127
|
await this.awaitStartup("ttl");
|
|
1710
2128
|
for (const layer of this.layers) {
|
|
1711
2129
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1727,7 +2145,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1727
2145
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1728
2146
|
*/
|
|
1729
2147
|
async set(key, value, options) {
|
|
1730
|
-
const normalizedKey = this.qualifyKey(
|
|
2148
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1731
2149
|
this.validateWriteOptions(options);
|
|
1732
2150
|
await this.awaitStartup("set");
|
|
1733
2151
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1736,7 +2154,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1736
2154
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1737
2155
|
*/
|
|
1738
2156
|
async delete(key) {
|
|
1739
|
-
const normalizedKey = this.qualifyKey(
|
|
2157
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1740
2158
|
await this.awaitStartup("delete");
|
|
1741
2159
|
await this.deleteKeys([normalizedKey]);
|
|
1742
2160
|
await this.publishInvalidation({
|
|
@@ -1748,6 +2166,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1748
2166
|
}
|
|
1749
2167
|
async clear() {
|
|
1750
2168
|
await this.awaitStartup("clear");
|
|
2169
|
+
this.beginClearEpoch();
|
|
1751
2170
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1752
2171
|
await this.tagIndex.clear();
|
|
1753
2172
|
this.ttlResolver.clearProfiles();
|
|
@@ -1764,7 +2183,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1764
2183
|
return;
|
|
1765
2184
|
}
|
|
1766
2185
|
await this.awaitStartup("mdelete");
|
|
1767
|
-
const normalizedKeys = keys.map((k) =>
|
|
2186
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1768
2187
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1769
2188
|
await this.deleteKeys(cacheKeys);
|
|
1770
2189
|
await this.publishInvalidation({
|
|
@@ -1781,7 +2200,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1781
2200
|
}
|
|
1782
2201
|
const normalizedEntries = entries.map((entry) => ({
|
|
1783
2202
|
...entry,
|
|
1784
|
-
key: this.qualifyKey(
|
|
2203
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1785
2204
|
}));
|
|
1786
2205
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1787
2206
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1790,7 +2209,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1790
2209
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1791
2210
|
return Promise.all(
|
|
1792
2211
|
normalizedEntries.map((entry) => {
|
|
1793
|
-
const optionsSignature =
|
|
2212
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1794
2213
|
const existing = pendingReads.get(entry.key);
|
|
1795
2214
|
if (!existing) {
|
|
1796
2215
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1859,7 +2278,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1859
2278
|
this.assertActive("mset");
|
|
1860
2279
|
const normalizedEntries = entries.map((entry) => ({
|
|
1861
2280
|
...entry,
|
|
1862
|
-
key: this.qualifyKey(
|
|
2281
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1863
2282
|
}));
|
|
1864
2283
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1865
2284
|
await this.awaitStartup("mset");
|
|
@@ -1902,7 +2321,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1902
2321
|
*/
|
|
1903
2322
|
wrap(prefix, fetcher, options = {}) {
|
|
1904
2323
|
return (...args) => {
|
|
1905
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2324
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1906
2325
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1907
2326
|
return this.get(key, () => fetcher(...args), options);
|
|
1908
2327
|
};
|
|
@@ -1912,11 +2331,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1912
2331
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1913
2332
|
*/
|
|
1914
2333
|
namespace(prefix) {
|
|
2334
|
+
validateNamespaceKey(prefix);
|
|
1915
2335
|
return new CacheNamespace(this, prefix);
|
|
1916
2336
|
}
|
|
1917
2337
|
async invalidateByTag(tag) {
|
|
2338
|
+
validateTag(tag);
|
|
1918
2339
|
await this.awaitStartup("invalidateByTag");
|
|
1919
|
-
const keys = await this.
|
|
2340
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1920
2341
|
await this.deleteKeys(keys);
|
|
1921
2342
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1922
2343
|
}
|
|
@@ -1924,23 +2345,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1924
2345
|
if (tags.length === 0) {
|
|
1925
2346
|
return;
|
|
1926
2347
|
}
|
|
2348
|
+
validateTags(tags);
|
|
1927
2349
|
await this.awaitStartup("invalidateByTags");
|
|
1928
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2350
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1929
2351
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2352
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1930
2353
|
await this.deleteKeys(keys);
|
|
1931
2354
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1932
2355
|
}
|
|
1933
2356
|
async invalidateByPattern(pattern) {
|
|
1934
|
-
|
|
2357
|
+
validatePattern(pattern);
|
|
1935
2358
|
await this.awaitStartup("invalidateByPattern");
|
|
1936
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2359
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2360
|
+
this.qualifyPattern(pattern),
|
|
2361
|
+
this.invalidationMaxKeys()
|
|
2362
|
+
);
|
|
1937
2363
|
await this.deleteKeys(keys);
|
|
1938
2364
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1939
2365
|
}
|
|
1940
2366
|
async invalidateByPrefix(prefix) {
|
|
1941
2367
|
await this.awaitStartup("invalidateByPrefix");
|
|
1942
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1943
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2368
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2369
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1944
2370
|
await this.deleteKeys(keys);
|
|
1945
2371
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1946
2372
|
}
|
|
@@ -2010,7 +2436,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2010
2436
|
* Returns `null` if the key does not exist in any layer.
|
|
2011
2437
|
*/
|
|
2012
2438
|
async inspect(key) {
|
|
2013
|
-
const userKey =
|
|
2439
|
+
const userKey = validateCacheKey(key);
|
|
2014
2440
|
const normalizedKey = this.qualifyKey(userKey);
|
|
2015
2441
|
await this.awaitStartup("inspect");
|
|
2016
2442
|
const foundInLayers = [];
|
|
@@ -2047,50 +2473,79 @@ var CacheStack = class extends EventEmitter {
|
|
|
2047
2473
|
}
|
|
2048
2474
|
async exportState() {
|
|
2049
2475
|
await this.awaitStartup("exportState");
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
const keys = await layer.keys();
|
|
2056
|
-
for (const key of keys) {
|
|
2057
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
2058
|
-
if (exported.has(exportedKey)) {
|
|
2059
|
-
continue;
|
|
2060
|
-
}
|
|
2061
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2062
|
-
if (stored === null) {
|
|
2063
|
-
continue;
|
|
2064
|
-
}
|
|
2065
|
-
exported.set(exportedKey, {
|
|
2066
|
-
key: exportedKey,
|
|
2067
|
-
value: stored,
|
|
2068
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
2069
|
-
});
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
return [...exported.values()];
|
|
2476
|
+
const entries = [];
|
|
2477
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2478
|
+
entries.push(entry);
|
|
2479
|
+
});
|
|
2480
|
+
return entries;
|
|
2073
2481
|
}
|
|
2074
2482
|
async importState(entries) {
|
|
2075
2483
|
await this.awaitStartup("importState");
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2484
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2485
|
+
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2486
|
+
value: entry.value,
|
|
2487
|
+
ttl: entry.ttl
|
|
2488
|
+
}));
|
|
2489
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2490
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2491
|
+
await Promise.all(
|
|
2492
|
+
batch.map(async (entry) => {
|
|
2493
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2494
|
+
await this.tagIndex.touch(entry.key);
|
|
2495
|
+
})
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2083
2498
|
}
|
|
2084
2499
|
async persistToFile(filePath) {
|
|
2085
2500
|
this.assertActive("persistToFile");
|
|
2086
|
-
const snapshot = await this.exportState();
|
|
2087
2501
|
const { promises: fs } = await import("fs");
|
|
2088
|
-
|
|
2502
|
+
const path = await import("path");
|
|
2503
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2504
|
+
const tempPath = path.join(
|
|
2505
|
+
path.dirname(targetPath),
|
|
2506
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2507
|
+
);
|
|
2508
|
+
let handle;
|
|
2509
|
+
try {
|
|
2510
|
+
handle = await fs.open(tempPath, "wx");
|
|
2511
|
+
const openedHandle = handle;
|
|
2512
|
+
await openedHandle.writeFile("[", "utf8");
|
|
2513
|
+
let wroteAny = false;
|
|
2514
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2515
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2516
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2517
|
+
wroteAny = true;
|
|
2518
|
+
});
|
|
2519
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2520
|
+
await openedHandle.close();
|
|
2521
|
+
handle = void 0;
|
|
2522
|
+
await fs.rename(tempPath, targetPath);
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
await handle?.close().catch(() => void 0);
|
|
2525
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
2526
|
+
throw error;
|
|
2527
|
+
}
|
|
2089
2528
|
}
|
|
2090
2529
|
async restoreFromFile(filePath) {
|
|
2091
2530
|
this.assertActive("restoreFromFile");
|
|
2092
|
-
const { promises: fs } = await import("fs");
|
|
2093
|
-
const
|
|
2531
|
+
const { promises: fs, constants } = await import("fs");
|
|
2532
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2533
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2534
|
+
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2535
|
+
let raw;
|
|
2536
|
+
try {
|
|
2537
|
+
if (snapshotMaxBytes !== false) {
|
|
2538
|
+
const stat = await handle.stat();
|
|
2539
|
+
if (stat.size > snapshotMaxBytes) {
|
|
2540
|
+
throw new Error(
|
|
2541
|
+
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2546
|
+
} finally {
|
|
2547
|
+
await handle.close();
|
|
2548
|
+
}
|
|
2094
2549
|
let parsed;
|
|
2095
2550
|
try {
|
|
2096
2551
|
parsed = JSON.parse(raw);
|
|
@@ -2134,14 +2589,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2134
2589
|
await this.handleInvalidationMessage(message);
|
|
2135
2590
|
});
|
|
2136
2591
|
}
|
|
2137
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2592
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2138
2593
|
const fetchTask = async () => {
|
|
2139
2594
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2140
2595
|
if (secondHit.found) {
|
|
2141
2596
|
this.metricsCollector.increment("hits");
|
|
2142
2597
|
return secondHit.value;
|
|
2143
2598
|
}
|
|
2144
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2599
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2145
2600
|
};
|
|
2146
2601
|
const singleFlightTask = async () => {
|
|
2147
2602
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -2151,7 +2606,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2151
2606
|
key,
|
|
2152
2607
|
this.resolveSingleFlightOptions(),
|
|
2153
2608
|
fetchTask,
|
|
2154
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2609
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2155
2610
|
);
|
|
2156
2611
|
};
|
|
2157
2612
|
if (this.options.stampedePrevention === false) {
|
|
@@ -2159,7 +2614,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2159
2614
|
}
|
|
2160
2615
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2161
2616
|
}
|
|
2162
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2617
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2163
2618
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2164
2619
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2165
2620
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2173,9 +2628,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2173
2628
|
}
|
|
2174
2629
|
await this.sleep(pollIntervalMs);
|
|
2175
2630
|
}
|
|
2176
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2631
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2177
2632
|
}
|
|
2178
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2633
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2179
2634
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2180
2635
|
this.metricsCollector.increment("fetches");
|
|
2181
2636
|
const fetchStart = Date.now();
|
|
@@ -2196,6 +2651,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2196
2651
|
if (!this.shouldNegativeCache(options)) {
|
|
2197
2652
|
return null;
|
|
2198
2653
|
}
|
|
2654
|
+
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2655
|
+
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2656
|
+
key,
|
|
2657
|
+
expectedClearEpoch,
|
|
2658
|
+
clearEpoch: this.clearEpoch,
|
|
2659
|
+
expectedKeyEpoch,
|
|
2660
|
+
keyEpoch: this.currentKeyEpoch(key)
|
|
2661
|
+
});
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2199
2664
|
await this.storeEntry(key, "empty", null, options);
|
|
2200
2665
|
return null;
|
|
2201
2666
|
}
|
|
@@ -2208,11 +2673,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
2208
2673
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2209
2674
|
}
|
|
2210
2675
|
}
|
|
2676
|
+
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2677
|
+
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2678
|
+
key,
|
|
2679
|
+
expectedClearEpoch,
|
|
2680
|
+
clearEpoch: this.clearEpoch,
|
|
2681
|
+
expectedKeyEpoch,
|
|
2682
|
+
keyEpoch: this.currentKeyEpoch(key)
|
|
2683
|
+
});
|
|
2684
|
+
return fetched;
|
|
2685
|
+
}
|
|
2211
2686
|
await this.storeEntry(key, "value", fetched, options);
|
|
2212
2687
|
return fetched;
|
|
2213
2688
|
}
|
|
2214
2689
|
async storeEntry(key, kind, value, options) {
|
|
2690
|
+
const clearEpoch = this.clearEpoch;
|
|
2691
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2215
2692
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2693
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2216
2696
|
if (options?.tags) {
|
|
2217
2697
|
await this.tagIndex.track(key, options.tags);
|
|
2218
2698
|
} else {
|
|
@@ -2227,6 +2707,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
2227
2707
|
}
|
|
2228
2708
|
async writeBatch(entries) {
|
|
2229
2709
|
const now = Date.now();
|
|
2710
|
+
const clearEpoch = this.clearEpoch;
|
|
2711
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2230
2712
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2231
2713
|
const immediateOperations = [];
|
|
2232
2714
|
const deferredOperations = [];
|
|
@@ -2243,12 +2725,21 @@ var CacheStack = class extends EventEmitter {
|
|
|
2243
2725
|
}
|
|
2244
2726
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2245
2727
|
const operation = async () => {
|
|
2728
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
const activeEntries = layerEntries.filter(
|
|
2732
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
|
|
2733
|
+
);
|
|
2734
|
+
if (activeEntries.length === 0) {
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2246
2737
|
try {
|
|
2247
2738
|
if (layer.setMany) {
|
|
2248
|
-
await layer.setMany(
|
|
2739
|
+
await layer.setMany(activeEntries);
|
|
2249
2740
|
return;
|
|
2250
2741
|
}
|
|
2251
|
-
await Promise.all(
|
|
2742
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2252
2743
|
} catch (error) {
|
|
2253
2744
|
await this.handleLayerFailure(layer, "write", error);
|
|
2254
2745
|
}
|
|
@@ -2261,7 +2752,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2261
2752
|
}
|
|
2262
2753
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2263
2754
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2755
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2264
2758
|
for (const entry of entries) {
|
|
2759
|
+
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2265
2762
|
if (entry.options?.tags) {
|
|
2266
2763
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2267
2764
|
} else {
|
|
@@ -2363,10 +2860,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
2363
2860
|
}
|
|
2364
2861
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2365
2862
|
const now = Date.now();
|
|
2863
|
+
const clearEpoch = this.clearEpoch;
|
|
2864
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2366
2865
|
const immediateOperations = [];
|
|
2367
2866
|
const deferredOperations = [];
|
|
2368
2867
|
for (const layer of this.layers) {
|
|
2369
2868
|
const operation = async () => {
|
|
2869
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2370
2872
|
if (this.shouldSkipLayer(layer)) {
|
|
2371
2873
|
return;
|
|
2372
2874
|
}
|
|
@@ -2430,10 +2932,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
2430
2932
|
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
2431
2933
|
return;
|
|
2432
2934
|
}
|
|
2935
|
+
const clearEpoch = this.clearEpoch;
|
|
2936
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2433
2937
|
const refresh = (async () => {
|
|
2434
2938
|
this.metricsCollector.increment("refreshes");
|
|
2435
2939
|
try {
|
|
2436
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2940
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2437
2941
|
} catch (error) {
|
|
2438
2942
|
this.metricsCollector.increment("refreshErrors");
|
|
2439
2943
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2443,14 +2947,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2443
2947
|
})();
|
|
2444
2948
|
this.backgroundRefreshes.set(key, refresh);
|
|
2445
2949
|
}
|
|
2446
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2950
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2447
2951
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2448
2952
|
await this.fetchWithGuards(
|
|
2449
2953
|
key,
|
|
2450
2954
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2451
2955
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2452
2956
|
}),
|
|
2453
|
-
options
|
|
2957
|
+
options,
|
|
2958
|
+
expectedClearEpoch,
|
|
2959
|
+
expectedKeyEpoch
|
|
2454
2960
|
);
|
|
2455
2961
|
}
|
|
2456
2962
|
resolveSingleFlightOptions() {
|
|
@@ -2465,6 +2971,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2465
2971
|
if (keys.length === 0) {
|
|
2466
2972
|
return;
|
|
2467
2973
|
}
|
|
2974
|
+
this.bumpKeyEpochs(keys);
|
|
2468
2975
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2469
2976
|
for (const key of keys) {
|
|
2470
2977
|
await this.tagIndex.remove(key);
|
|
@@ -2487,21 +2994,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
2487
2994
|
return;
|
|
2488
2995
|
}
|
|
2489
2996
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2490
|
-
if (localLayers.length === 0) {
|
|
2491
|
-
return;
|
|
2492
|
-
}
|
|
2493
2997
|
if (message.scope === "clear") {
|
|
2998
|
+
this.beginClearEpoch();
|
|
2494
2999
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2495
3000
|
await this.tagIndex.clear();
|
|
2496
3001
|
this.ttlResolver.clearProfiles();
|
|
3002
|
+
this.circuitBreakerManager.clear();
|
|
2497
3003
|
return;
|
|
2498
3004
|
}
|
|
2499
3005
|
const keys = message.keys ?? [];
|
|
3006
|
+
this.bumpKeyEpochs(keys);
|
|
2500
3007
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2501
3008
|
if (message.operation !== "write") {
|
|
2502
3009
|
for (const key of keys) {
|
|
2503
3010
|
await this.tagIndex.remove(key);
|
|
2504
3011
|
this.ttlResolver.deleteProfile(key);
|
|
3012
|
+
this.circuitBreakerManager.delete(key);
|
|
2505
3013
|
}
|
|
2506
3014
|
}
|
|
2507
3015
|
}
|
|
@@ -2607,6 +3115,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
2607
3115
|
shouldWriteBehind(layer) {
|
|
2608
3116
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2609
3117
|
}
|
|
3118
|
+
beginClearEpoch() {
|
|
3119
|
+
this.clearEpoch += 1;
|
|
3120
|
+
this.keyEpochs.clear();
|
|
3121
|
+
this.writeBehindQueue.length = 0;
|
|
3122
|
+
}
|
|
3123
|
+
currentKeyEpoch(key) {
|
|
3124
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
3125
|
+
}
|
|
3126
|
+
bumpKeyEpochs(keys) {
|
|
3127
|
+
for (const key of keys) {
|
|
3128
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
3132
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
3133
|
+
return true;
|
|
3134
|
+
}
|
|
3135
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
3136
|
+
return true;
|
|
3137
|
+
}
|
|
3138
|
+
return false;
|
|
3139
|
+
}
|
|
2610
3140
|
async enqueueWriteBehind(operation) {
|
|
2611
3141
|
this.writeBehindQueue.push(operation);
|
|
2612
3142
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
@@ -2733,118 +3263,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
2733
3263
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2734
3264
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2735
3265
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
3266
|
+
validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
3267
|
+
validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
3268
|
+
validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
3269
|
+
validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
3270
|
+
validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
3271
|
+
validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
3272
|
+
validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
3273
|
+
validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
3274
|
+
validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
3275
|
+
validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
3276
|
+
if (this.options.snapshotMaxBytes !== false) {
|
|
3277
|
+
validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
|
|
3278
|
+
}
|
|
3279
|
+
if (this.options.snapshotMaxEntries !== false) {
|
|
3280
|
+
validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
|
|
3281
|
+
}
|
|
3282
|
+
if (this.options.invalidationMaxKeys !== false) {
|
|
3283
|
+
validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
|
|
3284
|
+
}
|
|
3285
|
+
validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
3286
|
+
validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
3287
|
+
validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2749
3288
|
if (typeof this.options.generationCleanup === "object") {
|
|
2750
|
-
|
|
3289
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2751
3290
|
}
|
|
2752
3291
|
if (this.options.generation !== void 0) {
|
|
2753
|
-
|
|
3292
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2754
3293
|
}
|
|
2755
3294
|
}
|
|
2756
3295
|
validateWriteOptions(options) {
|
|
2757
3296
|
if (!options) {
|
|
2758
3297
|
return;
|
|
2759
3298
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
validateLayerNumberOption(name, value) {
|
|
2772
|
-
if (value === void 0) {
|
|
2773
|
-
return;
|
|
2774
|
-
}
|
|
2775
|
-
if (typeof value === "number") {
|
|
2776
|
-
this.validateNonNegativeNumber(name, value);
|
|
2777
|
-
return;
|
|
2778
|
-
}
|
|
2779
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2780
|
-
if (layerValue === void 0) {
|
|
2781
|
-
continue;
|
|
2782
|
-
}
|
|
2783
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
validatePositiveNumber(name, value) {
|
|
2787
|
-
if (value === void 0) {
|
|
2788
|
-
return;
|
|
2789
|
-
}
|
|
2790
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2791
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
validateRateLimitOptions(name, options) {
|
|
2795
|
-
if (!options) {
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2799
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2800
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2801
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2802
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2803
|
-
}
|
|
2804
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2805
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
validateNonNegativeNumber(name, value) {
|
|
2809
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2810
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
validateCacheKey(key) {
|
|
2814
|
-
if (key.length === 0) {
|
|
2815
|
-
throw new Error("Cache key must not be empty.");
|
|
2816
|
-
}
|
|
2817
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2818
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2819
|
-
}
|
|
2820
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2821
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2822
|
-
}
|
|
2823
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2824
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2825
|
-
}
|
|
2826
|
-
return key;
|
|
2827
|
-
}
|
|
2828
|
-
validatePattern(pattern) {
|
|
2829
|
-
if (pattern.length === 0) {
|
|
2830
|
-
throw new Error("Pattern must not be empty.");
|
|
2831
|
-
}
|
|
2832
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2833
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2834
|
-
}
|
|
2835
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2836
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
validateTtlPolicy(name, policy) {
|
|
2840
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2841
|
-
return;
|
|
2842
|
-
}
|
|
2843
|
-
if ("alignTo" in policy) {
|
|
2844
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2845
|
-
return;
|
|
2846
|
-
}
|
|
2847
|
-
throw new Error(`${name} is invalid.`);
|
|
3299
|
+
validateLayerNumberOption("options.ttl", options.ttl);
|
|
3300
|
+
validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
3301
|
+
validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
3302
|
+
validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
3303
|
+
validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
3304
|
+
validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
3305
|
+
validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
3306
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
3307
|
+
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3308
|
+
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3309
|
+
validateTags(options.tags);
|
|
2848
3310
|
}
|
|
2849
3311
|
assertActive(operation) {
|
|
2850
3312
|
if (this.isDisconnecting) {
|
|
@@ -2856,24 +3318,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2856
3318
|
await this.startup;
|
|
2857
3319
|
this.assertActive(operation);
|
|
2858
3320
|
}
|
|
2859
|
-
serializeOptions(options) {
|
|
2860
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2861
|
-
}
|
|
2862
|
-
validateAdaptiveTtlOptions(options) {
|
|
2863
|
-
if (!options || options === true) {
|
|
2864
|
-
return;
|
|
2865
|
-
}
|
|
2866
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2867
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2868
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2869
|
-
}
|
|
2870
|
-
validateCircuitBreakerOptions(options) {
|
|
2871
|
-
if (!options) {
|
|
2872
|
-
return;
|
|
2873
|
-
}
|
|
2874
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2875
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2876
|
-
}
|
|
2877
3321
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2878
3322
|
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
2879
3323
|
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
@@ -2941,18 +3385,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2941
3385
|
this.emit("error", { operation, ...context });
|
|
2942
3386
|
}
|
|
2943
3387
|
}
|
|
2944
|
-
serializeKeyPart(value) {
|
|
2945
|
-
if (typeof value === "string") {
|
|
2946
|
-
return `s:${value}`;
|
|
2947
|
-
}
|
|
2948
|
-
if (typeof value === "number") {
|
|
2949
|
-
return `n:${value}`;
|
|
2950
|
-
}
|
|
2951
|
-
if (typeof value === "boolean") {
|
|
2952
|
-
return `b:${value}`;
|
|
2953
|
-
}
|
|
2954
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2955
|
-
}
|
|
2956
3388
|
isCacheSnapshotEntries(value) {
|
|
2957
3389
|
return Array.isArray(value) && value.every((entry) => {
|
|
2958
3390
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2965,54 +3397,72 @@ var CacheStack = class extends EventEmitter {
|
|
|
2965
3397
|
sanitizeSnapshotValue(value) {
|
|
2966
3398
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2967
3399
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3400
|
+
snapshotMaxBytes() {
|
|
3401
|
+
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3402
|
+
}
|
|
3403
|
+
snapshotMaxEntries() {
|
|
3404
|
+
return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
|
|
3405
|
+
}
|
|
3406
|
+
invalidationMaxKeys() {
|
|
3407
|
+
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3408
|
+
}
|
|
3409
|
+
async collectKeysForTag(tag) {
|
|
3410
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3411
|
+
if (this.tagIndex.forEachKeyForTag) {
|
|
3412
|
+
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3413
|
+
keys.add(key);
|
|
3414
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3415
|
+
});
|
|
3416
|
+
return [...keys];
|
|
2974
3417
|
}
|
|
2975
|
-
const
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
if (baseDir !== false) {
|
|
2979
|
-
const relative = path.relative(baseDir, resolved);
|
|
2980
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2981
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2982
|
-
}
|
|
3418
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3419
|
+
keys.add(key);
|
|
3420
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2983
3421
|
}
|
|
2984
|
-
return
|
|
3422
|
+
return [...keys];
|
|
2985
3423
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3424
|
+
assertWithinInvalidationKeyLimit(size) {
|
|
3425
|
+
const maxKeys = this.invalidationMaxKeys();
|
|
3426
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
3427
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2989
3428
|
}
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3429
|
+
}
|
|
3430
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
3431
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3432
|
+
for (const layer of this.layers) {
|
|
3433
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
3434
|
+
continue;
|
|
3435
|
+
}
|
|
3436
|
+
const visitKey = async (key) => {
|
|
3437
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
3438
|
+
if (exported.has(exportedKey)) {
|
|
3439
|
+
return;
|
|
2994
3440
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3441
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
3442
|
+
if (stored === null) {
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
exported.add(exportedKey);
|
|
3446
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3447
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3448
|
+
}
|
|
3449
|
+
await visitor({
|
|
3450
|
+
key: exportedKey,
|
|
3451
|
+
value: stored,
|
|
3452
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
3453
|
+
});
|
|
3454
|
+
};
|
|
3455
|
+
if (layer.forEachKey) {
|
|
3456
|
+
await layer.forEachKey(visitKey);
|
|
3457
|
+
continue;
|
|
3458
|
+
}
|
|
3459
|
+
const keys = await layer.keys?.();
|
|
3460
|
+
for (const key of keys ?? []) {
|
|
3461
|
+
await visitKey(key);
|
|
3462
|
+
}
|
|
2998
3463
|
}
|
|
2999
|
-
return value;
|
|
3000
3464
|
}
|
|
3001
3465
|
};
|
|
3002
|
-
function createInstanceId() {
|
|
3003
|
-
if (globalThis.crypto?.randomUUID) {
|
|
3004
|
-
return globalThis.crypto.randomUUID();
|
|
3005
|
-
}
|
|
3006
|
-
const bytes = new Uint8Array(16);
|
|
3007
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
3008
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
3009
|
-
} else {
|
|
3010
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
3011
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
3015
|
-
}
|
|
3016
3466
|
|
|
3017
3467
|
// src/module.ts
|
|
3018
3468
|
var InjectCacheStack = () => Inject(CACHE_STACK);
|