layercache 1.2.3 → 1.2.5
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 +101 -84
- package/dist/{chunk-KOYGHLVP.js → chunk-JC26W3KK.js} +27 -7
- package/dist/cli.cjs +2 -16
- package/dist/cli.js +2 -16
- package/dist/{edge-B_rUqDy6.d.cts → edge-P07GCO2Y.d.cts} +2 -2
- package/dist/{edge-B_rUqDy6.d.ts → edge-P07GCO2Y.d.ts} +2 -2
- package/dist/edge.cjs +27 -7
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +235 -125
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +161 -71
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +209 -118
- package/packages/nestjs/dist/index.d.cts +2 -2
- package/packages/nestjs/dist/index.d.ts +2 -2
- package/packages/nestjs/dist/index.js +209 -118
|
@@ -338,6 +338,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
338
338
|
* ```
|
|
339
339
|
*/
|
|
340
340
|
namespace(childPrefix) {
|
|
341
|
+
validateNamespaceKey(childPrefix);
|
|
341
342
|
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
342
343
|
}
|
|
343
344
|
qualify(key) {
|
|
@@ -467,6 +468,118 @@ function addMap(base, delta) {
|
|
|
467
468
|
}
|
|
468
469
|
return result;
|
|
469
470
|
}
|
|
471
|
+
function validateNamespaceKey(key) {
|
|
472
|
+
if (key.length === 0) {
|
|
473
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
474
|
+
}
|
|
475
|
+
if (key.length > 256) {
|
|
476
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
477
|
+
}
|
|
478
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
479
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
484
|
+
var PatternMatcher = class _PatternMatcher {
|
|
485
|
+
/**
|
|
486
|
+
* Tests whether a glob-style pattern matches a value.
|
|
487
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
488
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
489
|
+
* quadratic memory usage on long patterns/keys.
|
|
490
|
+
*/
|
|
491
|
+
static matches(pattern, value) {
|
|
492
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
496
|
+
*/
|
|
497
|
+
static matchLinear(pattern, value) {
|
|
498
|
+
let patternIndex = 0;
|
|
499
|
+
let valueIndex = 0;
|
|
500
|
+
let starIndex = -1;
|
|
501
|
+
let backtrackValueIndex = 0;
|
|
502
|
+
while (valueIndex < value.length) {
|
|
503
|
+
const patternChar = pattern[patternIndex];
|
|
504
|
+
const valueChar = value[valueIndex];
|
|
505
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
506
|
+
starIndex = patternIndex;
|
|
507
|
+
patternIndex += 1;
|
|
508
|
+
backtrackValueIndex = valueIndex;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
512
|
+
patternIndex += 1;
|
|
513
|
+
valueIndex += 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (starIndex !== -1) {
|
|
517
|
+
patternIndex = starIndex + 1;
|
|
518
|
+
backtrackValueIndex += 1;
|
|
519
|
+
valueIndex = backtrackValueIndex;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
525
|
+
patternIndex += 1;
|
|
526
|
+
}
|
|
527
|
+
return patternIndex === pattern.length;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// ../../src/internal/CacheKeyDiscovery.ts
|
|
532
|
+
var CacheKeyDiscovery = class {
|
|
533
|
+
constructor(options) {
|
|
534
|
+
this.options = options;
|
|
535
|
+
}
|
|
536
|
+
options;
|
|
537
|
+
async collectKeysWithPrefix(prefix) {
|
|
538
|
+
const { tagIndex } = this.options;
|
|
539
|
+
const matches = new Set(
|
|
540
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
541
|
+
);
|
|
542
|
+
await Promise.all(
|
|
543
|
+
this.options.layers.map(async (layer) => {
|
|
544
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
const keys = await layer.keys();
|
|
549
|
+
for (const key of keys) {
|
|
550
|
+
if (key.startsWith(prefix)) {
|
|
551
|
+
matches.add(key);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
return [...matches];
|
|
560
|
+
}
|
|
561
|
+
async collectKeysMatchingPattern(pattern) {
|
|
562
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
563
|
+
await Promise.all(
|
|
564
|
+
this.options.layers.map(async (layer) => {
|
|
565
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const keys = await layer.keys();
|
|
570
|
+
for (const key of keys) {
|
|
571
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
572
|
+
matches.add(key);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} catch (error) {
|
|
576
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
577
|
+
}
|
|
578
|
+
})
|
|
579
|
+
);
|
|
580
|
+
return [...matches];
|
|
581
|
+
}
|
|
582
|
+
};
|
|
470
583
|
|
|
471
584
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
472
585
|
var CircuitBreakerManager = class {
|
|
@@ -486,9 +599,7 @@ var CircuitBreakerManager = class {
|
|
|
486
599
|
}
|
|
487
600
|
const now = Date.now();
|
|
488
601
|
if (state.openUntil <= now) {
|
|
489
|
-
|
|
490
|
-
state.failures = 0;
|
|
491
|
-
this.breakers.set(key, state);
|
|
602
|
+
this.breakers.delete(key);
|
|
492
603
|
return;
|
|
493
604
|
}
|
|
494
605
|
const remainingMs = state.openUntil - now;
|
|
@@ -499,15 +610,15 @@ var CircuitBreakerManager = class {
|
|
|
499
610
|
if (!options) {
|
|
500
611
|
return;
|
|
501
612
|
}
|
|
613
|
+
this.pruneIfNeeded();
|
|
502
614
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
503
615
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
504
|
-
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
616
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
505
617
|
state.failures += 1;
|
|
506
618
|
if (state.failures >= failureThreshold) {
|
|
507
619
|
state.openUntil = Date.now() + cooldownMs;
|
|
508
620
|
}
|
|
509
621
|
this.breakers.set(key, state);
|
|
510
|
-
this.pruneIfNeeded();
|
|
511
622
|
}
|
|
512
623
|
recordSuccess(key) {
|
|
513
624
|
this.breakers.delete(key);
|
|
@@ -518,8 +629,7 @@ var CircuitBreakerManager = class {
|
|
|
518
629
|
return false;
|
|
519
630
|
}
|
|
520
631
|
if (state.openUntil <= Date.now()) {
|
|
521
|
-
|
|
522
|
-
state.failures = 0;
|
|
632
|
+
this.breakers.delete(key);
|
|
523
633
|
return false;
|
|
524
634
|
}
|
|
525
635
|
return true;
|
|
@@ -543,15 +653,20 @@ var CircuitBreakerManager = class {
|
|
|
543
653
|
if (this.breakers.size <= this.maxEntries) {
|
|
544
654
|
return;
|
|
545
655
|
}
|
|
656
|
+
const now = Date.now();
|
|
546
657
|
for (const [key, state] of this.breakers.entries()) {
|
|
547
658
|
if (this.breakers.size <= this.maxEntries) {
|
|
548
|
-
|
|
659
|
+
return;
|
|
549
660
|
}
|
|
550
|
-
if (!state.openUntil || state.openUntil <=
|
|
661
|
+
if (!state.openUntil || state.openUntil <= now) {
|
|
551
662
|
this.breakers.delete(key);
|
|
552
663
|
}
|
|
553
664
|
}
|
|
554
|
-
|
|
665
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
669
|
+
for (const [key] of sorted) {
|
|
555
670
|
if (this.breakers.size <= this.maxEntries) {
|
|
556
671
|
break;
|
|
557
672
|
}
|
|
@@ -561,6 +676,7 @@ var CircuitBreakerManager = class {
|
|
|
561
676
|
};
|
|
562
677
|
|
|
563
678
|
// ../../src/internal/FetchRateLimiter.ts
|
|
679
|
+
var MAX_BUCKETS = 1e4;
|
|
564
680
|
var FetchRateLimiter = class {
|
|
565
681
|
buckets = /* @__PURE__ */ new Map();
|
|
566
682
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -726,10 +842,25 @@ var FetchRateLimiter = class {
|
|
|
726
842
|
if (existing) {
|
|
727
843
|
return existing;
|
|
728
844
|
}
|
|
845
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
846
|
+
this.evictIdleBuckets();
|
|
847
|
+
}
|
|
729
848
|
const bucket = { active: 0, startedAt: [] };
|
|
730
849
|
this.buckets.set(bucketKey, bucket);
|
|
731
850
|
return bucket;
|
|
732
851
|
}
|
|
852
|
+
evictIdleBuckets() {
|
|
853
|
+
for (const [key, bucket] of this.buckets.entries()) {
|
|
854
|
+
if (this.buckets.size <= MAX_BUCKETS * 0.9) {
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
|
|
858
|
+
this.buckets.delete(key);
|
|
859
|
+
this.queuesByBucket.delete(key);
|
|
860
|
+
this.pendingBuckets.delete(key);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
733
864
|
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
734
865
|
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
735
866
|
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
@@ -1056,66 +1187,18 @@ var TtlResolver = class {
|
|
|
1056
1187
|
return;
|
|
1057
1188
|
}
|
|
1058
1189
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
1059
|
-
|
|
1060
|
-
for (
|
|
1061
|
-
|
|
1062
|
-
|
|
1190
|
+
const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
1191
|
+
for (let i = 0; i < toRemove && i < sorted.length; i++) {
|
|
1192
|
+
const entry = sorted[i];
|
|
1193
|
+
if (entry) {
|
|
1194
|
+
this.accessProfiles.delete(entry[0]);
|
|
1063
1195
|
}
|
|
1064
|
-
this.accessProfiles.delete(key);
|
|
1065
|
-
removed += 1;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
|
-
// ../../src/invalidation/PatternMatcher.ts
|
|
1071
|
-
var PatternMatcher = class _PatternMatcher {
|
|
1072
|
-
/**
|
|
1073
|
-
* Tests whether a glob-style pattern matches a value.
|
|
1074
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
1075
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
1076
|
-
* quadratic memory usage on long patterns/keys.
|
|
1077
|
-
*/
|
|
1078
|
-
static matches(pattern, value) {
|
|
1079
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
1083
|
-
*/
|
|
1084
|
-
static matchLinear(pattern, value) {
|
|
1085
|
-
let patternIndex = 0;
|
|
1086
|
-
let valueIndex = 0;
|
|
1087
|
-
let starIndex = -1;
|
|
1088
|
-
let backtrackValueIndex = 0;
|
|
1089
|
-
while (valueIndex < value.length) {
|
|
1090
|
-
const patternChar = pattern[patternIndex];
|
|
1091
|
-
const valueChar = value[valueIndex];
|
|
1092
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
1093
|
-
starIndex = patternIndex;
|
|
1094
|
-
patternIndex += 1;
|
|
1095
|
-
backtrackValueIndex = valueIndex;
|
|
1096
|
-
continue;
|
|
1097
|
-
}
|
|
1098
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
1099
|
-
patternIndex += 1;
|
|
1100
|
-
valueIndex += 1;
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
if (starIndex !== -1) {
|
|
1104
|
-
patternIndex = starIndex + 1;
|
|
1105
|
-
backtrackValueIndex += 1;
|
|
1106
|
-
valueIndex = backtrackValueIndex;
|
|
1107
|
-
continue;
|
|
1108
|
-
}
|
|
1109
|
-
return false;
|
|
1110
|
-
}
|
|
1111
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
1112
|
-
patternIndex += 1;
|
|
1113
1196
|
}
|
|
1114
|
-
return patternIndex === pattern.length;
|
|
1115
1197
|
}
|
|
1116
1198
|
};
|
|
1117
1199
|
|
|
1118
1200
|
// ../../src/invalidation/TagIndex.ts
|
|
1201
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
1119
1202
|
var TagIndex = class {
|
|
1120
1203
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
1121
1204
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -1170,7 +1253,7 @@ var TagIndex = class {
|
|
|
1170
1253
|
}
|
|
1171
1254
|
async matchPattern(pattern) {
|
|
1172
1255
|
const matches = /* @__PURE__ */ new Set();
|
|
1173
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1256
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1174
1257
|
return [...matches];
|
|
1175
1258
|
}
|
|
1176
1259
|
async clear() {
|
|
@@ -1222,7 +1305,10 @@ var TagIndex = class {
|
|
|
1222
1305
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1223
1306
|
}
|
|
1224
1307
|
}
|
|
1225
|
-
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
1308
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1309
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1226
1312
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
1227
1313
|
if (visited.has(stateKey)) {
|
|
1228
1314
|
return;
|
|
@@ -1239,21 +1325,37 @@ var TagIndex = class {
|
|
|
1239
1325
|
return;
|
|
1240
1326
|
}
|
|
1241
1327
|
if (patternChar === "*") {
|
|
1242
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1328
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
1243
1329
|
for (const [character, child2] of node.children) {
|
|
1244
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1330
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
1245
1331
|
}
|
|
1246
1332
|
return;
|
|
1247
1333
|
}
|
|
1248
1334
|
if (patternChar === "?") {
|
|
1249
1335
|
for (const [character, child2] of node.children) {
|
|
1250
|
-
this.collectPatternMatches(
|
|
1336
|
+
this.collectPatternMatches(
|
|
1337
|
+
child2,
|
|
1338
|
+
`${prefix}${character}`,
|
|
1339
|
+
pattern,
|
|
1340
|
+
patternIndex + 1,
|
|
1341
|
+
matches,
|
|
1342
|
+
visited,
|
|
1343
|
+
depth + 1
|
|
1344
|
+
);
|
|
1251
1345
|
}
|
|
1252
1346
|
return;
|
|
1253
1347
|
}
|
|
1254
1348
|
const child = node.children.get(patternChar);
|
|
1255
1349
|
if (child) {
|
|
1256
|
-
this.collectPatternMatches(
|
|
1350
|
+
this.collectPatternMatches(
|
|
1351
|
+
child,
|
|
1352
|
+
`${prefix}${patternChar}`,
|
|
1353
|
+
pattern,
|
|
1354
|
+
patternIndex + 1,
|
|
1355
|
+
matches,
|
|
1356
|
+
visited,
|
|
1357
|
+
depth + 1
|
|
1358
|
+
);
|
|
1257
1359
|
}
|
|
1258
1360
|
}
|
|
1259
1361
|
pruneKnownKeysIfNeeded() {
|
|
@@ -1395,6 +1497,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
|
1395
1497
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1396
1498
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1397
1499
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1500
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
1398
1501
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1399
1502
|
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1400
1503
|
var DebugLogger = class {
|
|
@@ -1443,6 +1546,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1443
1546
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1444
1547
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1445
1548
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1549
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1550
|
+
layers: this.layers,
|
|
1551
|
+
tagIndex: this.tagIndex,
|
|
1552
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1553
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1554
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1446
1557
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1447
1558
|
this.logger.warn?.(
|
|
1448
1559
|
"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."
|
|
@@ -1470,6 +1581,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1470
1581
|
unsubscribeInvalidation;
|
|
1471
1582
|
logger;
|
|
1472
1583
|
tagIndex;
|
|
1584
|
+
keyDiscovery;
|
|
1473
1585
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1474
1586
|
snapshotSerializer = new JsonSerializer();
|
|
1475
1587
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
@@ -1819,15 +1931,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1819
1931
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1820
1932
|
}
|
|
1821
1933
|
async invalidateByPattern(pattern) {
|
|
1934
|
+
this.validatePattern(pattern);
|
|
1822
1935
|
await this.awaitStartup("invalidateByPattern");
|
|
1823
|
-
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1936
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1824
1937
|
await this.deleteKeys(keys);
|
|
1825
1938
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1826
1939
|
}
|
|
1827
1940
|
async invalidateByPrefix(prefix) {
|
|
1828
1941
|
await this.awaitStartup("invalidateByPrefix");
|
|
1829
1942
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1830
|
-
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1943
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1831
1944
|
await this.deleteKeys(keys);
|
|
1832
1945
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1833
1946
|
}
|
|
@@ -2440,50 +2553,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2440
2553
|
shouldBroadcastL1Invalidation() {
|
|
2441
2554
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2442
2555
|
}
|
|
2443
|
-
async collectKeysWithPrefix(prefix) {
|
|
2444
|
-
const matches = new Set(
|
|
2445
|
-
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
2446
|
-
);
|
|
2447
|
-
await Promise.all(
|
|
2448
|
-
this.layers.map(async (layer) => {
|
|
2449
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2450
|
-
return;
|
|
2451
|
-
}
|
|
2452
|
-
try {
|
|
2453
|
-
const keys = await layer.keys();
|
|
2454
|
-
for (const key of keys) {
|
|
2455
|
-
if (key.startsWith(prefix)) {
|
|
2456
|
-
matches.add(key);
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
} catch (error) {
|
|
2460
|
-
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
2461
|
-
}
|
|
2462
|
-
})
|
|
2463
|
-
);
|
|
2464
|
-
return [...matches];
|
|
2465
|
-
}
|
|
2466
|
-
async collectKeysMatchingPattern(pattern) {
|
|
2467
|
-
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
2468
|
-
await Promise.all(
|
|
2469
|
-
this.layers.map(async (layer) => {
|
|
2470
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2471
|
-
return;
|
|
2472
|
-
}
|
|
2473
|
-
try {
|
|
2474
|
-
const keys = await layer.keys();
|
|
2475
|
-
for (const key of keys) {
|
|
2476
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
2477
|
-
matches.add(key);
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
} catch (error) {
|
|
2481
|
-
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
2482
|
-
}
|
|
2483
|
-
})
|
|
2484
|
-
);
|
|
2485
|
-
return [...matches];
|
|
2486
|
-
}
|
|
2487
2556
|
shouldCleanupGenerations() {
|
|
2488
2557
|
return Boolean(this.options.generationCleanup);
|
|
2489
2558
|
}
|
|
@@ -2506,7 +2575,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2506
2575
|
}
|
|
2507
2576
|
async cleanupGeneration(generation) {
|
|
2508
2577
|
const prefix = `v${generation}:`;
|
|
2509
|
-
const keys = await this.collectKeysWithPrefix(prefix);
|
|
2578
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2510
2579
|
if (keys.length === 0) {
|
|
2511
2580
|
return;
|
|
2512
2581
|
}
|
|
@@ -2756,6 +2825,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
2756
2825
|
}
|
|
2757
2826
|
return key;
|
|
2758
2827
|
}
|
|
2828
|
+
validatePattern(pattern) {
|
|
2829
|
+
if (pattern.length === 0) {
|
|
2830
|
+
throw new Error("Pattern must not be empty.");
|
|
2831
|
+
}
|
|
2832
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2833
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2834
|
+
}
|
|
2835
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2836
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2759
2839
|
validateTtlPolicy(name, policy) {
|
|
2760
2840
|
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2761
2841
|
return;
|
|
@@ -2920,7 +3000,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
2920
3000
|
}
|
|
2921
3001
|
};
|
|
2922
3002
|
function createInstanceId() {
|
|
2923
|
-
|
|
3003
|
+
if (globalThis.crypto?.randomUUID) {
|
|
3004
|
+
return globalThis.crypto.randomUUID();
|
|
3005
|
+
}
|
|
3006
|
+
const bytes = new Uint8Array(16);
|
|
3007
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
3008
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
3009
|
+
} else {
|
|
3010
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
3011
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
2924
3015
|
}
|
|
2925
3016
|
|
|
2926
3017
|
// src/module.ts
|