layercache 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -21
- package/README.md +254 -906
- package/dist/{chunk-7V7XAB74.js → chunk-4PPBOOXT.js} +37 -3
- package/dist/{chunk-QHWG7QS5.js → chunk-BQLL6IM5.js} +47 -1
- package/dist/{chunk-KOYGHLVP.js → chunk-GJBKCFE6.js} +65 -10
- package/dist/cli.cjs +85 -19
- package/dist/cli.js +4 -18
- package/dist/{edge-Dw97n89L.d.ts → edge-DLstcDMn.d.cts} +32 -13
- package/dist/{edge-Dw97n89L.d.cts → edge-DLstcDMn.d.ts} +32 -13
- package/dist/edge.cjs +101 -12
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +1160 -350
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +1017 -342
- package/package.json +29 -3
- package/packages/nestjs/dist/index.cjs +793 -270
- package/packages/nestjs/dist/index.d.cts +23 -12
- package/packages/nestjs/dist/index.d.ts +23 -12
- package/packages/nestjs/dist/index.js +793 -270
|
@@ -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
|
);
|
|
@@ -338,11 +354,30 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
338
354
|
* ```
|
|
339
355
|
*/
|
|
340
356
|
namespace(childPrefix) {
|
|
357
|
+
validateNamespaceKey(childPrefix);
|
|
341
358
|
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
342
359
|
}
|
|
343
360
|
qualify(key) {
|
|
344
361
|
return `${this.prefix}:${key}`;
|
|
345
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
|
+
}
|
|
346
381
|
async trackMetrics(operation) {
|
|
347
382
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
348
383
|
const before = this.cache.getMetrics();
|
|
@@ -467,6 +502,20 @@ function addMap(base, delta) {
|
|
|
467
502
|
}
|
|
468
503
|
return result;
|
|
469
504
|
}
|
|
505
|
+
function validateNamespaceKey(key) {
|
|
506
|
+
if (key.length === 0) {
|
|
507
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
508
|
+
}
|
|
509
|
+
if (key.length > 256) {
|
|
510
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
511
|
+
}
|
|
512
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
513
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
514
|
+
}
|
|
515
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
516
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
470
519
|
|
|
471
520
|
// ../../src/invalidation/PatternMatcher.ts
|
|
472
521
|
var PatternMatcher = class _PatternMatcher {
|
|
@@ -522,21 +571,41 @@ var CacheKeyDiscovery = class {
|
|
|
522
571
|
this.options = options;
|
|
523
572
|
}
|
|
524
573
|
options;
|
|
525
|
-
async collectKeysWithPrefix(prefix) {
|
|
574
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
526
575
|
const { tagIndex } = this.options;
|
|
527
|
-
const matches = new Set(
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
}
|
|
530
589
|
await Promise.all(
|
|
531
590
|
this.options.layers.map(async (layer) => {
|
|
532
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
591
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
533
592
|
return;
|
|
534
593
|
}
|
|
535
594
|
try {
|
|
536
|
-
|
|
537
|
-
|
|
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 ?? []) {
|
|
538
606
|
if (key.startsWith(prefix)) {
|
|
539
607
|
matches.add(key);
|
|
608
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
540
609
|
}
|
|
541
610
|
}
|
|
542
611
|
} catch (error) {
|
|
@@ -546,18 +615,39 @@ var CacheKeyDiscovery = class {
|
|
|
546
615
|
);
|
|
547
616
|
return [...matches];
|
|
548
617
|
}
|
|
549
|
-
async collectKeysMatchingPattern(pattern) {
|
|
550
|
-
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
|
+
}
|
|
551
631
|
await Promise.all(
|
|
552
632
|
this.options.layers.map(async (layer) => {
|
|
553
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
633
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
554
634
|
return;
|
|
555
635
|
}
|
|
556
636
|
try {
|
|
557
|
-
|
|
558
|
-
|
|
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 ?? []) {
|
|
559
648
|
if (PatternMatcher.matches(pattern, key)) {
|
|
560
649
|
matches.add(key);
|
|
650
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
561
651
|
}
|
|
562
652
|
}
|
|
563
653
|
} catch (error) {
|
|
@@ -567,8 +657,280 @@ var CacheKeyDiscovery = class {
|
|
|
567
657
|
);
|
|
568
658
|
return [...matches];
|
|
569
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
|
+
}
|
|
570
665
|
};
|
|
571
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
|
+
|
|
572
934
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
573
935
|
var CircuitBreakerManager = class {
|
|
574
936
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -587,9 +949,7 @@ var CircuitBreakerManager = class {
|
|
|
587
949
|
}
|
|
588
950
|
const now = Date.now();
|
|
589
951
|
if (state.openUntil <= now) {
|
|
590
|
-
|
|
591
|
-
state.failures = 0;
|
|
592
|
-
this.breakers.set(key, state);
|
|
952
|
+
this.breakers.delete(key);
|
|
593
953
|
return;
|
|
594
954
|
}
|
|
595
955
|
const remainingMs = state.openUntil - now;
|
|
@@ -600,15 +960,15 @@ var CircuitBreakerManager = class {
|
|
|
600
960
|
if (!options) {
|
|
601
961
|
return;
|
|
602
962
|
}
|
|
963
|
+
this.pruneIfNeeded();
|
|
603
964
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
604
965
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
605
|
-
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
966
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
606
967
|
state.failures += 1;
|
|
607
968
|
if (state.failures >= failureThreshold) {
|
|
608
969
|
state.openUntil = Date.now() + cooldownMs;
|
|
609
970
|
}
|
|
610
971
|
this.breakers.set(key, state);
|
|
611
|
-
this.pruneIfNeeded();
|
|
612
972
|
}
|
|
613
973
|
recordSuccess(key) {
|
|
614
974
|
this.breakers.delete(key);
|
|
@@ -619,8 +979,7 @@ var CircuitBreakerManager = class {
|
|
|
619
979
|
return false;
|
|
620
980
|
}
|
|
621
981
|
if (state.openUntil <= Date.now()) {
|
|
622
|
-
|
|
623
|
-
state.failures = 0;
|
|
982
|
+
this.breakers.delete(key);
|
|
624
983
|
return false;
|
|
625
984
|
}
|
|
626
985
|
return true;
|
|
@@ -644,15 +1003,20 @@ var CircuitBreakerManager = class {
|
|
|
644
1003
|
if (this.breakers.size <= this.maxEntries) {
|
|
645
1004
|
return;
|
|
646
1005
|
}
|
|
1006
|
+
const now = Date.now();
|
|
647
1007
|
for (const [key, state] of this.breakers.entries()) {
|
|
648
1008
|
if (this.breakers.size <= this.maxEntries) {
|
|
649
|
-
|
|
1009
|
+
return;
|
|
650
1010
|
}
|
|
651
|
-
if (!state.openUntil || state.openUntil <=
|
|
1011
|
+
if (!state.openUntil || state.openUntil <= now) {
|
|
652
1012
|
this.breakers.delete(key);
|
|
653
1013
|
}
|
|
654
1014
|
}
|
|
655
|
-
|
|
1015
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
1019
|
+
for (const [key] of sorted) {
|
|
656
1020
|
if (this.breakers.size <= this.maxEntries) {
|
|
657
1021
|
break;
|
|
658
1022
|
}
|
|
@@ -662,6 +1026,7 @@ var CircuitBreakerManager = class {
|
|
|
662
1026
|
};
|
|
663
1027
|
|
|
664
1028
|
// ../../src/internal/FetchRateLimiter.ts
|
|
1029
|
+
var MAX_BUCKETS = 1e4;
|
|
665
1030
|
var FetchRateLimiter = class {
|
|
666
1031
|
buckets = /* @__PURE__ */ new Map();
|
|
667
1032
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -827,10 +1192,25 @@ var FetchRateLimiter = class {
|
|
|
827
1192
|
if (existing) {
|
|
828
1193
|
return existing;
|
|
829
1194
|
}
|
|
1195
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1196
|
+
this.evictIdleBuckets();
|
|
1197
|
+
}
|
|
830
1198
|
const bucket = { active: 0, startedAt: [] };
|
|
831
1199
|
this.buckets.set(bucketKey, bucket);
|
|
832
1200
|
return bucket;
|
|
833
1201
|
}
|
|
1202
|
+
evictIdleBuckets() {
|
|
1203
|
+
for (const [key, bucket] of this.buckets.entries()) {
|
|
1204
|
+
if (this.buckets.size <= MAX_BUCKETS * 0.9) {
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
|
|
1208
|
+
this.buckets.delete(key);
|
|
1209
|
+
this.queuesByBucket.delete(key);
|
|
1210
|
+
this.pendingBuckets.delete(key);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
834
1214
|
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
835
1215
|
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
836
1216
|
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
@@ -944,19 +1324,47 @@ function isStoredValueEnvelope(value) {
|
|
|
944
1324
|
if (v.kind !== "value" && v.kind !== "empty") {
|
|
945
1325
|
return false;
|
|
946
1326
|
}
|
|
947
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
1327
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
948
1328
|
return false;
|
|
949
1329
|
}
|
|
950
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
1330
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
951
1331
|
return false;
|
|
952
1332
|
}
|
|
953
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
1333
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
954
1334
|
return false;
|
|
955
1335
|
}
|
|
956
1336
|
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
957
1337
|
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
958
1338
|
return false;
|
|
959
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
|
+
}
|
|
960
1368
|
return true;
|
|
961
1369
|
}
|
|
962
1370
|
function createStoredValueEnvelope(options) {
|
|
@@ -1055,6 +1463,12 @@ function normalizePositiveSeconds(value) {
|
|
|
1055
1463
|
}
|
|
1056
1464
|
return value;
|
|
1057
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
|
+
}
|
|
1058
1472
|
|
|
1059
1473
|
// ../../src/internal/TtlResolver.ts
|
|
1060
1474
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1157,18 +1571,18 @@ var TtlResolver = class {
|
|
|
1157
1571
|
return;
|
|
1158
1572
|
}
|
|
1159
1573
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
1160
|
-
|
|
1161
|
-
for (
|
|
1162
|
-
|
|
1163
|
-
|
|
1574
|
+
const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
1575
|
+
for (let i = 0; i < toRemove && i < sorted.length; i++) {
|
|
1576
|
+
const entry = sorted[i];
|
|
1577
|
+
if (entry) {
|
|
1578
|
+
this.accessProfiles.delete(entry[0]);
|
|
1164
1579
|
}
|
|
1165
|
-
this.accessProfiles.delete(key);
|
|
1166
|
-
removed += 1;
|
|
1167
1580
|
}
|
|
1168
1581
|
}
|
|
1169
1582
|
};
|
|
1170
1583
|
|
|
1171
1584
|
// ../../src/invalidation/TagIndex.ts
|
|
1585
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
1172
1586
|
var TagIndex = class {
|
|
1173
1587
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
1174
1588
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -1209,6 +1623,11 @@ var TagIndex = class {
|
|
|
1209
1623
|
async keysForTag(tag) {
|
|
1210
1624
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1211
1625
|
}
|
|
1626
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1627
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1628
|
+
await visitor(key);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1212
1631
|
async keysForPrefix(prefix) {
|
|
1213
1632
|
const node = this.findNode(prefix);
|
|
1214
1633
|
if (!node) {
|
|
@@ -1218,14 +1637,27 @@ var TagIndex = class {
|
|
|
1218
1637
|
this.collectFromNode(node, prefix, matches);
|
|
1219
1638
|
return matches;
|
|
1220
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
|
+
}
|
|
1221
1647
|
async tagsForKey(key) {
|
|
1222
1648
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1223
1649
|
}
|
|
1224
1650
|
async matchPattern(pattern) {
|
|
1225
1651
|
const matches = /* @__PURE__ */ new Set();
|
|
1226
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1652
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1227
1653
|
return [...matches];
|
|
1228
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
|
+
}
|
|
1229
1661
|
async clear() {
|
|
1230
1662
|
this.tagToKeys.clear();
|
|
1231
1663
|
this.keyToTags.clear();
|
|
@@ -1275,7 +1707,18 @@ var TagIndex = class {
|
|
|
1275
1707
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1276
1708
|
}
|
|
1277
1709
|
}
|
|
1278
|
-
|
|
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
|
+
}
|
|
1718
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1719
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1279
1722
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
1280
1723
|
if (visited.has(stateKey)) {
|
|
1281
1724
|
return;
|
|
@@ -1292,21 +1735,37 @@ var TagIndex = class {
|
|
|
1292
1735
|
return;
|
|
1293
1736
|
}
|
|
1294
1737
|
if (patternChar === "*") {
|
|
1295
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1738
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
1296
1739
|
for (const [character, child2] of node.children) {
|
|
1297
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1740
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
1298
1741
|
}
|
|
1299
1742
|
return;
|
|
1300
1743
|
}
|
|
1301
1744
|
if (patternChar === "?") {
|
|
1302
1745
|
for (const [character, child2] of node.children) {
|
|
1303
|
-
this.collectPatternMatches(
|
|
1746
|
+
this.collectPatternMatches(
|
|
1747
|
+
child2,
|
|
1748
|
+
`${prefix}${character}`,
|
|
1749
|
+
pattern,
|
|
1750
|
+
patternIndex + 1,
|
|
1751
|
+
matches,
|
|
1752
|
+
visited,
|
|
1753
|
+
depth + 1
|
|
1754
|
+
);
|
|
1304
1755
|
}
|
|
1305
1756
|
return;
|
|
1306
1757
|
}
|
|
1307
1758
|
const child = node.children.get(patternChar);
|
|
1308
1759
|
if (child) {
|
|
1309
|
-
this.collectPatternMatches(
|
|
1760
|
+
this.collectPatternMatches(
|
|
1761
|
+
child,
|
|
1762
|
+
`${prefix}${patternChar}`,
|
|
1763
|
+
pattern,
|
|
1764
|
+
patternIndex + 1,
|
|
1765
|
+
matches,
|
|
1766
|
+
visited,
|
|
1767
|
+
depth + 1
|
|
1768
|
+
);
|
|
1310
1769
|
}
|
|
1311
1770
|
}
|
|
1312
1771
|
pruneKnownKeysIfNeeded() {
|
|
@@ -1373,22 +1832,27 @@ var TagIndex = class {
|
|
|
1373
1832
|
|
|
1374
1833
|
// ../../src/serialization/JsonSerializer.ts
|
|
1375
1834
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1835
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1376
1836
|
var JsonSerializer = class {
|
|
1377
1837
|
serialize(value) {
|
|
1378
1838
|
return JSON.stringify(value);
|
|
1379
1839
|
}
|
|
1380
1840
|
deserialize(payload) {
|
|
1381
1841
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1382
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1842
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1383
1843
|
}
|
|
1384
1844
|
};
|
|
1385
1845
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1386
|
-
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
|
+
}
|
|
1387
1851
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1388
|
-
|
|
1852
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1389
1853
|
}
|
|
1390
1854
|
if (Array.isArray(value)) {
|
|
1391
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1855
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1392
1856
|
}
|
|
1393
1857
|
if (!isPlainObject(value)) {
|
|
1394
1858
|
return value;
|
|
@@ -1398,7 +1862,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1398
1862
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1399
1863
|
continue;
|
|
1400
1864
|
}
|
|
1401
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1865
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1402
1866
|
}
|
|
1403
1867
|
return sanitized;
|
|
1404
1868
|
}
|
|
@@ -1447,9 +1911,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1447
1911
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1448
1912
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1449
1913
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1450
|
-
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;
|
|
1451
1918
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1452
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1453
1919
|
var DebugLogger = class {
|
|
1454
1920
|
enabled;
|
|
1455
1921
|
constructor(enabled) {
|
|
@@ -1536,6 +2002,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1536
2002
|
snapshotSerializer = new JsonSerializer();
|
|
1537
2003
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1538
2004
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2005
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1539
2006
|
ttlResolver;
|
|
1540
2007
|
circuitBreakerManager;
|
|
1541
2008
|
currentGeneration;
|
|
@@ -1543,6 +2010,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1543
2010
|
writeBehindTimer;
|
|
1544
2011
|
writeBehindFlushPromise;
|
|
1545
2012
|
generationCleanupPromise;
|
|
2013
|
+
clearEpoch = 0;
|
|
1546
2014
|
isDisconnecting = false;
|
|
1547
2015
|
disconnectPromise;
|
|
1548
2016
|
/**
|
|
@@ -1552,7 +2020,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1552
2020
|
* and no `fetcher` is provided.
|
|
1553
2021
|
*/
|
|
1554
2022
|
async get(key, fetcher, options) {
|
|
1555
|
-
const normalizedKey = this.qualifyKey(
|
|
2023
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1556
2024
|
this.validateWriteOptions(options);
|
|
1557
2025
|
await this.awaitStartup("get");
|
|
1558
2026
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1622,7 +2090,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1622
2090
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1623
2091
|
*/
|
|
1624
2092
|
async has(key) {
|
|
1625
|
-
const normalizedKey = this.qualifyKey(
|
|
2093
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1626
2094
|
await this.awaitStartup("has");
|
|
1627
2095
|
for (const layer of this.layers) {
|
|
1628
2096
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1655,7 +2123,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1655
2123
|
* that has it, or null if the key is not found / has no TTL.
|
|
1656
2124
|
*/
|
|
1657
2125
|
async ttl(key) {
|
|
1658
|
-
const normalizedKey = this.qualifyKey(
|
|
2126
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1659
2127
|
await this.awaitStartup("ttl");
|
|
1660
2128
|
for (const layer of this.layers) {
|
|
1661
2129
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1677,7 +2145,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1677
2145
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1678
2146
|
*/
|
|
1679
2147
|
async set(key, value, options) {
|
|
1680
|
-
const normalizedKey = this.qualifyKey(
|
|
2148
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1681
2149
|
this.validateWriteOptions(options);
|
|
1682
2150
|
await this.awaitStartup("set");
|
|
1683
2151
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1686,7 +2154,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1686
2154
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1687
2155
|
*/
|
|
1688
2156
|
async delete(key) {
|
|
1689
|
-
const normalizedKey = this.qualifyKey(
|
|
2157
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1690
2158
|
await this.awaitStartup("delete");
|
|
1691
2159
|
await this.deleteKeys([normalizedKey]);
|
|
1692
2160
|
await this.publishInvalidation({
|
|
@@ -1698,6 +2166,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1698
2166
|
}
|
|
1699
2167
|
async clear() {
|
|
1700
2168
|
await this.awaitStartup("clear");
|
|
2169
|
+
this.beginClearEpoch();
|
|
1701
2170
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1702
2171
|
await this.tagIndex.clear();
|
|
1703
2172
|
this.ttlResolver.clearProfiles();
|
|
@@ -1714,7 +2183,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1714
2183
|
return;
|
|
1715
2184
|
}
|
|
1716
2185
|
await this.awaitStartup("mdelete");
|
|
1717
|
-
const normalizedKeys = keys.map((k) =>
|
|
2186
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1718
2187
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1719
2188
|
await this.deleteKeys(cacheKeys);
|
|
1720
2189
|
await this.publishInvalidation({
|
|
@@ -1731,7 +2200,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1731
2200
|
}
|
|
1732
2201
|
const normalizedEntries = entries.map((entry) => ({
|
|
1733
2202
|
...entry,
|
|
1734
|
-
key: this.qualifyKey(
|
|
2203
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1735
2204
|
}));
|
|
1736
2205
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1737
2206
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1740,7 +2209,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1740
2209
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1741
2210
|
return Promise.all(
|
|
1742
2211
|
normalizedEntries.map((entry) => {
|
|
1743
|
-
const optionsSignature =
|
|
2212
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1744
2213
|
const existing = pendingReads.get(entry.key);
|
|
1745
2214
|
if (!existing) {
|
|
1746
2215
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1809,7 +2278,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1809
2278
|
this.assertActive("mset");
|
|
1810
2279
|
const normalizedEntries = entries.map((entry) => ({
|
|
1811
2280
|
...entry,
|
|
1812
|
-
key: this.qualifyKey(
|
|
2281
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1813
2282
|
}));
|
|
1814
2283
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1815
2284
|
await this.awaitStartup("mset");
|
|
@@ -1852,7 +2321,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1852
2321
|
*/
|
|
1853
2322
|
wrap(prefix, fetcher, options = {}) {
|
|
1854
2323
|
return (...args) => {
|
|
1855
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2324
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1856
2325
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1857
2326
|
return this.get(key, () => fetcher(...args), options);
|
|
1858
2327
|
};
|
|
@@ -1862,11 +2331,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1862
2331
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1863
2332
|
*/
|
|
1864
2333
|
namespace(prefix) {
|
|
2334
|
+
validateNamespaceKey(prefix);
|
|
1865
2335
|
return new CacheNamespace(this, prefix);
|
|
1866
2336
|
}
|
|
1867
2337
|
async invalidateByTag(tag) {
|
|
2338
|
+
validateTag(tag);
|
|
1868
2339
|
await this.awaitStartup("invalidateByTag");
|
|
1869
|
-
const keys = await this.
|
|
2340
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1870
2341
|
await this.deleteKeys(keys);
|
|
1871
2342
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1872
2343
|
}
|
|
@@ -1874,22 +2345,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1874
2345
|
if (tags.length === 0) {
|
|
1875
2346
|
return;
|
|
1876
2347
|
}
|
|
2348
|
+
validateTags(tags);
|
|
1877
2349
|
await this.awaitStartup("invalidateByTags");
|
|
1878
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2350
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1879
2351
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2352
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1880
2353
|
await this.deleteKeys(keys);
|
|
1881
2354
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1882
2355
|
}
|
|
1883
2356
|
async invalidateByPattern(pattern) {
|
|
2357
|
+
validatePattern(pattern);
|
|
1884
2358
|
await this.awaitStartup("invalidateByPattern");
|
|
1885
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2359
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2360
|
+
this.qualifyPattern(pattern),
|
|
2361
|
+
this.invalidationMaxKeys()
|
|
2362
|
+
);
|
|
1886
2363
|
await this.deleteKeys(keys);
|
|
1887
2364
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1888
2365
|
}
|
|
1889
2366
|
async invalidateByPrefix(prefix) {
|
|
1890
2367
|
await this.awaitStartup("invalidateByPrefix");
|
|
1891
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1892
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2368
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2369
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1893
2370
|
await this.deleteKeys(keys);
|
|
1894
2371
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1895
2372
|
}
|
|
@@ -1959,7 +2436,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1959
2436
|
* Returns `null` if the key does not exist in any layer.
|
|
1960
2437
|
*/
|
|
1961
2438
|
async inspect(key) {
|
|
1962
|
-
const userKey =
|
|
2439
|
+
const userKey = validateCacheKey(key);
|
|
1963
2440
|
const normalizedKey = this.qualifyKey(userKey);
|
|
1964
2441
|
await this.awaitStartup("inspect");
|
|
1965
2442
|
const foundInLayers = [];
|
|
@@ -1996,50 +2473,79 @@ var CacheStack = class extends EventEmitter {
|
|
|
1996
2473
|
}
|
|
1997
2474
|
async exportState() {
|
|
1998
2475
|
await this.awaitStartup("exportState");
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
const keys = await layer.keys();
|
|
2005
|
-
for (const key of keys) {
|
|
2006
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
2007
|
-
if (exported.has(exportedKey)) {
|
|
2008
|
-
continue;
|
|
2009
|
-
}
|
|
2010
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2011
|
-
if (stored === null) {
|
|
2012
|
-
continue;
|
|
2013
|
-
}
|
|
2014
|
-
exported.set(exportedKey, {
|
|
2015
|
-
key: exportedKey,
|
|
2016
|
-
value: stored,
|
|
2017
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
return [...exported.values()];
|
|
2476
|
+
const entries = [];
|
|
2477
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2478
|
+
entries.push(entry);
|
|
2479
|
+
});
|
|
2480
|
+
return entries;
|
|
2022
2481
|
}
|
|
2023
2482
|
async importState(entries) {
|
|
2024
2483
|
await this.awaitStartup("importState");
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
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
|
+
}
|
|
2032
2498
|
}
|
|
2033
2499
|
async persistToFile(filePath) {
|
|
2034
2500
|
this.assertActive("persistToFile");
|
|
2035
|
-
const snapshot = await this.exportState();
|
|
2036
2501
|
const { promises: fs } = await import("fs");
|
|
2037
|
-
|
|
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
|
+
}
|
|
2038
2528
|
}
|
|
2039
2529
|
async restoreFromFile(filePath) {
|
|
2040
2530
|
this.assertActive("restoreFromFile");
|
|
2041
|
-
const { promises: fs } = await import("fs");
|
|
2042
|
-
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
|
+
}
|
|
2043
2549
|
let parsed;
|
|
2044
2550
|
try {
|
|
2045
2551
|
parsed = JSON.parse(raw);
|
|
@@ -2083,14 +2589,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2083
2589
|
await this.handleInvalidationMessage(message);
|
|
2084
2590
|
});
|
|
2085
2591
|
}
|
|
2086
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2592
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2087
2593
|
const fetchTask = async () => {
|
|
2088
2594
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2089
2595
|
if (secondHit.found) {
|
|
2090
2596
|
this.metricsCollector.increment("hits");
|
|
2091
2597
|
return secondHit.value;
|
|
2092
2598
|
}
|
|
2093
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2599
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2094
2600
|
};
|
|
2095
2601
|
const singleFlightTask = async () => {
|
|
2096
2602
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -2100,7 +2606,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2100
2606
|
key,
|
|
2101
2607
|
this.resolveSingleFlightOptions(),
|
|
2102
2608
|
fetchTask,
|
|
2103
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2609
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2104
2610
|
);
|
|
2105
2611
|
};
|
|
2106
2612
|
if (this.options.stampedePrevention === false) {
|
|
@@ -2108,7 +2614,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2108
2614
|
}
|
|
2109
2615
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2110
2616
|
}
|
|
2111
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2617
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2112
2618
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2113
2619
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2114
2620
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2122,9 +2628,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2122
2628
|
}
|
|
2123
2629
|
await this.sleep(pollIntervalMs);
|
|
2124
2630
|
}
|
|
2125
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2631
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2126
2632
|
}
|
|
2127
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2633
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2128
2634
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2129
2635
|
this.metricsCollector.increment("fetches");
|
|
2130
2636
|
const fetchStart = Date.now();
|
|
@@ -2145,6 +2651,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2145
2651
|
if (!this.shouldNegativeCache(options)) {
|
|
2146
2652
|
return null;
|
|
2147
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
|
+
}
|
|
2148
2664
|
await this.storeEntry(key, "empty", null, options);
|
|
2149
2665
|
return null;
|
|
2150
2666
|
}
|
|
@@ -2157,11 +2673,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
2157
2673
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2158
2674
|
}
|
|
2159
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
|
+
}
|
|
2160
2686
|
await this.storeEntry(key, "value", fetched, options);
|
|
2161
2687
|
return fetched;
|
|
2162
2688
|
}
|
|
2163
2689
|
async storeEntry(key, kind, value, options) {
|
|
2690
|
+
const clearEpoch = this.clearEpoch;
|
|
2691
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2164
2692
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2693
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2165
2696
|
if (options?.tags) {
|
|
2166
2697
|
await this.tagIndex.track(key, options.tags);
|
|
2167
2698
|
} else {
|
|
@@ -2176,6 +2707,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
2176
2707
|
}
|
|
2177
2708
|
async writeBatch(entries) {
|
|
2178
2709
|
const now = Date.now();
|
|
2710
|
+
const clearEpoch = this.clearEpoch;
|
|
2711
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2179
2712
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2180
2713
|
const immediateOperations = [];
|
|
2181
2714
|
const deferredOperations = [];
|
|
@@ -2192,12 +2725,21 @@ var CacheStack = class extends EventEmitter {
|
|
|
2192
2725
|
}
|
|
2193
2726
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2194
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
|
+
}
|
|
2195
2737
|
try {
|
|
2196
2738
|
if (layer.setMany) {
|
|
2197
|
-
await layer.setMany(
|
|
2739
|
+
await layer.setMany(activeEntries);
|
|
2198
2740
|
return;
|
|
2199
2741
|
}
|
|
2200
|
-
await Promise.all(
|
|
2742
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2201
2743
|
} catch (error) {
|
|
2202
2744
|
await this.handleLayerFailure(layer, "write", error);
|
|
2203
2745
|
}
|
|
@@ -2210,7 +2752,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2210
2752
|
}
|
|
2211
2753
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2212
2754
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2755
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2213
2758
|
for (const entry of entries) {
|
|
2759
|
+
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2214
2762
|
if (entry.options?.tags) {
|
|
2215
2763
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2216
2764
|
} else {
|
|
@@ -2312,10 +2860,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
2312
2860
|
}
|
|
2313
2861
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2314
2862
|
const now = Date.now();
|
|
2863
|
+
const clearEpoch = this.clearEpoch;
|
|
2864
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2315
2865
|
const immediateOperations = [];
|
|
2316
2866
|
const deferredOperations = [];
|
|
2317
2867
|
for (const layer of this.layers) {
|
|
2318
2868
|
const operation = async () => {
|
|
2869
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2319
2872
|
if (this.shouldSkipLayer(layer)) {
|
|
2320
2873
|
return;
|
|
2321
2874
|
}
|
|
@@ -2379,10 +2932,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
2379
2932
|
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
2380
2933
|
return;
|
|
2381
2934
|
}
|
|
2935
|
+
const clearEpoch = this.clearEpoch;
|
|
2936
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2382
2937
|
const refresh = (async () => {
|
|
2383
2938
|
this.metricsCollector.increment("refreshes");
|
|
2384
2939
|
try {
|
|
2385
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2940
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2386
2941
|
} catch (error) {
|
|
2387
2942
|
this.metricsCollector.increment("refreshErrors");
|
|
2388
2943
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2392,14 +2947,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2392
2947
|
})();
|
|
2393
2948
|
this.backgroundRefreshes.set(key, refresh);
|
|
2394
2949
|
}
|
|
2395
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2950
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2396
2951
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2397
2952
|
await this.fetchWithGuards(
|
|
2398
2953
|
key,
|
|
2399
2954
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2400
2955
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2401
2956
|
}),
|
|
2402
|
-
options
|
|
2957
|
+
options,
|
|
2958
|
+
expectedClearEpoch,
|
|
2959
|
+
expectedKeyEpoch
|
|
2403
2960
|
);
|
|
2404
2961
|
}
|
|
2405
2962
|
resolveSingleFlightOptions() {
|
|
@@ -2414,6 +2971,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2414
2971
|
if (keys.length === 0) {
|
|
2415
2972
|
return;
|
|
2416
2973
|
}
|
|
2974
|
+
this.bumpKeyEpochs(keys);
|
|
2417
2975
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2418
2976
|
for (const key of keys) {
|
|
2419
2977
|
await this.tagIndex.remove(key);
|
|
@@ -2436,21 +2994,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
2436
2994
|
return;
|
|
2437
2995
|
}
|
|
2438
2996
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2439
|
-
if (localLayers.length === 0) {
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2442
2997
|
if (message.scope === "clear") {
|
|
2998
|
+
this.beginClearEpoch();
|
|
2443
2999
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2444
3000
|
await this.tagIndex.clear();
|
|
2445
3001
|
this.ttlResolver.clearProfiles();
|
|
3002
|
+
this.circuitBreakerManager.clear();
|
|
2446
3003
|
return;
|
|
2447
3004
|
}
|
|
2448
3005
|
const keys = message.keys ?? [];
|
|
3006
|
+
this.bumpKeyEpochs(keys);
|
|
2449
3007
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2450
3008
|
if (message.operation !== "write") {
|
|
2451
3009
|
for (const key of keys) {
|
|
2452
3010
|
await this.tagIndex.remove(key);
|
|
2453
3011
|
this.ttlResolver.deleteProfile(key);
|
|
3012
|
+
this.circuitBreakerManager.delete(key);
|
|
2454
3013
|
}
|
|
2455
3014
|
}
|
|
2456
3015
|
}
|
|
@@ -2556,6 +3115,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
2556
3115
|
shouldWriteBehind(layer) {
|
|
2557
3116
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2558
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
|
+
}
|
|
2559
3140
|
async enqueueWriteBehind(operation) {
|
|
2560
3141
|
this.writeBehindQueue.push(operation);
|
|
2561
3142
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
@@ -2682,107 +3263,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
2682
3263
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2683
3264
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2684
3265
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
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);
|
|
2698
3288
|
if (typeof this.options.generationCleanup === "object") {
|
|
2699
|
-
|
|
3289
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2700
3290
|
}
|
|
2701
3291
|
if (this.options.generation !== void 0) {
|
|
2702
|
-
|
|
3292
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2703
3293
|
}
|
|
2704
3294
|
}
|
|
2705
3295
|
validateWriteOptions(options) {
|
|
2706
3296
|
if (!options) {
|
|
2707
3297
|
return;
|
|
2708
3298
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
validateLayerNumberOption(name, value) {
|
|
2721
|
-
if (value === void 0) {
|
|
2722
|
-
return;
|
|
2723
|
-
}
|
|
2724
|
-
if (typeof value === "number") {
|
|
2725
|
-
this.validateNonNegativeNumber(name, value);
|
|
2726
|
-
return;
|
|
2727
|
-
}
|
|
2728
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2729
|
-
if (layerValue === void 0) {
|
|
2730
|
-
continue;
|
|
2731
|
-
}
|
|
2732
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
validatePositiveNumber(name, value) {
|
|
2736
|
-
if (value === void 0) {
|
|
2737
|
-
return;
|
|
2738
|
-
}
|
|
2739
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2740
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
validateRateLimitOptions(name, options) {
|
|
2744
|
-
if (!options) {
|
|
2745
|
-
return;
|
|
2746
|
-
}
|
|
2747
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2748
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2749
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2750
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2751
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2752
|
-
}
|
|
2753
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2754
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
validateNonNegativeNumber(name, value) {
|
|
2758
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2759
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
validateCacheKey(key) {
|
|
2763
|
-
if (key.length === 0) {
|
|
2764
|
-
throw new Error("Cache key must not be empty.");
|
|
2765
|
-
}
|
|
2766
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2767
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2768
|
-
}
|
|
2769
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2770
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2771
|
-
}
|
|
2772
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2773
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2774
|
-
}
|
|
2775
|
-
return key;
|
|
2776
|
-
}
|
|
2777
|
-
validateTtlPolicy(name, policy) {
|
|
2778
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2779
|
-
return;
|
|
2780
|
-
}
|
|
2781
|
-
if ("alignTo" in policy) {
|
|
2782
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
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);
|
|
2786
3310
|
}
|
|
2787
3311
|
assertActive(operation) {
|
|
2788
3312
|
if (this.isDisconnecting) {
|
|
@@ -2794,24 +3318,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2794
3318
|
await this.startup;
|
|
2795
3319
|
this.assertActive(operation);
|
|
2796
3320
|
}
|
|
2797
|
-
serializeOptions(options) {
|
|
2798
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2799
|
-
}
|
|
2800
|
-
validateAdaptiveTtlOptions(options) {
|
|
2801
|
-
if (!options || options === true) {
|
|
2802
|
-
return;
|
|
2803
|
-
}
|
|
2804
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2805
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2806
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2807
|
-
}
|
|
2808
|
-
validateCircuitBreakerOptions(options) {
|
|
2809
|
-
if (!options) {
|
|
2810
|
-
return;
|
|
2811
|
-
}
|
|
2812
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2813
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2814
|
-
}
|
|
2815
3321
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2816
3322
|
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
2817
3323
|
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
@@ -2879,18 +3385,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2879
3385
|
this.emit("error", { operation, ...context });
|
|
2880
3386
|
}
|
|
2881
3387
|
}
|
|
2882
|
-
serializeKeyPart(value) {
|
|
2883
|
-
if (typeof value === "string") {
|
|
2884
|
-
return `s:${value}`;
|
|
2885
|
-
}
|
|
2886
|
-
if (typeof value === "number") {
|
|
2887
|
-
return `n:${value}`;
|
|
2888
|
-
}
|
|
2889
|
-
if (typeof value === "boolean") {
|
|
2890
|
-
return `b:${value}`;
|
|
2891
|
-
}
|
|
2892
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2893
|
-
}
|
|
2894
3388
|
isCacheSnapshotEntries(value) {
|
|
2895
3389
|
return Array.isArray(value) && value.every((entry) => {
|
|
2896
3390
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2903,43 +3397,72 @@ var CacheStack = class extends EventEmitter {
|
|
|
2903
3397
|
sanitizeSnapshotValue(value) {
|
|
2904
3398
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2905
3399
|
}
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
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];
|
|
2912
3417
|
}
|
|
2913
|
-
const
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
if (baseDir !== false) {
|
|
2917
|
-
const relative = path.relative(baseDir, resolved);
|
|
2918
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2919
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2920
|
-
}
|
|
3418
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3419
|
+
keys.add(key);
|
|
3420
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2921
3421
|
}
|
|
2922
|
-
return
|
|
3422
|
+
return [...keys];
|
|
2923
3423
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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}).`);
|
|
2927
3428
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
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;
|
|
2932
3440
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
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
|
+
}
|
|
2936
3463
|
}
|
|
2937
|
-
return value;
|
|
2938
3464
|
}
|
|
2939
3465
|
};
|
|
2940
|
-
function createInstanceId() {
|
|
2941
|
-
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2942
|
-
}
|
|
2943
3466
|
|
|
2944
3467
|
// src/module.ts
|
|
2945
3468
|
var InjectCacheStack = () => Inject(CACHE_STACK);
|