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
package/dist/index.cjs
CHANGED
|
@@ -176,6 +176,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
176
176
|
* ```
|
|
177
177
|
*/
|
|
178
178
|
namespace(childPrefix) {
|
|
179
|
+
validateNamespaceKey(childPrefix);
|
|
179
180
|
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
180
181
|
}
|
|
181
182
|
qualify(key) {
|
|
@@ -305,6 +306,118 @@ function addMap(base, delta) {
|
|
|
305
306
|
}
|
|
306
307
|
return result;
|
|
307
308
|
}
|
|
309
|
+
function validateNamespaceKey(key) {
|
|
310
|
+
if (key.length === 0) {
|
|
311
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
312
|
+
}
|
|
313
|
+
if (key.length > 256) {
|
|
314
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
315
|
+
}
|
|
316
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
317
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/invalidation/PatternMatcher.ts
|
|
322
|
+
var PatternMatcher = class _PatternMatcher {
|
|
323
|
+
/**
|
|
324
|
+
* Tests whether a glob-style pattern matches a value.
|
|
325
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
326
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
327
|
+
* quadratic memory usage on long patterns/keys.
|
|
328
|
+
*/
|
|
329
|
+
static matches(pattern, value) {
|
|
330
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
334
|
+
*/
|
|
335
|
+
static matchLinear(pattern, value) {
|
|
336
|
+
let patternIndex = 0;
|
|
337
|
+
let valueIndex = 0;
|
|
338
|
+
let starIndex = -1;
|
|
339
|
+
let backtrackValueIndex = 0;
|
|
340
|
+
while (valueIndex < value.length) {
|
|
341
|
+
const patternChar = pattern[patternIndex];
|
|
342
|
+
const valueChar = value[valueIndex];
|
|
343
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
344
|
+
starIndex = patternIndex;
|
|
345
|
+
patternIndex += 1;
|
|
346
|
+
backtrackValueIndex = valueIndex;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
350
|
+
patternIndex += 1;
|
|
351
|
+
valueIndex += 1;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (starIndex !== -1) {
|
|
355
|
+
patternIndex = starIndex + 1;
|
|
356
|
+
backtrackValueIndex += 1;
|
|
357
|
+
valueIndex = backtrackValueIndex;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
363
|
+
patternIndex += 1;
|
|
364
|
+
}
|
|
365
|
+
return patternIndex === pattern.length;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/internal/CacheKeyDiscovery.ts
|
|
370
|
+
var CacheKeyDiscovery = class {
|
|
371
|
+
constructor(options) {
|
|
372
|
+
this.options = options;
|
|
373
|
+
}
|
|
374
|
+
options;
|
|
375
|
+
async collectKeysWithPrefix(prefix) {
|
|
376
|
+
const { tagIndex } = this.options;
|
|
377
|
+
const matches = new Set(
|
|
378
|
+
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
379
|
+
);
|
|
380
|
+
await Promise.all(
|
|
381
|
+
this.options.layers.map(async (layer) => {
|
|
382
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const keys = await layer.keys();
|
|
387
|
+
for (const key of keys) {
|
|
388
|
+
if (key.startsWith(prefix)) {
|
|
389
|
+
matches.add(key);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
return [...matches];
|
|
398
|
+
}
|
|
399
|
+
async collectKeysMatchingPattern(pattern) {
|
|
400
|
+
const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
|
|
401
|
+
await Promise.all(
|
|
402
|
+
this.options.layers.map(async (layer) => {
|
|
403
|
+
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const keys = await layer.keys();
|
|
408
|
+
for (const key of keys) {
|
|
409
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
410
|
+
matches.add(key);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
);
|
|
418
|
+
return [...matches];
|
|
419
|
+
}
|
|
420
|
+
};
|
|
308
421
|
|
|
309
422
|
// src/internal/CircuitBreakerManager.ts
|
|
310
423
|
var CircuitBreakerManager = class {
|
|
@@ -324,9 +437,7 @@ var CircuitBreakerManager = class {
|
|
|
324
437
|
}
|
|
325
438
|
const now = Date.now();
|
|
326
439
|
if (state.openUntil <= now) {
|
|
327
|
-
|
|
328
|
-
state.failures = 0;
|
|
329
|
-
this.breakers.set(key, state);
|
|
440
|
+
this.breakers.delete(key);
|
|
330
441
|
return;
|
|
331
442
|
}
|
|
332
443
|
const remainingMs = state.openUntil - now;
|
|
@@ -337,15 +448,15 @@ var CircuitBreakerManager = class {
|
|
|
337
448
|
if (!options) {
|
|
338
449
|
return;
|
|
339
450
|
}
|
|
451
|
+
this.pruneIfNeeded();
|
|
340
452
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
341
453
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
342
|
-
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
454
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
343
455
|
state.failures += 1;
|
|
344
456
|
if (state.failures >= failureThreshold) {
|
|
345
457
|
state.openUntil = Date.now() + cooldownMs;
|
|
346
458
|
}
|
|
347
459
|
this.breakers.set(key, state);
|
|
348
|
-
this.pruneIfNeeded();
|
|
349
460
|
}
|
|
350
461
|
recordSuccess(key) {
|
|
351
462
|
this.breakers.delete(key);
|
|
@@ -356,8 +467,7 @@ var CircuitBreakerManager = class {
|
|
|
356
467
|
return false;
|
|
357
468
|
}
|
|
358
469
|
if (state.openUntil <= Date.now()) {
|
|
359
|
-
|
|
360
|
-
state.failures = 0;
|
|
470
|
+
this.breakers.delete(key);
|
|
361
471
|
return false;
|
|
362
472
|
}
|
|
363
473
|
return true;
|
|
@@ -381,15 +491,20 @@ var CircuitBreakerManager = class {
|
|
|
381
491
|
if (this.breakers.size <= this.maxEntries) {
|
|
382
492
|
return;
|
|
383
493
|
}
|
|
494
|
+
const now = Date.now();
|
|
384
495
|
for (const [key, state] of this.breakers.entries()) {
|
|
385
496
|
if (this.breakers.size <= this.maxEntries) {
|
|
386
|
-
|
|
497
|
+
return;
|
|
387
498
|
}
|
|
388
|
-
if (!state.openUntil || state.openUntil <=
|
|
499
|
+
if (!state.openUntil || state.openUntil <= now) {
|
|
389
500
|
this.breakers.delete(key);
|
|
390
501
|
}
|
|
391
502
|
}
|
|
392
|
-
|
|
503
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
507
|
+
for (const [key] of sorted) {
|
|
393
508
|
if (this.breakers.size <= this.maxEntries) {
|
|
394
509
|
break;
|
|
395
510
|
}
|
|
@@ -399,6 +514,7 @@ var CircuitBreakerManager = class {
|
|
|
399
514
|
};
|
|
400
515
|
|
|
401
516
|
// src/internal/FetchRateLimiter.ts
|
|
517
|
+
var MAX_BUCKETS = 1e4;
|
|
402
518
|
var FetchRateLimiter = class {
|
|
403
519
|
buckets = /* @__PURE__ */ new Map();
|
|
404
520
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -564,10 +680,25 @@ var FetchRateLimiter = class {
|
|
|
564
680
|
if (existing) {
|
|
565
681
|
return existing;
|
|
566
682
|
}
|
|
683
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
684
|
+
this.evictIdleBuckets();
|
|
685
|
+
}
|
|
567
686
|
const bucket = { active: 0, startedAt: [] };
|
|
568
687
|
this.buckets.set(bucketKey, bucket);
|
|
569
688
|
return bucket;
|
|
570
689
|
}
|
|
690
|
+
evictIdleBuckets() {
|
|
691
|
+
for (const [key, bucket] of this.buckets.entries()) {
|
|
692
|
+
if (this.buckets.size <= MAX_BUCKETS * 0.9) {
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
|
|
696
|
+
this.buckets.delete(key);
|
|
697
|
+
this.queuesByBucket.delete(key);
|
|
698
|
+
this.pendingBuckets.delete(key);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
571
702
|
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
572
703
|
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
573
704
|
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
@@ -894,66 +1025,18 @@ var TtlResolver = class {
|
|
|
894
1025
|
return;
|
|
895
1026
|
}
|
|
896
1027
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
897
|
-
|
|
898
|
-
for (
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
this.accessProfiles.delete(key);
|
|
903
|
-
removed += 1;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
// src/invalidation/PatternMatcher.ts
|
|
909
|
-
var PatternMatcher = class _PatternMatcher {
|
|
910
|
-
/**
|
|
911
|
-
* Tests whether a glob-style pattern matches a value.
|
|
912
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
913
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
914
|
-
* quadratic memory usage on long patterns/keys.
|
|
915
|
-
*/
|
|
916
|
-
static matches(pattern, value) {
|
|
917
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Linear-time glob matching with O(1) extra memory.
|
|
921
|
-
*/
|
|
922
|
-
static matchLinear(pattern, value) {
|
|
923
|
-
let patternIndex = 0;
|
|
924
|
-
let valueIndex = 0;
|
|
925
|
-
let starIndex = -1;
|
|
926
|
-
let backtrackValueIndex = 0;
|
|
927
|
-
while (valueIndex < value.length) {
|
|
928
|
-
const patternChar = pattern[patternIndex];
|
|
929
|
-
const valueChar = value[valueIndex];
|
|
930
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
931
|
-
starIndex = patternIndex;
|
|
932
|
-
patternIndex += 1;
|
|
933
|
-
backtrackValueIndex = valueIndex;
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
937
|
-
patternIndex += 1;
|
|
938
|
-
valueIndex += 1;
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
if (starIndex !== -1) {
|
|
942
|
-
patternIndex = starIndex + 1;
|
|
943
|
-
backtrackValueIndex += 1;
|
|
944
|
-
valueIndex = backtrackValueIndex;
|
|
945
|
-
continue;
|
|
1028
|
+
const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
1029
|
+
for (let i = 0; i < toRemove && i < sorted.length; i++) {
|
|
1030
|
+
const entry = sorted[i];
|
|
1031
|
+
if (entry) {
|
|
1032
|
+
this.accessProfiles.delete(entry[0]);
|
|
946
1033
|
}
|
|
947
|
-
return false;
|
|
948
|
-
}
|
|
949
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
950
|
-
patternIndex += 1;
|
|
951
1034
|
}
|
|
952
|
-
return patternIndex === pattern.length;
|
|
953
1035
|
}
|
|
954
1036
|
};
|
|
955
1037
|
|
|
956
1038
|
// src/invalidation/TagIndex.ts
|
|
1039
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
957
1040
|
var TagIndex = class {
|
|
958
1041
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
959
1042
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -1008,7 +1091,7 @@ var TagIndex = class {
|
|
|
1008
1091
|
}
|
|
1009
1092
|
async matchPattern(pattern) {
|
|
1010
1093
|
const matches = /* @__PURE__ */ new Set();
|
|
1011
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1094
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1012
1095
|
return [...matches];
|
|
1013
1096
|
}
|
|
1014
1097
|
async clear() {
|
|
@@ -1060,7 +1143,10 @@ var TagIndex = class {
|
|
|
1060
1143
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1061
1144
|
}
|
|
1062
1145
|
}
|
|
1063
|
-
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
1146
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1147
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1064
1150
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
1065
1151
|
if (visited.has(stateKey)) {
|
|
1066
1152
|
return;
|
|
@@ -1077,21 +1163,37 @@ var TagIndex = class {
|
|
|
1077
1163
|
return;
|
|
1078
1164
|
}
|
|
1079
1165
|
if (patternChar === "*") {
|
|
1080
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1166
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
1081
1167
|
for (const [character, child2] of node.children) {
|
|
1082
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1168
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
1083
1169
|
}
|
|
1084
1170
|
return;
|
|
1085
1171
|
}
|
|
1086
1172
|
if (patternChar === "?") {
|
|
1087
1173
|
for (const [character, child2] of node.children) {
|
|
1088
|
-
this.collectPatternMatches(
|
|
1174
|
+
this.collectPatternMatches(
|
|
1175
|
+
child2,
|
|
1176
|
+
`${prefix}${character}`,
|
|
1177
|
+
pattern,
|
|
1178
|
+
patternIndex + 1,
|
|
1179
|
+
matches,
|
|
1180
|
+
visited,
|
|
1181
|
+
depth + 1
|
|
1182
|
+
);
|
|
1089
1183
|
}
|
|
1090
1184
|
return;
|
|
1091
1185
|
}
|
|
1092
1186
|
const child = node.children.get(patternChar);
|
|
1093
1187
|
if (child) {
|
|
1094
|
-
this.collectPatternMatches(
|
|
1188
|
+
this.collectPatternMatches(
|
|
1189
|
+
child,
|
|
1190
|
+
`${prefix}${patternChar}`,
|
|
1191
|
+
pattern,
|
|
1192
|
+
patternIndex + 1,
|
|
1193
|
+
matches,
|
|
1194
|
+
visited,
|
|
1195
|
+
depth + 1
|
|
1196
|
+
);
|
|
1095
1197
|
}
|
|
1096
1198
|
}
|
|
1097
1199
|
pruneKnownKeysIfNeeded() {
|
|
@@ -1234,6 +1336,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
|
1234
1336
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1235
1337
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1236
1338
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1339
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
1237
1340
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1238
1341
|
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1239
1342
|
var DebugLogger = class {
|
|
@@ -1282,6 +1385,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1282
1385
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
1283
1386
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
1284
1387
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1388
|
+
this.keyDiscovery = new CacheKeyDiscovery({
|
|
1389
|
+
layers: this.layers,
|
|
1390
|
+
tagIndex: this.tagIndex,
|
|
1391
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1392
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1393
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1285
1396
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1286
1397
|
this.logger.warn?.(
|
|
1287
1398
|
"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."
|
|
@@ -1309,6 +1420,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1309
1420
|
unsubscribeInvalidation;
|
|
1310
1421
|
logger;
|
|
1311
1422
|
tagIndex;
|
|
1423
|
+
keyDiscovery;
|
|
1312
1424
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1313
1425
|
snapshotSerializer = new JsonSerializer();
|
|
1314
1426
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
@@ -1658,15 +1770,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1658
1770
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1659
1771
|
}
|
|
1660
1772
|
async invalidateByPattern(pattern) {
|
|
1773
|
+
this.validatePattern(pattern);
|
|
1661
1774
|
await this.awaitStartup("invalidateByPattern");
|
|
1662
|
-
const keys = await this.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1775
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1663
1776
|
await this.deleteKeys(keys);
|
|
1664
1777
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1665
1778
|
}
|
|
1666
1779
|
async invalidateByPrefix(prefix) {
|
|
1667
1780
|
await this.awaitStartup("invalidateByPrefix");
|
|
1668
1781
|
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1669
|
-
const keys = await this.collectKeysWithPrefix(qualifiedPrefix);
|
|
1782
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1670
1783
|
await this.deleteKeys(keys);
|
|
1671
1784
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1672
1785
|
}
|
|
@@ -2279,50 +2392,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2279
2392
|
shouldBroadcastL1Invalidation() {
|
|
2280
2393
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2281
2394
|
}
|
|
2282
|
-
async collectKeysWithPrefix(prefix) {
|
|
2283
|
-
const matches = new Set(
|
|
2284
|
-
this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(prefix) : await this.tagIndex.matchPattern(`${prefix}*`)
|
|
2285
|
-
);
|
|
2286
|
-
await Promise.all(
|
|
2287
|
-
this.layers.map(async (layer) => {
|
|
2288
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
try {
|
|
2292
|
-
const keys = await layer.keys();
|
|
2293
|
-
for (const key of keys) {
|
|
2294
|
-
if (key.startsWith(prefix)) {
|
|
2295
|
-
matches.add(key);
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
} catch (error) {
|
|
2299
|
-
await this.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
2300
|
-
}
|
|
2301
|
-
})
|
|
2302
|
-
);
|
|
2303
|
-
return [...matches];
|
|
2304
|
-
}
|
|
2305
|
-
async collectKeysMatchingPattern(pattern) {
|
|
2306
|
-
const matches = new Set(await this.tagIndex.matchPattern(pattern));
|
|
2307
|
-
await Promise.all(
|
|
2308
|
-
this.layers.map(async (layer) => {
|
|
2309
|
-
if (!layer.keys || this.shouldSkipLayer(layer)) {
|
|
2310
|
-
return;
|
|
2311
|
-
}
|
|
2312
|
-
try {
|
|
2313
|
-
const keys = await layer.keys();
|
|
2314
|
-
for (const key of keys) {
|
|
2315
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
2316
|
-
matches.add(key);
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
} catch (error) {
|
|
2320
|
-
await this.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
2321
|
-
}
|
|
2322
|
-
})
|
|
2323
|
-
);
|
|
2324
|
-
return [...matches];
|
|
2325
|
-
}
|
|
2326
2395
|
shouldCleanupGenerations() {
|
|
2327
2396
|
return Boolean(this.options.generationCleanup);
|
|
2328
2397
|
}
|
|
@@ -2345,7 +2414,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2345
2414
|
}
|
|
2346
2415
|
async cleanupGeneration(generation) {
|
|
2347
2416
|
const prefix = `v${generation}:`;
|
|
2348
|
-
const keys = await this.collectKeysWithPrefix(prefix);
|
|
2417
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2349
2418
|
if (keys.length === 0) {
|
|
2350
2419
|
return;
|
|
2351
2420
|
}
|
|
@@ -2595,6 +2664,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2595
2664
|
}
|
|
2596
2665
|
return key;
|
|
2597
2666
|
}
|
|
2667
|
+
validatePattern(pattern) {
|
|
2668
|
+
if (pattern.length === 0) {
|
|
2669
|
+
throw new Error("Pattern must not be empty.");
|
|
2670
|
+
}
|
|
2671
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2672
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2673
|
+
}
|
|
2674
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2675
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2598
2678
|
validateTtlPolicy(name, policy) {
|
|
2599
2679
|
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2600
2680
|
return;
|
|
@@ -2759,7 +2839,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2759
2839
|
}
|
|
2760
2840
|
};
|
|
2761
2841
|
function createInstanceId() {
|
|
2762
|
-
|
|
2842
|
+
if (globalThis.crypto?.randomUUID) {
|
|
2843
|
+
return globalThis.crypto.randomUUID();
|
|
2844
|
+
}
|
|
2845
|
+
const bytes = new Uint8Array(16);
|
|
2846
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
2847
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
2848
|
+
} else {
|
|
2849
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2850
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
2763
2854
|
}
|
|
2764
2855
|
|
|
2765
2856
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -2801,7 +2892,7 @@ var RedisInvalidationBus = class {
|
|
|
2801
2892
|
async dispatchToHandlers(payload) {
|
|
2802
2893
|
let message;
|
|
2803
2894
|
try {
|
|
2804
|
-
const parsed = JSON.parse(payload);
|
|
2895
|
+
const parsed = sanitizeJsonValue2(JSON.parse(payload));
|
|
2805
2896
|
if (!this.isInvalidationMessage(parsed)) {
|
|
2806
2897
|
throw new Error("Invalid invalidation payload shape.");
|
|
2807
2898
|
}
|
|
@@ -2838,6 +2929,22 @@ var RedisInvalidationBus = class {
|
|
|
2838
2929
|
console.error(`[layercache] ${message}`, error);
|
|
2839
2930
|
}
|
|
2840
2931
|
};
|
|
2932
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2933
|
+
function sanitizeJsonValue2(value) {
|
|
2934
|
+
if (Array.isArray(value)) {
|
|
2935
|
+
return value.map(sanitizeJsonValue2);
|
|
2936
|
+
}
|
|
2937
|
+
if (value && typeof value === "object") {
|
|
2938
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
2939
|
+
for (const key of Object.keys(value)) {
|
|
2940
|
+
if (!DANGEROUS_KEYS.has(key)) {
|
|
2941
|
+
result[key] = sanitizeJsonValue2(value[key]);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
return result;
|
|
2945
|
+
}
|
|
2946
|
+
return value;
|
|
2947
|
+
}
|
|
2841
2948
|
|
|
2842
2949
|
// src/invalidation/RedisTagIndex.ts
|
|
2843
2950
|
var RedisTagIndex = class {
|
|
@@ -2978,6 +3085,8 @@ function createCacheStatsHandler(cache) {
|
|
|
2978
3085
|
return async (_request, response) => {
|
|
2979
3086
|
response.statusCode = 200;
|
|
2980
3087
|
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
3088
|
+
response.setHeader?.("cache-control", "no-store");
|
|
3089
|
+
response.setHeader?.("x-content-type-options", "nosniff");
|
|
2981
3090
|
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
2982
3091
|
};
|
|
2983
3092
|
}
|
|
@@ -3063,7 +3172,7 @@ function normalizeUrl(url) {
|
|
|
3063
3172
|
try {
|
|
3064
3173
|
const parsed = new URL(url, "http://localhost");
|
|
3065
3174
|
parsed.searchParams.sort();
|
|
3066
|
-
return
|
|
3175
|
+
return parsed.pathname + parsed.search;
|
|
3067
3176
|
} catch {
|
|
3068
3177
|
return url;
|
|
3069
3178
|
}
|
|
@@ -3114,7 +3223,7 @@ function normalizeUrl2(url) {
|
|
|
3114
3223
|
try {
|
|
3115
3224
|
const parsed = new URL(url, "http://localhost");
|
|
3116
3225
|
parsed.searchParams.sort();
|
|
3117
|
-
return
|
|
3226
|
+
return parsed.pathname + parsed.search;
|
|
3118
3227
|
} catch {
|
|
3119
3228
|
return url;
|
|
3120
3229
|
}
|
|
@@ -3577,8 +3686,9 @@ var RedisLayer = class {
|
|
|
3577
3686
|
}
|
|
3578
3687
|
}
|
|
3579
3688
|
try {
|
|
3580
|
-
await this.client.del(this.withPrefix(key))
|
|
3581
|
-
} catch {
|
|
3689
|
+
await this.client.del(this.withPrefix(key));
|
|
3690
|
+
} catch (deleteError) {
|
|
3691
|
+
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
3582
3692
|
}
|
|
3583
3693
|
return null;
|
|
3584
3694
|
}
|
|
@@ -3978,7 +4088,7 @@ var MemcachedLayer = class {
|
|
|
3978
4088
|
|
|
3979
4089
|
// src/serialization/MsgpackSerializer.ts
|
|
3980
4090
|
var import_msgpack = require("@msgpack/msgpack");
|
|
3981
|
-
var
|
|
4091
|
+
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3982
4092
|
var MsgpackSerializer = class {
|
|
3983
4093
|
serialize(value) {
|
|
3984
4094
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
@@ -3997,7 +4107,7 @@ function sanitizeMsgpackValue(value) {
|
|
|
3997
4107
|
}
|
|
3998
4108
|
const sanitized = {};
|
|
3999
4109
|
for (const [key, entry] of Object.entries(value)) {
|
|
4000
|
-
if (
|
|
4110
|
+
if (DANGEROUS_KEYS2.has(key)) {
|
|
4001
4111
|
continue;
|
|
4002
4112
|
}
|
|
4003
4113
|
sanitized[key] = sanitizeMsgpackValue(entry);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-P07GCO2Y.cjs';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.cjs';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-
|
|
2
|
-
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
import { I as InvalidationBus, C as CacheLogger, a as InvalidationMessage, b as CacheTagIndex, c as CacheStack, d as CacheWrapOptions, e as CacheGetOptions, f as CacheLayer, g as CacheSerializer, h as CacheLayerSetManyEntry, i as CacheSingleFlightCoordinator, j as CacheSingleFlightExecutionOptions } from './edge-P07GCO2Y.js';
|
|
2
|
+
export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheDegradationOptions, n as CacheHealthCheckResult, o as CacheHitRateSnapshot, p as CacheInspectResult, q as CacheLayerLatency, r as CacheMGetEntry, s as CacheMSetEntry, t as CacheMetricsSnapshot, u as CacheMissError, v as CacheNamespace, w as CacheRateLimitOptions, x as CacheSnapshotEntry, y as CacheStackEvents, z as CacheStackOptions, A as CacheStatsSnapshot, B as CacheTtlPolicy, D as CacheTtlPolicyContext, E as CacheWarmEntry, F as CacheWarmOptions, G as CacheWarmProgress, H as CacheWriteBehindOptions, J as CacheWriteOptions, K as EvictionPolicy, L as LayerTtlMap, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.js';
|
|
3
3
|
import Redis from 'ioredis';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
|