layercache 1.2.2 → 1.2.4
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/README.md +119 -89
- package/dist/{chunk-ZMDB5KOK.js → chunk-7V7XAB74.js} +24 -1
- package/dist/{chunk-46UH7LNM.js → chunk-KOYGHLVP.js} +142 -18
- package/dist/{chunk-IXCMHVHP.js → chunk-QHWG7QS5.js} +1 -1
- package/dist/cli.cjs +37 -3
- package/dist/cli.js +15 -4
- package/dist/{edge-DLpdQN0W.d.ts → edge-Dw97n89L.d.cts} +33 -1
- package/dist/{edge-DLpdQN0W.d.cts → edge-Dw97n89L.d.ts} +33 -1
- package/dist/edge.cjs +165 -17
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +657 -146
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +447 -84
- package/package.json +4 -4
- package/packages/nestjs/dist/index.cjs +558 -91
- package/packages/nestjs/dist/index.d.cts +24 -0
- package/packages/nestjs/dist/index.d.ts +24 -0
- package/packages/nestjs/dist/index.js +558 -91
package/dist/index.cjs
CHANGED
|
@@ -306,6 +306,107 @@ function addMap(base, delta) {
|
|
|
306
306
|
return result;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// src/invalidation/PatternMatcher.ts
|
|
310
|
+
var PatternMatcher = class _PatternMatcher {
|
|
311
|
+
/**
|
|
312
|
+
* Tests whether a glob-style pattern matches a value.
|
|
313
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
314
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
315
|
+
* quadratic memory usage on long patterns/keys.
|
|
316
|
+
*/
|
|
317
|
+
static matches(pattern, value) {
|
|
318
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
322
|
+
*/
|
|
323
|
+
static matchLinear(pattern, value) {
|
|
324
|
+
let patternIndex = 0;
|
|
325
|
+
let valueIndex = 0;
|
|
326
|
+
let starIndex = -1;
|
|
327
|
+
let backtrackValueIndex = 0;
|
|
328
|
+
while (valueIndex < value.length) {
|
|
329
|
+
const patternChar = pattern[patternIndex];
|
|
330
|
+
const valueChar = value[valueIndex];
|
|
331
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
332
|
+
starIndex = patternIndex;
|
|
333
|
+
patternIndex += 1;
|
|
334
|
+
backtrackValueIndex = valueIndex;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
338
|
+
patternIndex += 1;
|
|
339
|
+
valueIndex += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (starIndex !== -1) {
|
|
343
|
+
patternIndex = starIndex + 1;
|
|
344
|
+
backtrackValueIndex += 1;
|
|
345
|
+
valueIndex = backtrackValueIndex;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
351
|
+
patternIndex += 1;
|
|
352
|
+
}
|
|
353
|
+
return patternIndex === pattern.length;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/internal/CacheKeyDiscovery.ts
|
|
358
|
+
var CacheKeyDiscovery = class {
|
|
359
|
+
constructor(options) {
|
|
360
|
+
this.options = options;
|
|
361
|
+
}
|
|
362
|
+
options;
|
|
363
|
+
async collectKeysWithPrefix(prefix) {
|
|
364
|
+
const { tagIndex } = this.options;
|
|
365
|
+
const matches = new Set(
|
|
366
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
367
|
+
);
|
|
368
|
+
await Promise.all(
|
|
369
|
+
this.options.layers.map(async (layer) => {
|
|
370
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const keys = await layer.keys();
|
|
375
|
+
for (const key of keys) {
|
|
376
|
+
if (key.startsWith(prefix)) {
|
|
377
|
+
matches.add(key);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
return [...matches];
|
|
386
|
+
}
|
|
387
|
+
async collectKeysMatchingPattern(pattern) {
|
|
388
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
389
|
+
await Promise.all(
|
|
390
|
+
this.options.layers.map(async (layer) => {
|
|
391
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const keys = await layer.keys();
|
|
396
|
+
for (const key of keys) {
|
|
397
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
398
|
+
matches.add(key);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
return [...matches];
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
309
410
|
// src/internal/CircuitBreakerManager.ts
|
|
310
411
|
var CircuitBreakerManager = class {
|
|
311
412
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -400,8 +501,9 @@ var CircuitBreakerManager = class {
|
|
|
400
501
|
|
|
401
502
|
// src/internal/FetchRateLimiter.ts
|
|
402
503
|
var FetchRateLimiter = class {
|
|
403
|
-
queue = [];
|
|
404
504
|
buckets = /* @__PURE__ */ new Map();
|
|
505
|
+
queuesByBucket = /* @__PURE__ */ new Map();
|
|
506
|
+
pendingBuckets = /* @__PURE__ */ new Set();
|
|
405
507
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
406
508
|
nextFetcherBucketId = 0;
|
|
407
509
|
drainTimer;
|
|
@@ -414,13 +516,17 @@ var FetchRateLimiter = class {
|
|
|
414
516
|
return task();
|
|
415
517
|
}
|
|
416
518
|
return new Promise((resolve2, reject) => {
|
|
417
|
-
this.
|
|
418
|
-
|
|
519
|
+
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
520
|
+
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
521
|
+
queue.push({
|
|
522
|
+
bucketKey,
|
|
419
523
|
options: normalized,
|
|
420
524
|
task,
|
|
421
525
|
resolve: resolve2,
|
|
422
526
|
reject
|
|
423
527
|
});
|
|
528
|
+
this.queuesByBucket.set(bucketKey, queue);
|
|
529
|
+
this.pendingBuckets.add(bucketKey);
|
|
424
530
|
this.drain();
|
|
425
531
|
});
|
|
426
532
|
}
|
|
@@ -463,22 +569,30 @@ var FetchRateLimiter = class {
|
|
|
463
569
|
clearTimeout(this.drainTimer);
|
|
464
570
|
this.drainTimer = void 0;
|
|
465
571
|
}
|
|
466
|
-
while (this.
|
|
467
|
-
let
|
|
572
|
+
while (this.pendingBuckets.size > 0) {
|
|
573
|
+
let nextBucketKey;
|
|
468
574
|
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
469
|
-
for (
|
|
470
|
-
const
|
|
575
|
+
for (const bucketKey of this.pendingBuckets) {
|
|
576
|
+
const queue2 = this.queuesByBucket.get(bucketKey);
|
|
577
|
+
if (!queue2 || queue2.length === 0) {
|
|
578
|
+
this.pendingBuckets.delete(bucketKey);
|
|
579
|
+
this.queuesByBucket.delete(bucketKey);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const next2 = queue2[0];
|
|
471
583
|
if (!next2) {
|
|
584
|
+
this.pendingBuckets.delete(bucketKey);
|
|
585
|
+
this.queuesByBucket.delete(bucketKey);
|
|
472
586
|
continue;
|
|
473
587
|
}
|
|
474
|
-
const waitMs = this.waitTime(
|
|
588
|
+
const waitMs = this.waitTime(bucketKey, next2.options);
|
|
475
589
|
if (waitMs <= 0) {
|
|
476
|
-
|
|
590
|
+
nextBucketKey = bucketKey;
|
|
477
591
|
break;
|
|
478
592
|
}
|
|
479
593
|
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
480
594
|
}
|
|
481
|
-
if (
|
|
595
|
+
if (!nextBucketKey) {
|
|
482
596
|
if (Number.isFinite(nextWaitMs)) {
|
|
483
597
|
this.drainTimer = setTimeout(() => {
|
|
484
598
|
this.drainTimer = void 0;
|
|
@@ -488,15 +602,32 @@ var FetchRateLimiter = class {
|
|
|
488
602
|
}
|
|
489
603
|
return;
|
|
490
604
|
}
|
|
491
|
-
const
|
|
605
|
+
const queue = this.queuesByBucket.get(nextBucketKey);
|
|
606
|
+
const next = queue?.shift();
|
|
492
607
|
if (!next) {
|
|
493
|
-
|
|
608
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
609
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (!queue || queue.length === 0) {
|
|
613
|
+
this.pendingBuckets.delete(nextBucketKey);
|
|
614
|
+
this.queuesByBucket.delete(nextBucketKey);
|
|
494
615
|
}
|
|
495
616
|
const bucket = this.bucketState(next.bucketKey);
|
|
617
|
+
if (bucket.cleanupTimer) {
|
|
618
|
+
clearTimeout(bucket.cleanupTimer);
|
|
619
|
+
bucket.cleanupTimer = void 0;
|
|
620
|
+
}
|
|
496
621
|
bucket.active += 1;
|
|
497
|
-
|
|
622
|
+
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
623
|
+
bucket.startedAt.push(Date.now());
|
|
624
|
+
}
|
|
498
625
|
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
499
626
|
bucket.active -= 1;
|
|
627
|
+
if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
|
|
628
|
+
this.pendingBuckets.add(next.bucketKey);
|
|
629
|
+
}
|
|
630
|
+
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
500
631
|
this.drain();
|
|
501
632
|
});
|
|
502
633
|
}
|
|
@@ -538,6 +669,31 @@ var FetchRateLimiter = class {
|
|
|
538
669
|
this.buckets.set(bucketKey, bucket);
|
|
539
670
|
return bucket;
|
|
540
671
|
}
|
|
672
|
+
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
673
|
+
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
674
|
+
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
675
|
+
this.buckets.delete(bucketKey);
|
|
676
|
+
this.queuesByBucket.delete(bucketKey);
|
|
677
|
+
this.pendingBuckets.delete(bucketKey);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (!intervalMs || bucket.active > 0 || queued > 0) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (bucket.cleanupTimer) {
|
|
684
|
+
clearTimeout(bucket.cleanupTimer);
|
|
685
|
+
}
|
|
686
|
+
bucket.cleanupTimer = setTimeout(() => {
|
|
687
|
+
bucket.cleanupTimer = void 0;
|
|
688
|
+
this.prune(bucket, Date.now(), intervalMs);
|
|
689
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && (this.queuesByBucket.get(bucketKey)?.length ?? 0) === 0) {
|
|
690
|
+
this.buckets.delete(bucketKey);
|
|
691
|
+
this.queuesByBucket.delete(bucketKey);
|
|
692
|
+
this.pendingBuckets.delete(bucketKey);
|
|
693
|
+
}
|
|
694
|
+
}, intervalMs);
|
|
695
|
+
bucket.cleanupTimer.unref?.();
|
|
696
|
+
}
|
|
541
697
|
};
|
|
542
698
|
|
|
543
699
|
// src/internal/MetricsCollector.ts
|
|
@@ -616,7 +772,30 @@ var MetricsCollector = class {
|
|
|
616
772
|
|
|
617
773
|
// src/internal/StoredValue.ts
|
|
618
774
|
function isStoredValueEnvelope(value) {
|
|
619
|
-
|
|
775
|
+
if (typeof value !== "object" || value === null) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
const v = value;
|
|
779
|
+
if (v.__layercache !== 1) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
795
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
return true;
|
|
620
799
|
}
|
|
621
800
|
function createStoredValueEnvelope(options) {
|
|
622
801
|
const now = options.now ?? Date.now();
|
|
@@ -827,69 +1006,23 @@ var TtlResolver = class {
|
|
|
827
1006
|
}
|
|
828
1007
|
};
|
|
829
1008
|
|
|
830
|
-
// src/invalidation/PatternMatcher.ts
|
|
831
|
-
var PatternMatcher = class _PatternMatcher {
|
|
832
|
-
/**
|
|
833
|
-
* Tests whether a glob-style pattern matches a value.
|
|
834
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
835
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
836
|
-
* quadratic memory usage on long patterns/keys.
|
|
837
|
-
*/
|
|
838
|
-
static matches(pattern, value) {
|
|
839
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
843
|
-
*/
|
|
844
|
-
static matchLinear(pattern, value) {
|
|
845
|
-
let patternIndex = 0;
|
|
846
|
-
let valueIndex = 0;
|
|
847
|
-
let starIndex = -1;
|
|
848
|
-
let backtrackValueIndex = 0;
|
|
849
|
-
while (valueIndex < value.length) {
|
|
850
|
-
const patternChar = pattern[patternIndex];
|
|
851
|
-
const valueChar = value[valueIndex];
|
|
852
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
853
|
-
starIndex = patternIndex;
|
|
854
|
-
patternIndex += 1;
|
|
855
|
-
backtrackValueIndex = valueIndex;
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
859
|
-
patternIndex += 1;
|
|
860
|
-
valueIndex += 1;
|
|
861
|
-
continue;
|
|
862
|
-
}
|
|
863
|
-
if (starIndex !== -1) {
|
|
864
|
-
patternIndex = starIndex + 1;
|
|
865
|
-
backtrackValueIndex += 1;
|
|
866
|
-
valueIndex = backtrackValueIndex;
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
872
|
-
patternIndex += 1;
|
|
873
|
-
}
|
|
874
|
-
return patternIndex === pattern.length;
|
|
875
|
-
}
|
|
876
|
-
};
|
|
877
|
-
|
|
878
1009
|
// src/invalidation/TagIndex.ts
|
|
879
1010
|
var TagIndex = class {
|
|
880
1011
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
881
1012
|
keyToTags = /* @__PURE__ */ new Map();
|
|
882
1013
|
knownKeys = /* @__PURE__ */ new Set();
|
|
883
1014
|
maxKnownKeys;
|
|
1015
|
+
nextNodeId = 1;
|
|
1016
|
+
root = this.createTrieNode();
|
|
884
1017
|
constructor(options = {}) {
|
|
885
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
1018
|
+
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
886
1019
|
}
|
|
887
1020
|
async touch(key) {
|
|
888
|
-
this.
|
|
1021
|
+
this.insertKnownKey(key);
|
|
889
1022
|
this.pruneKnownKeysIfNeeded();
|
|
890
1023
|
}
|
|
891
1024
|
async track(key, tags) {
|
|
892
|
-
this.
|
|
1025
|
+
this.insertKnownKey(key);
|
|
893
1026
|
this.pruneKnownKeysIfNeeded();
|
|
894
1027
|
if (tags.length === 0) {
|
|
895
1028
|
return;
|
|
@@ -915,18 +1048,104 @@ var TagIndex = class {
|
|
|
915
1048
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
916
1049
|
}
|
|
917
1050
|
async keysForPrefix(prefix) {
|
|
918
|
-
|
|
1051
|
+
const node = this.findNode(prefix);
|
|
1052
|
+
if (!node) {
|
|
1053
|
+
return [];
|
|
1054
|
+
}
|
|
1055
|
+
const matches = [];
|
|
1056
|
+
this.collectFromNode(node, prefix, matches);
|
|
1057
|
+
return matches;
|
|
919
1058
|
}
|
|
920
1059
|
async tagsForKey(key) {
|
|
921
1060
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
922
1061
|
}
|
|
923
1062
|
async matchPattern(pattern) {
|
|
924
|
-
|
|
1063
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1064
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1065
|
+
return [...matches];
|
|
925
1066
|
}
|
|
926
1067
|
async clear() {
|
|
927
1068
|
this.tagToKeys.clear();
|
|
928
1069
|
this.keyToTags.clear();
|
|
929
1070
|
this.knownKeys.clear();
|
|
1071
|
+
this.root.children.clear();
|
|
1072
|
+
this.root.terminal = false;
|
|
1073
|
+
this.nextNodeId = this.root.id + 1;
|
|
1074
|
+
}
|
|
1075
|
+
createTrieNode() {
|
|
1076
|
+
return {
|
|
1077
|
+
id: this.nextNodeId++,
|
|
1078
|
+
terminal: false,
|
|
1079
|
+
children: /* @__PURE__ */ new Map()
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
insertKnownKey(key) {
|
|
1083
|
+
if (this.knownKeys.has(key)) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
this.knownKeys.add(key);
|
|
1087
|
+
let node = this.root;
|
|
1088
|
+
for (const character of key) {
|
|
1089
|
+
let child = node.children.get(character);
|
|
1090
|
+
if (!child) {
|
|
1091
|
+
child = this.createTrieNode();
|
|
1092
|
+
node.children.set(character, child);
|
|
1093
|
+
}
|
|
1094
|
+
node = child;
|
|
1095
|
+
}
|
|
1096
|
+
node.terminal = true;
|
|
1097
|
+
}
|
|
1098
|
+
findNode(prefix) {
|
|
1099
|
+
let node = this.root;
|
|
1100
|
+
for (const character of prefix) {
|
|
1101
|
+
node = node.children.get(character);
|
|
1102
|
+
if (!node) {
|
|
1103
|
+
return void 0;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return node;
|
|
1107
|
+
}
|
|
1108
|
+
collectFromNode(node, prefix, matches) {
|
|
1109
|
+
if (node.terminal) {
|
|
1110
|
+
matches.push(prefix);
|
|
1111
|
+
}
|
|
1112
|
+
for (const [character, child] of node.children) {
|
|
1113
|
+
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
1117
|
+
const stateKey = `${node.id}:${patternIndex}`;
|
|
1118
|
+
if (visited.has(stateKey)) {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
visited.add(stateKey);
|
|
1122
|
+
if (patternIndex === pattern.length) {
|
|
1123
|
+
if (node.terminal) {
|
|
1124
|
+
matches.add(prefix);
|
|
1125
|
+
}
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const patternChar = pattern[patternIndex];
|
|
1129
|
+
if (patternChar === void 0) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (patternChar === "*") {
|
|
1133
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1134
|
+
for (const [character, child2] of node.children) {
|
|
1135
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1136
|
+
}
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (patternChar === "?") {
|
|
1140
|
+
for (const [character, child2] of node.children) {
|
|
1141
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
|
|
1142
|
+
}
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const child = node.children.get(patternChar);
|
|
1146
|
+
if (child) {
|
|
1147
|
+
this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
|
|
1148
|
+
}
|
|
930
1149
|
}
|
|
931
1150
|
pruneKnownKeysIfNeeded() {
|
|
932
1151
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
@@ -943,7 +1162,7 @@ var TagIndex = class {
|
|
|
943
1162
|
}
|
|
944
1163
|
}
|
|
945
1164
|
removeKey(key) {
|
|
946
|
-
this.
|
|
1165
|
+
this.removeKnownKey(key);
|
|
947
1166
|
const tags = this.keyToTags.get(key);
|
|
948
1167
|
if (!tags) {
|
|
949
1168
|
return;
|
|
@@ -960,7 +1179,70 @@ var TagIndex = class {
|
|
|
960
1179
|
}
|
|
961
1180
|
this.keyToTags.delete(key);
|
|
962
1181
|
}
|
|
1182
|
+
removeKnownKey(key) {
|
|
1183
|
+
if (!this.knownKeys.delete(key)) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const path = [];
|
|
1187
|
+
let node = this.root;
|
|
1188
|
+
for (const character of key) {
|
|
1189
|
+
const child = node.children.get(character);
|
|
1190
|
+
if (!child) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
path.push([node, character]);
|
|
1194
|
+
node = child;
|
|
1195
|
+
}
|
|
1196
|
+
node.terminal = false;
|
|
1197
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
1198
|
+
const entry = path[index];
|
|
1199
|
+
if (!entry) {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
const [parent, character] = entry;
|
|
1203
|
+
const child = parent.children.get(character);
|
|
1204
|
+
if (!child || child.terminal || child.children.size > 0) {
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
parent.children.delete(character);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
// src/serialization/JsonSerializer.ts
|
|
1213
|
+
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1214
|
+
var JsonSerializer = class {
|
|
1215
|
+
serialize(value) {
|
|
1216
|
+
return JSON.stringify(value);
|
|
1217
|
+
}
|
|
1218
|
+
deserialize(payload) {
|
|
1219
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1220
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1221
|
+
}
|
|
963
1222
|
};
|
|
1223
|
+
var MAX_SANITIZE_DEPTH = 200;
|
|
1224
|
+
function sanitizeJsonValue(value, depth) {
|
|
1225
|
+
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1226
|
+
return value;
|
|
1227
|
+
}
|
|
1228
|
+
if (Array.isArray(value)) {
|
|
1229
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1230
|
+
}
|
|
1231
|
+
if (!isPlainObject(value)) {
|
|
1232
|
+
return value;
|
|
1233
|
+
}
|
|
1234
|
+
const sanitized = {};
|
|
1235
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1236
|
+
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1240
|
+
}
|
|
1241
|
+
return sanitized;
|
|
1242
|
+
}
|
|
1243
|
+
function isPlainObject(value) {
|
|
1244
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1245
|
+
}
|
|
964
1246
|
|
|
965
1247
|
// src/stampede/StampedeGuard.ts
|
|
966
1248
|
var import_async_mutex2 = require("async-mutex");
|
|
@@ -972,7 +1254,8 @@ var StampedeGuard = class {
|
|
|
972
1254
|
return await entry.mutex.runExclusive(task);
|
|
973
1255
|
} finally {
|
|
974
1256
|
entry.references -= 1;
|
|
975
|
-
|
|
1257
|
+
const current = this.mutexes.get(key);
|
|
1258
|
+
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
976
1259
|
this.mutexes.delete(key);
|
|
977
1260
|
}
|
|
978
1261
|
}
|
|
@@ -1002,8 +1285,10 @@ var CacheMissError = class extends Error {
|
|
|
1002
1285
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1003
1286
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1004
1287
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1288
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1005
1289
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1006
1290
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1291
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1007
1292
|
var DebugLogger = class {
|
|
1008
1293
|
enabled;
|
|
1009
1294
|
constructor(enabled) {
|
|
@@ -1050,6 +1335,29 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1050
1335
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1051
1336
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1052
1337
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1338
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1339
|
+
layers: this.layers,
|
|
1340
|
+
tagIndex: this.tagIndex,
|
|
1341
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1342
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1343
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1347
|
+
this.logger.warn?.(
|
|
1348
|
+
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false && !layer.keys)) {
|
|
1352
|
+
this.logger.warn?.(
|
|
1353
|
+
"Using the default in-memory TagIndex with a shared cache layer that does not implement keys() can leave invalidateByPattern() and invalidateByPrefix() incomplete after restarts. Use RedisTagIndex or implement keys() on the shared layer."
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
if (options.invalidationBus && options.broadcastL1Invalidation === void 0 && options.publishSetInvalidation === void 0) {
|
|
1357
|
+
this.logger.warn?.(
|
|
1358
|
+
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1053
1361
|
this.initializeWriteBehind(options.writeBehind);
|
|
1054
1362
|
this.startup = this.initialize();
|
|
1055
1363
|
}
|
|
@@ -1062,7 +1370,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1062
1370
|
unsubscribeInvalidation;
|
|
1063
1371
|
logger;
|
|
1064
1372
|
tagIndex;
|
|
1373
|
+
keyDiscovery;
|
|
1065
1374
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1375
|
+
snapshotSerializer = new JsonSerializer();
|
|
1066
1376
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1067
1377
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1068
1378
|
ttlResolver;
|
|
@@ -1071,6 +1381,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1071
1381
|
writeBehindQueue = [];
|
|
1072
1382
|
writeBehindTimer;
|
|
1073
1383
|
writeBehindFlushPromise;
|
|
1384
|
+
generationCleanupPromise;
|
|
1074
1385
|
isDisconnecting = false;
|
|
1075
1386
|
disconnectPromise;
|
|
1076
1387
|
/**
|
|
@@ -1083,6 +1394,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1083
1394
|
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1084
1395
|
this.validateWriteOptions(options);
|
|
1085
1396
|
await this.awaitStartup("get");
|
|
1397
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1398
|
+
}
|
|
1399
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1086
1400
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1087
1401
|
if (hit.found) {
|
|
1088
1402
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1160,6 +1474,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1160
1474
|
return true;
|
|
1161
1475
|
}
|
|
1162
1476
|
} catch {
|
|
1477
|
+
await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
|
|
1163
1478
|
}
|
|
1164
1479
|
} else {
|
|
1165
1480
|
try {
|
|
@@ -1167,7 +1482,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1167
1482
|
if (value !== null) {
|
|
1168
1483
|
return true;
|
|
1169
1484
|
}
|
|
1170
|
-
} catch {
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
await this.reportRecoverableLayerFailure(layer, "has", error);
|
|
1171
1487
|
}
|
|
1172
1488
|
}
|
|
1173
1489
|
}
|
|
@@ -1259,13 +1575,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1259
1575
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1260
1576
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
1261
1577
|
if (!canFastPath) {
|
|
1578
|
+
await this.awaitStartup("mget");
|
|
1262
1579
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1263
1580
|
return Promise.all(
|
|
1264
1581
|
normalizedEntries.map((entry) => {
|
|
1265
1582
|
const optionsSignature = this.serializeOptions(entry.options);
|
|
1266
1583
|
const existing = pendingReads.get(entry.key);
|
|
1267
1584
|
if (!existing) {
|
|
1268
|
-
const promise = this.
|
|
1585
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
1269
1586
|
pendingReads.set(entry.key, {
|
|
1270
1587
|
promise,
|
|
1271
1588
|
fetch: entry.fetch,
|
|
@@ -1404,14 +1721,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1404
1721
|
}
|
|
1405
1722
|
async invalidateByPattern(pattern) {
|
|
1406
1723
|
await this.awaitStartup("invalidateByPattern");
|
|
1407
|
-
const keys = await this.
|
|
1724
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1408
1725
|
await this.deleteKeys(keys);
|
|
1409
1726
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1410
1727
|
}
|
|
1411
1728
|
async invalidateByPrefix(prefix) {
|
|
1412
1729
|
await this.awaitStartup("invalidateByPrefix");
|
|
1413
1730
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1414
|
-
const keys =
|
|
1731
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1415
1732
|
await this.deleteKeys(keys);
|
|
1416
1733
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1417
1734
|
}
|
|
@@ -1461,9 +1778,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1461
1778
|
})
|
|
1462
1779
|
);
|
|
1463
1780
|
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Rotates the active generation prefix used for all future cache keys.
|
|
1783
|
+
* Previous-generation keys remain in the underlying layers until they expire,
|
|
1784
|
+
* unless `generationCleanup` is enabled to prune them in the background.
|
|
1785
|
+
*/
|
|
1464
1786
|
bumpGeneration(nextGeneration) {
|
|
1465
1787
|
const current = this.currentGeneration ?? 0;
|
|
1788
|
+
const previousGeneration = this.currentGeneration;
|
|
1466
1789
|
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1790
|
+
if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
|
|
1791
|
+
this.scheduleGenerationCleanup(previousGeneration);
|
|
1792
|
+
}
|
|
1467
1793
|
return this.currentGeneration;
|
|
1468
1794
|
}
|
|
1469
1795
|
/**
|
|
@@ -1547,27 +1873,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1547
1873
|
this.assertActive("persistToFile");
|
|
1548
1874
|
const snapshot = await this.exportState();
|
|
1549
1875
|
const { promises: fs2 } = await import("fs");
|
|
1550
|
-
await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1876
|
+
await fs2.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
|
|
1551
1877
|
}
|
|
1552
1878
|
async restoreFromFile(filePath) {
|
|
1553
1879
|
this.assertActive("restoreFromFile");
|
|
1554
1880
|
const { promises: fs2 } = await import("fs");
|
|
1555
|
-
const raw = await fs2.readFile(filePath, "utf8");
|
|
1881
|
+
const raw = await fs2.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
|
|
1556
1882
|
let parsed;
|
|
1557
1883
|
try {
|
|
1558
|
-
parsed = JSON.parse(raw
|
|
1559
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1560
|
-
return Object.assign(/* @__PURE__ */ Object.create(null), value);
|
|
1561
|
-
}
|
|
1562
|
-
return value;
|
|
1563
|
-
});
|
|
1884
|
+
parsed = JSON.parse(raw);
|
|
1564
1885
|
} catch (cause) {
|
|
1565
1886
|
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1566
1887
|
}
|
|
1567
1888
|
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1568
1889
|
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1569
1890
|
}
|
|
1570
|
-
await this.importState(
|
|
1891
|
+
await this.importState(
|
|
1892
|
+
parsed.map((entry) => ({
|
|
1893
|
+
key: entry.key,
|
|
1894
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1895
|
+
ttl: entry.ttl
|
|
1896
|
+
}))
|
|
1897
|
+
);
|
|
1571
1898
|
}
|
|
1572
1899
|
async disconnect() {
|
|
1573
1900
|
if (!this.disconnectPromise) {
|
|
@@ -1576,6 +1903,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1576
1903
|
await this.startup;
|
|
1577
1904
|
await this.unsubscribeInvalidation?.();
|
|
1578
1905
|
await this.flushWriteBehindQueue();
|
|
1906
|
+
await this.generationCleanupPromise;
|
|
1579
1907
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1580
1908
|
if (this.writeBehindTimer) {
|
|
1581
1909
|
clearInterval(this.writeBehindTimer);
|
|
@@ -1659,8 +1987,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1659
1987
|
await this.storeEntry(key, "empty", null, options);
|
|
1660
1988
|
return null;
|
|
1661
1989
|
}
|
|
1662
|
-
if (options?.shouldCache
|
|
1663
|
-
|
|
1990
|
+
if (options?.shouldCache) {
|
|
1991
|
+
try {
|
|
1992
|
+
if (!options.shouldCache(fetched)) {
|
|
1993
|
+
return fetched;
|
|
1994
|
+
}
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
1997
|
+
}
|
|
1664
1998
|
}
|
|
1665
1999
|
await this.storeEntry(key, "value", fetched, options);
|
|
1666
2000
|
return fetched;
|
|
@@ -1887,7 +2221,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1887
2221
|
const refresh = (async () => {
|
|
1888
2222
|
this.metricsCollector.increment("refreshes");
|
|
1889
2223
|
try {
|
|
1890
|
-
await this.
|
|
2224
|
+
await this.runBackgroundRefresh(key, fetcher, options);
|
|
1891
2225
|
} catch (error) {
|
|
1892
2226
|
this.metricsCollector.increment("refreshErrors");
|
|
1893
2227
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -1897,6 +2231,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1897
2231
|
})();
|
|
1898
2232
|
this.backgroundRefreshes.set(key, refresh);
|
|
1899
2233
|
}
|
|
2234
|
+
async runBackgroundRefresh(key, fetcher, options) {
|
|
2235
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2236
|
+
await this.fetchWithGuards(
|
|
2237
|
+
key,
|
|
2238
|
+
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2239
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2240
|
+
}),
|
|
2241
|
+
options
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
1900
2244
|
resolveSingleFlightOptions() {
|
|
1901
2245
|
return {
|
|
1902
2246
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
@@ -1964,8 +2308,76 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1964
2308
|
sleep(ms) {
|
|
1965
2309
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1966
2310
|
}
|
|
2311
|
+
async withTimeout(promise, timeoutMs, onTimeout) {
|
|
2312
|
+
if (timeoutMs <= 0) {
|
|
2313
|
+
return promise;
|
|
2314
|
+
}
|
|
2315
|
+
let timer;
|
|
2316
|
+
const observedPromise = promise.then(
|
|
2317
|
+
(value) => ({ kind: "value", value }),
|
|
2318
|
+
(error) => ({ kind: "error", error })
|
|
2319
|
+
);
|
|
2320
|
+
try {
|
|
2321
|
+
const result = await Promise.race([
|
|
2322
|
+
observedPromise,
|
|
2323
|
+
new Promise((_, reject) => {
|
|
2324
|
+
timer = setTimeout(() => reject(onTimeout()), timeoutMs);
|
|
2325
|
+
timer.unref?.();
|
|
2326
|
+
})
|
|
2327
|
+
]);
|
|
2328
|
+
if (result && typeof result === "object" && "kind" in result) {
|
|
2329
|
+
if (result.kind === "error") {
|
|
2330
|
+
throw result.error;
|
|
2331
|
+
}
|
|
2332
|
+
return result.value;
|
|
2333
|
+
}
|
|
2334
|
+
return result;
|
|
2335
|
+
} finally {
|
|
2336
|
+
if (timer) {
|
|
2337
|
+
clearTimeout(timer);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
1967
2341
|
shouldBroadcastL1Invalidation() {
|
|
1968
|
-
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ??
|
|
2342
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2343
|
+
}
|
|
2344
|
+
shouldCleanupGenerations() {
|
|
2345
|
+
return Boolean(this.options.generationCleanup);
|
|
2346
|
+
}
|
|
2347
|
+
generationCleanupBatchSize() {
|
|
2348
|
+
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2349
|
+
return configured ?? 500;
|
|
2350
|
+
}
|
|
2351
|
+
scheduleGenerationCleanup(generation) {
|
|
2352
|
+
const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
|
|
2353
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
2354
|
+
generation,
|
|
2355
|
+
error: this.formatError(error)
|
|
2356
|
+
});
|
|
2357
|
+
});
|
|
2358
|
+
this.generationCleanupPromise = task.finally(() => {
|
|
2359
|
+
if (this.generationCleanupPromise === task) {
|
|
2360
|
+
this.generationCleanupPromise = void 0;
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
async cleanupGeneration(generation) {
|
|
2365
|
+
const prefix = `v${generation}:`;
|
|
2366
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2367
|
+
if (keys.length === 0) {
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
const batchSize = this.generationCleanupBatchSize();
|
|
2371
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2372
|
+
const batch = keys.slice(index, index + batchSize);
|
|
2373
|
+
await this.deleteKeys(batch);
|
|
2374
|
+
await this.publishInvalidation({
|
|
2375
|
+
scope: "keys",
|
|
2376
|
+
keys: batch,
|
|
2377
|
+
sourceId: this.instanceId,
|
|
2378
|
+
operation: "invalidate"
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
1969
2381
|
}
|
|
1970
2382
|
initializeWriteBehind(options) {
|
|
1971
2383
|
if (this.options.writeStrategy !== "write-behind") {
|
|
@@ -2003,7 +2415,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2003
2415
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2004
2416
|
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2005
2417
|
this.writeBehindFlushPromise = (async () => {
|
|
2006
|
-
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2418
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2419
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2420
|
+
if (failures.length > 0) {
|
|
2421
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2422
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
2423
|
+
failed: failures.length,
|
|
2424
|
+
total: batch.length,
|
|
2425
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2426
|
+
});
|
|
2427
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2428
|
+
}
|
|
2007
2429
|
})();
|
|
2008
2430
|
await this.writeBehindFlushPromise;
|
|
2009
2431
|
this.writeBehindFlushPromise = void 0;
|
|
@@ -2108,9 +2530,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2108
2530
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2109
2531
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2110
2532
|
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2533
|
+
this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2111
2534
|
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2112
2535
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2113
2536
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2537
|
+
if (typeof this.options.generationCleanup === "object") {
|
|
2538
|
+
this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2539
|
+
}
|
|
2114
2540
|
if (this.options.generation !== void 0) {
|
|
2115
2541
|
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2116
2542
|
}
|
|
@@ -2182,6 +2608,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2182
2608
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2183
2609
|
throw new Error("Cache key contains unsupported control characters.");
|
|
2184
2610
|
}
|
|
2611
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2612
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2613
|
+
}
|
|
2185
2614
|
return key;
|
|
2186
2615
|
}
|
|
2187
2616
|
validateTtlPolicy(name, policy) {
|
|
@@ -2259,6 +2688,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2259
2688
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
2260
2689
|
return null;
|
|
2261
2690
|
}
|
|
2691
|
+
async reportRecoverableLayerFailure(layer, operation, error) {
|
|
2692
|
+
if (this.isGracefulDegradationEnabled()) {
|
|
2693
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
this.logger.warn?.("layer-operation-failed", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2697
|
+
this.emitError(operation, { layer: layer.name, degraded: false, error: this.formatError(error) });
|
|
2698
|
+
}
|
|
2262
2699
|
isGracefulDegradationEnabled() {
|
|
2263
2700
|
return Boolean(this.options.gracefulDegradation);
|
|
2264
2701
|
}
|
|
@@ -2282,10 +2719,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2282
2719
|
}
|
|
2283
2720
|
}
|
|
2284
2721
|
serializeKeyPart(value) {
|
|
2285
|
-
if (typeof value === "string"
|
|
2286
|
-
return
|
|
2722
|
+
if (typeof value === "string") {
|
|
2723
|
+
return `s:${value}`;
|
|
2724
|
+
}
|
|
2725
|
+
if (typeof value === "number") {
|
|
2726
|
+
return `n:${value}`;
|
|
2727
|
+
}
|
|
2728
|
+
if (typeof value === "boolean") {
|
|
2729
|
+
return `b:${value}`;
|
|
2287
2730
|
}
|
|
2288
|
-
return JSON.stringify(this.normalizeForSerialization(value))
|
|
2731
|
+
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2289
2732
|
}
|
|
2290
2733
|
isCacheSnapshotEntries(value) {
|
|
2291
2734
|
return Array.isArray(value) && value.every((entry) => {
|
|
@@ -2293,15 +2736,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2293
2736
|
return false;
|
|
2294
2737
|
}
|
|
2295
2738
|
const candidate = entry;
|
|
2296
|
-
return typeof candidate.key === "string";
|
|
2739
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
2297
2740
|
});
|
|
2298
2741
|
}
|
|
2742
|
+
sanitizeSnapshotValue(value) {
|
|
2743
|
+
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2744
|
+
}
|
|
2745
|
+
async validateSnapshotFilePath(filePath) {
|
|
2746
|
+
if (filePath.length === 0) {
|
|
2747
|
+
throw new Error("filePath must not be empty.");
|
|
2748
|
+
}
|
|
2749
|
+
if (filePath.includes("\0")) {
|
|
2750
|
+
throw new Error("filePath must not contain null bytes.");
|
|
2751
|
+
}
|
|
2752
|
+
const path = await import("path");
|
|
2753
|
+
const resolved = path.resolve(filePath);
|
|
2754
|
+
const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
|
|
2755
|
+
if (baseDir !== false) {
|
|
2756
|
+
const relative = path.relative(baseDir, resolved);
|
|
2757
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2758
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return resolved;
|
|
2762
|
+
}
|
|
2299
2763
|
normalizeForSerialization(value) {
|
|
2300
2764
|
if (Array.isArray(value)) {
|
|
2301
2765
|
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
2302
2766
|
}
|
|
2303
2767
|
if (value && typeof value === "object") {
|
|
2304
2768
|
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
2769
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
2770
|
+
return normalized;
|
|
2771
|
+
}
|
|
2305
2772
|
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
2306
2773
|
return normalized;
|
|
2307
2774
|
}, {});
|
|
@@ -2562,7 +3029,7 @@ function createCachedMethodDecorator(options) {
|
|
|
2562
3029
|
function createFastifyLayercachePlugin(cache, options = {}) {
|
|
2563
3030
|
return async (fastify) => {
|
|
2564
3031
|
fastify.decorate("cache", cache);
|
|
2565
|
-
if (options.exposeStatsRoute
|
|
3032
|
+
if (options.exposeStatsRoute === true && fastify.get) {
|
|
2566
3033
|
fastify.get(options.statsPath ?? "/cache/stats", async () => cache.getStats());
|
|
2567
3034
|
}
|
|
2568
3035
|
};
|
|
@@ -2578,7 +3045,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
2578
3045
|
next();
|
|
2579
3046
|
return;
|
|
2580
3047
|
}
|
|
2581
|
-
const
|
|
3048
|
+
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
3049
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
|
|
2582
3050
|
const cached = await cache.get(key, void 0, options);
|
|
2583
3051
|
if (cached !== null) {
|
|
2584
3052
|
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
@@ -2594,7 +3062,12 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
2594
3062
|
if (originalJson) {
|
|
2595
3063
|
res.json = (body) => {
|
|
2596
3064
|
res.setHeader?.("x-cache", "MISS");
|
|
2597
|
-
|
|
3065
|
+
cache.set(key, body, options).catch((err) => {
|
|
3066
|
+
cache.emit("error", {
|
|
3067
|
+
operation: "set",
|
|
3068
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3069
|
+
});
|
|
3070
|
+
});
|
|
2598
3071
|
return originalJson(body);
|
|
2599
3072
|
};
|
|
2600
3073
|
}
|
|
@@ -2604,6 +3077,15 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
2604
3077
|
}
|
|
2605
3078
|
};
|
|
2606
3079
|
}
|
|
3080
|
+
function normalizeUrl(url) {
|
|
3081
|
+
try {
|
|
3082
|
+
const parsed = new URL(url, "http://localhost");
|
|
3083
|
+
parsed.searchParams.sort();
|
|
3084
|
+
return decodeURIComponent(parsed.pathname) + parsed.search;
|
|
3085
|
+
} catch {
|
|
3086
|
+
return url;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
2607
3089
|
|
|
2608
3090
|
// src/integrations/graphql.ts
|
|
2609
3091
|
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
@@ -2623,7 +3105,8 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
2623
3105
|
await next();
|
|
2624
3106
|
return;
|
|
2625
3107
|
}
|
|
2626
|
-
const
|
|
3108
|
+
const rawPath = context.req.path ?? context.req.url ?? "/";
|
|
3109
|
+
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl2(rawPath)}`;
|
|
2627
3110
|
const cached = await cache.get(key, void 0, options);
|
|
2628
3111
|
if (cached !== null) {
|
|
2629
3112
|
context.header?.("x-cache", "HIT");
|
|
@@ -2634,12 +3117,26 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
2634
3117
|
const originalJson = context.json.bind(context);
|
|
2635
3118
|
context.json = (body, status) => {
|
|
2636
3119
|
context.header?.("x-cache", "MISS");
|
|
2637
|
-
|
|
3120
|
+
cache.set(key, body, options).catch((err) => {
|
|
3121
|
+
cache.emit("error", {
|
|
3122
|
+
operation: "set",
|
|
3123
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3124
|
+
});
|
|
3125
|
+
});
|
|
2638
3126
|
return originalJson(body, status);
|
|
2639
3127
|
};
|
|
2640
3128
|
await next();
|
|
2641
3129
|
};
|
|
2642
3130
|
}
|
|
3131
|
+
function normalizeUrl2(url) {
|
|
3132
|
+
try {
|
|
3133
|
+
const parsed = new URL(url, "http://localhost");
|
|
3134
|
+
parsed.searchParams.sort();
|
|
3135
|
+
return decodeURIComponent(parsed.pathname) + parsed.search;
|
|
3136
|
+
} catch {
|
|
3137
|
+
return url;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
2643
3140
|
|
|
2644
3141
|
// src/integrations/opentelemetry.ts
|
|
2645
3142
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
@@ -2774,16 +3271,10 @@ var MemoryLayer = class {
|
|
|
2774
3271
|
return entry.value;
|
|
2775
3272
|
}
|
|
2776
3273
|
async getMany(keys) {
|
|
2777
|
-
|
|
2778
|
-
for (const key of keys) {
|
|
2779
|
-
values.push(await this.getEntry(key));
|
|
2780
|
-
}
|
|
2781
|
-
return values;
|
|
3274
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
2782
3275
|
}
|
|
2783
3276
|
async setMany(entries) {
|
|
2784
|
-
|
|
2785
|
-
await this.set(entry.key, entry.value, entry.ttl);
|
|
2786
|
-
}
|
|
3277
|
+
await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
|
|
2787
3278
|
}
|
|
2788
3279
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2789
3280
|
this.entries.delete(key);
|
|
@@ -2919,39 +3410,6 @@ var MemoryLayer = class {
|
|
|
2919
3410
|
// src/layers/RedisLayer.ts
|
|
2920
3411
|
var import_node_util = require("util");
|
|
2921
3412
|
var import_node_zlib = require("zlib");
|
|
2922
|
-
|
|
2923
|
-
// src/serialization/JsonSerializer.ts
|
|
2924
|
-
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2925
|
-
var JsonSerializer = class {
|
|
2926
|
-
serialize(value) {
|
|
2927
|
-
return JSON.stringify(value);
|
|
2928
|
-
}
|
|
2929
|
-
deserialize(payload) {
|
|
2930
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2931
|
-
return sanitizeJsonValue(JSON.parse(normalized));
|
|
2932
|
-
}
|
|
2933
|
-
};
|
|
2934
|
-
function sanitizeJsonValue(value) {
|
|
2935
|
-
if (Array.isArray(value)) {
|
|
2936
|
-
return value.map((entry) => sanitizeJsonValue(entry));
|
|
2937
|
-
}
|
|
2938
|
-
if (!isPlainObject(value)) {
|
|
2939
|
-
return value;
|
|
2940
|
-
}
|
|
2941
|
-
const sanitized = {};
|
|
2942
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2943
|
-
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
2944
|
-
continue;
|
|
2945
|
-
}
|
|
2946
|
-
sanitized[key] = sanitizeJsonValue(entry);
|
|
2947
|
-
}
|
|
2948
|
-
return sanitized;
|
|
2949
|
-
}
|
|
2950
|
-
function isPlainObject(value) {
|
|
2951
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
// src/layers/RedisLayer.ts
|
|
2955
3413
|
var BATCH_DELETE_SIZE = 500;
|
|
2956
3414
|
var gzipAsync = (0, import_node_util.promisify)(import_node_zlib.gzip);
|
|
2957
3415
|
var gunzipAsync = (0, import_node_util.promisify)(import_node_zlib.gunzip);
|
|
@@ -2968,6 +3426,7 @@ var RedisLayer = class {
|
|
|
2968
3426
|
scanCount;
|
|
2969
3427
|
compression;
|
|
2970
3428
|
compressionThreshold;
|
|
3429
|
+
decompressionMaxBytes;
|
|
2971
3430
|
disconnectOnDispose;
|
|
2972
3431
|
constructor(options) {
|
|
2973
3432
|
this.client = options.client;
|
|
@@ -2979,6 +3438,7 @@ var RedisLayer = class {
|
|
|
2979
3438
|
this.scanCount = options.scanCount ?? 100;
|
|
2980
3439
|
this.compression = options.compression;
|
|
2981
3440
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
3441
|
+
this.decompressionMaxBytes = options.decompressionMaxBytes ?? 64 * 1024 * 1024;
|
|
2982
3442
|
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
2983
3443
|
}
|
|
2984
3444
|
async get(key) {
|
|
@@ -3178,16 +3638,29 @@ var RedisLayer = class {
|
|
|
3178
3638
|
}
|
|
3179
3639
|
/**
|
|
3180
3640
|
* Decompresses the payload asynchronously if a compression header is present.
|
|
3641
|
+
* Enforces a maximum decompressed size to prevent decompression bomb attacks.
|
|
3181
3642
|
*/
|
|
3182
3643
|
async decodePayload(payload) {
|
|
3183
3644
|
if (!Buffer.isBuffer(payload)) {
|
|
3184
3645
|
return payload;
|
|
3185
3646
|
}
|
|
3186
3647
|
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
3187
|
-
|
|
3648
|
+
const decompressed = await gunzipAsync(payload.subarray(10));
|
|
3649
|
+
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3650
|
+
throw new Error(
|
|
3651
|
+
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3652
|
+
);
|
|
3653
|
+
}
|
|
3654
|
+
return decompressed;
|
|
3188
3655
|
}
|
|
3189
3656
|
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
3190
|
-
|
|
3657
|
+
const decompressed = await brotliDecompressAsync(payload.subarray(12));
|
|
3658
|
+
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3659
|
+
throw new Error(
|
|
3660
|
+
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
return decompressed;
|
|
3191
3664
|
}
|
|
3192
3665
|
return payload;
|
|
3193
3666
|
}
|
|
@@ -3247,8 +3720,13 @@ var DiskLayer = class {
|
|
|
3247
3720
|
const payload = this.serializer.serialize(entry);
|
|
3248
3721
|
const targetPath = this.keyToPath(key);
|
|
3249
3722
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3250
|
-
|
|
3251
|
-
|
|
3723
|
+
try {
|
|
3724
|
+
await import_node_fs.promises.writeFile(tempPath, payload);
|
|
3725
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
await this.safeDelete(tempPath);
|
|
3728
|
+
throw error;
|
|
3729
|
+
}
|
|
3252
3730
|
if (this.maxFiles !== void 0) {
|
|
3253
3731
|
await this.enforceMaxFiles();
|
|
3254
3732
|
}
|
|
@@ -3258,9 +3736,7 @@ var DiskLayer = class {
|
|
|
3258
3736
|
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
3259
3737
|
}
|
|
3260
3738
|
async setMany(entries) {
|
|
3261
|
-
|
|
3262
|
-
await this.set(entry.key, entry.value, entry.ttl);
|
|
3263
|
-
}
|
|
3739
|
+
await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
|
|
3264
3740
|
}
|
|
3265
3741
|
async has(key) {
|
|
3266
3742
|
const value = await this.getEntry(key);
|
|
@@ -3464,6 +3940,7 @@ var MemcachedLayer = class {
|
|
|
3464
3940
|
return unwrapStoredValue(await this.getEntry(key));
|
|
3465
3941
|
}
|
|
3466
3942
|
async getEntry(key) {
|
|
3943
|
+
this.validateKey(key);
|
|
3467
3944
|
const result = await this.client.get(this.withPrefix(key));
|
|
3468
3945
|
if (!result || result.value === null) {
|
|
3469
3946
|
return null;
|
|
@@ -3478,16 +3955,19 @@ var MemcachedLayer = class {
|
|
|
3478
3955
|
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
3479
3956
|
}
|
|
3480
3957
|
async set(key, value, ttl = this.defaultTtl) {
|
|
3958
|
+
this.validateKey(key);
|
|
3481
3959
|
const payload = this.serializer.serialize(value);
|
|
3482
3960
|
await this.client.set(this.withPrefix(key), payload, {
|
|
3483
3961
|
expires: ttl && ttl > 0 ? ttl : void 0
|
|
3484
3962
|
});
|
|
3485
3963
|
}
|
|
3486
3964
|
async has(key) {
|
|
3965
|
+
this.validateKey(key);
|
|
3487
3966
|
const result = await this.client.get(this.withPrefix(key));
|
|
3488
3967
|
return result !== null && result.value !== null;
|
|
3489
3968
|
}
|
|
3490
3969
|
async delete(key) {
|
|
3970
|
+
this.validateKey(key);
|
|
3491
3971
|
await this.client.delete(this.withPrefix(key));
|
|
3492
3972
|
}
|
|
3493
3973
|
async deleteMany(keys) {
|
|
@@ -3501,19 +3981,50 @@ var MemcachedLayer = class {
|
|
|
3501
3981
|
withPrefix(key) {
|
|
3502
3982
|
return `${this.keyPrefix}${key}`;
|
|
3503
3983
|
}
|
|
3984
|
+
validateKey(key) {
|
|
3985
|
+
const fullKey = this.withPrefix(key);
|
|
3986
|
+
if (Buffer.byteLength(fullKey, "utf8") > 250) {
|
|
3987
|
+
throw new Error(`MemcachedLayer: key exceeds 250-byte Memcached limit: "${fullKey.slice(0, 60)}..."`);
|
|
3988
|
+
}
|
|
3989
|
+
if (/[\s\x00-\x1f\x7f]/.test(fullKey)) {
|
|
3990
|
+
throw new Error(
|
|
3991
|
+
"MemcachedLayer: key contains invalid characters (whitespace or control characters are not allowed)."
|
|
3992
|
+
);
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3504
3995
|
};
|
|
3505
3996
|
|
|
3506
3997
|
// src/serialization/MsgpackSerializer.ts
|
|
3507
3998
|
var import_msgpack = require("@msgpack/msgpack");
|
|
3999
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3508
4000
|
var MsgpackSerializer = class {
|
|
3509
4001
|
serialize(value) {
|
|
3510
4002
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
3511
4003
|
}
|
|
3512
4004
|
deserialize(payload) {
|
|
3513
4005
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
3514
|
-
return (0, import_msgpack.decode)(normalized);
|
|
4006
|
+
return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized));
|
|
3515
4007
|
}
|
|
3516
4008
|
};
|
|
4009
|
+
function sanitizeMsgpackValue(value) {
|
|
4010
|
+
if (Array.isArray(value)) {
|
|
4011
|
+
return value.map((entry) => sanitizeMsgpackValue(entry));
|
|
4012
|
+
}
|
|
4013
|
+
if (!isPlainObject2(value)) {
|
|
4014
|
+
return value;
|
|
4015
|
+
}
|
|
4016
|
+
const sanitized = {};
|
|
4017
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4018
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
4019
|
+
continue;
|
|
4020
|
+
}
|
|
4021
|
+
sanitized[key] = sanitizeMsgpackValue(entry);
|
|
4022
|
+
}
|
|
4023
|
+
return sanitized;
|
|
4024
|
+
}
|
|
4025
|
+
function isPlainObject2(value) {
|
|
4026
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
4027
|
+
}
|
|
3517
4028
|
|
|
3518
4029
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
3519
4030
|
var import_node_crypto2 = require("crypto");
|
|
@@ -3642,7 +4153,7 @@ function createPrometheusMetricsExporter(stacks) {
|
|
|
3642
4153
|
};
|
|
3643
4154
|
}
|
|
3644
4155
|
function sanitizeLabel(value) {
|
|
3645
|
-
return value.replace(/["\\\n]/g, "_");
|
|
4156
|
+
return value.replace(/["\\\n\r]/g, "_");
|
|
3646
4157
|
}
|
|
3647
4158
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3648
4159
|
0 && (module.exports = {
|