layercache 1.2.4 → 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 +6 -0
- 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-Dw97n89L.d.cts → edge-P07GCO2Y.d.cts} +1 -0
- package/dist/{edge-Dw97n89L.d.ts → edge-P07GCO2Y.d.ts} +1 -0
- 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 +122 -30
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +96 -24
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +96 -23
- package/packages/nestjs/dist/index.d.cts +1 -0
- package/packages/nestjs/dist/index.d.ts +1 -0
- package/packages/nestjs/dist/index.js +96 -23
package/README.md
CHANGED
|
@@ -203,6 +203,8 @@ Glob-style deletion against the tracked key set, plus any layer that can enumera
|
|
|
203
203
|
await cache.invalidateByPattern('user:*') // deletes user:1, user:2, …
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
+
Patterns must be non-empty, at most 1024 characters long, and free of control characters.
|
|
207
|
+
|
|
206
208
|
For multi-instance deployments, prefer a shared `RedisTagIndex`. Without it, pattern invalidation still scans real layer keys when available, but that fallback only helps on layers that implement `keys()`, and tag tracking itself remains process-local.
|
|
207
209
|
|
|
208
210
|
### `cache.invalidateByPrefix(prefix): Promise<void>`
|
|
@@ -320,6 +322,8 @@ await cache.warm(
|
|
|
320
322
|
|
|
321
323
|
Returns a scoped view with the same full API (`get`, `set`, `delete`, `clear`, `mget`, `wrap`, `warm`, `invalidateByTag`, `invalidateByPattern`, `getMetrics`). `clear()` only touches `prefix:*` keys, and namespace metrics are serialized per `CacheStack` instance so unrelated caches do not block each other while metrics are collected.
|
|
322
324
|
|
|
325
|
+
Namespace prefixes must be non-empty, at most 256 characters long, and free of control characters.
|
|
326
|
+
|
|
323
327
|
```ts
|
|
324
328
|
const users = cache.namespace('users')
|
|
325
329
|
const posts = cache.namespace('posts')
|
|
@@ -712,6 +716,8 @@ http.createServer(statsHandler).listen(9090)
|
|
|
712
716
|
// GET / → JSON stats
|
|
713
717
|
```
|
|
714
718
|
|
|
719
|
+
The built-in handler returns JSON with `Cache-Control: no-store` and `X-Content-Type-Options: nosniff` headers.
|
|
720
|
+
|
|
715
721
|
Or use the Fastify plugin:
|
|
716
722
|
|
|
717
723
|
```ts
|
|
@@ -185,6 +185,7 @@ var MemoryLayer = class {
|
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
// src/invalidation/TagIndex.ts
|
|
188
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
188
189
|
var TagIndex = class {
|
|
189
190
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
190
191
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -239,7 +240,7 @@ var TagIndex = class {
|
|
|
239
240
|
}
|
|
240
241
|
async matchPattern(pattern) {
|
|
241
242
|
const matches = /* @__PURE__ */ new Set();
|
|
242
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
243
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
243
244
|
return [...matches];
|
|
244
245
|
}
|
|
245
246
|
async clear() {
|
|
@@ -291,7 +292,10 @@ var TagIndex = class {
|
|
|
291
292
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
|
-
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
295
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
296
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
295
299
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
296
300
|
if (visited.has(stateKey)) {
|
|
297
301
|
return;
|
|
@@ -308,21 +312,37 @@ var TagIndex = class {
|
|
|
308
312
|
return;
|
|
309
313
|
}
|
|
310
314
|
if (patternChar === "*") {
|
|
311
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
315
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
312
316
|
for (const [character, child2] of node.children) {
|
|
313
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
317
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
314
318
|
}
|
|
315
319
|
return;
|
|
316
320
|
}
|
|
317
321
|
if (patternChar === "?") {
|
|
318
322
|
for (const [character, child2] of node.children) {
|
|
319
|
-
this.collectPatternMatches(
|
|
323
|
+
this.collectPatternMatches(
|
|
324
|
+
child2,
|
|
325
|
+
`${prefix}${character}`,
|
|
326
|
+
pattern,
|
|
327
|
+
patternIndex + 1,
|
|
328
|
+
matches,
|
|
329
|
+
visited,
|
|
330
|
+
depth + 1
|
|
331
|
+
);
|
|
320
332
|
}
|
|
321
333
|
return;
|
|
322
334
|
}
|
|
323
335
|
const child = node.children.get(patternChar);
|
|
324
336
|
if (child) {
|
|
325
|
-
this.collectPatternMatches(
|
|
337
|
+
this.collectPatternMatches(
|
|
338
|
+
child,
|
|
339
|
+
`${prefix}${patternChar}`,
|
|
340
|
+
pattern,
|
|
341
|
+
patternIndex + 1,
|
|
342
|
+
matches,
|
|
343
|
+
visited,
|
|
344
|
+
depth + 1
|
|
345
|
+
);
|
|
326
346
|
}
|
|
327
347
|
}
|
|
328
348
|
pruneKnownKeysIfNeeded() {
|
|
@@ -423,7 +443,7 @@ function normalizeUrl(url) {
|
|
|
423
443
|
try {
|
|
424
444
|
const parsed = new URL(url, "http://localhost");
|
|
425
445
|
parsed.searchParams.sort();
|
|
426
|
-
return
|
|
446
|
+
return parsed.pathname + parsed.search;
|
|
427
447
|
} catch {
|
|
428
448
|
return url;
|
|
429
449
|
}
|
package/dist/cli.cjs
CHANGED
|
@@ -281,10 +281,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
281
281
|
}
|
|
282
282
|
const redisUrl = validateRedisUrl(args.redisUrl);
|
|
283
283
|
if (!redisUrl) {
|
|
284
|
-
process.stderr.write(
|
|
285
|
-
`Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
|
|
286
|
-
`
|
|
287
|
-
);
|
|
284
|
+
process.stderr.write("Error: invalid Redis URL. Expected format: redis://[user:password@]host[:port][/db]\n");
|
|
288
285
|
process.exitCode = 1;
|
|
289
286
|
return;
|
|
290
287
|
}
|
|
@@ -296,7 +293,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
296
293
|
try {
|
|
297
294
|
await redis.connect().catch((error) => {
|
|
298
295
|
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
-
throw new Error(`Failed to connect to Redis
|
|
296
|
+
throw new Error(`Failed to connect to Redis: ${message}`);
|
|
300
297
|
});
|
|
301
298
|
if (args.command === "stats") {
|
|
302
299
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
@@ -451,17 +448,6 @@ function summarizeInspectableValue(value) {
|
|
|
451
448
|
}
|
|
452
449
|
return value;
|
|
453
450
|
}
|
|
454
|
-
function maskRedisUrl(url) {
|
|
455
|
-
try {
|
|
456
|
-
const parsed = new URL(url);
|
|
457
|
-
if (parsed.password) {
|
|
458
|
-
parsed.password = "***";
|
|
459
|
-
}
|
|
460
|
-
return parsed.toString();
|
|
461
|
-
} catch {
|
|
462
|
-
return url.replace(/:([^@/]+)@/, ":***@");
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
451
|
if (process.argv[1]?.includes("cli.")) {
|
|
466
452
|
void main();
|
|
467
453
|
}
|
package/dist/cli.js
CHANGED
|
@@ -19,10 +19,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
19
19
|
}
|
|
20
20
|
const redisUrl = validateRedisUrl(args.redisUrl);
|
|
21
21
|
if (!redisUrl) {
|
|
22
|
-
process.stderr.write(
|
|
23
|
-
`Error: invalid Redis URL "${maskRedisUrl(args.redisUrl)}". Expected format: redis://[user:password@]host[:port][/db]
|
|
24
|
-
`
|
|
25
|
-
);
|
|
22
|
+
process.stderr.write("Error: invalid Redis URL. Expected format: redis://[user:password@]host[:port][/db]\n");
|
|
26
23
|
process.exitCode = 1;
|
|
27
24
|
return;
|
|
28
25
|
}
|
|
@@ -34,7 +31,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
34
31
|
try {
|
|
35
32
|
await redis.connect().catch((error) => {
|
|
36
33
|
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
-
throw new Error(`Failed to connect to Redis
|
|
34
|
+
throw new Error(`Failed to connect to Redis: ${message}`);
|
|
38
35
|
});
|
|
39
36
|
if (args.command === "stats") {
|
|
40
37
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
@@ -189,17 +186,6 @@ function summarizeInspectableValue(value) {
|
|
|
189
186
|
}
|
|
190
187
|
return value;
|
|
191
188
|
}
|
|
192
|
-
function maskRedisUrl(url) {
|
|
193
|
-
try {
|
|
194
|
-
const parsed = new URL(url);
|
|
195
|
-
if (parsed.password) {
|
|
196
|
-
parsed.password = "***";
|
|
197
|
-
}
|
|
198
|
-
return parsed.toString();
|
|
199
|
-
} catch {
|
|
200
|
-
return url.replace(/:([^@/]+)@/, ":***@");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
189
|
if (process.argv[1]?.includes("cli.")) {
|
|
204
190
|
void main();
|
|
205
191
|
}
|
|
@@ -663,6 +663,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
663
663
|
private validateRateLimitOptions;
|
|
664
664
|
private validateNonNegativeNumber;
|
|
665
665
|
private validateCacheKey;
|
|
666
|
+
private validatePattern;
|
|
666
667
|
private validateTtlPolicy;
|
|
667
668
|
private assertActive;
|
|
668
669
|
private awaitStartup;
|
|
@@ -663,6 +663,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
663
663
|
private validateRateLimitOptions;
|
|
664
664
|
private validateNonNegativeNumber;
|
|
665
665
|
private validateCacheKey;
|
|
666
|
+
private validatePattern;
|
|
666
667
|
private validateTtlPolicy;
|
|
667
668
|
private assertActive;
|
|
668
669
|
private awaitStartup;
|
package/dist/edge.cjs
CHANGED
|
@@ -295,6 +295,7 @@ var PatternMatcher = class _PatternMatcher {
|
|
|
295
295
|
};
|
|
296
296
|
|
|
297
297
|
// src/invalidation/TagIndex.ts
|
|
298
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
298
299
|
var TagIndex = class {
|
|
299
300
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
300
301
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -349,7 +350,7 @@ var TagIndex = class {
|
|
|
349
350
|
}
|
|
350
351
|
async matchPattern(pattern) {
|
|
351
352
|
const matches = /* @__PURE__ */ new Set();
|
|
352
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
353
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
353
354
|
return [...matches];
|
|
354
355
|
}
|
|
355
356
|
async clear() {
|
|
@@ -401,7 +402,10 @@ var TagIndex = class {
|
|
|
401
402
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
402
403
|
}
|
|
403
404
|
}
|
|
404
|
-
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
|
|
405
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
406
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
405
409
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
406
410
|
if (visited.has(stateKey)) {
|
|
407
411
|
return;
|
|
@@ -418,21 +422,37 @@ var TagIndex = class {
|
|
|
418
422
|
return;
|
|
419
423
|
}
|
|
420
424
|
if (patternChar === "*") {
|
|
421
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
425
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
422
426
|
for (const [character, child2] of node.children) {
|
|
423
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
427
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
424
428
|
}
|
|
425
429
|
return;
|
|
426
430
|
}
|
|
427
431
|
if (patternChar === "?") {
|
|
428
432
|
for (const [character, child2] of node.children) {
|
|
429
|
-
this.collectPatternMatches(
|
|
433
|
+
this.collectPatternMatches(
|
|
434
|
+
child2,
|
|
435
|
+
`${prefix}${character}`,
|
|
436
|
+
pattern,
|
|
437
|
+
patternIndex + 1,
|
|
438
|
+
matches,
|
|
439
|
+
visited,
|
|
440
|
+
depth + 1
|
|
441
|
+
);
|
|
430
442
|
}
|
|
431
443
|
return;
|
|
432
444
|
}
|
|
433
445
|
const child = node.children.get(patternChar);
|
|
434
446
|
if (child) {
|
|
435
|
-
this.collectPatternMatches(
|
|
447
|
+
this.collectPatternMatches(
|
|
448
|
+
child,
|
|
449
|
+
`${prefix}${patternChar}`,
|
|
450
|
+
pattern,
|
|
451
|
+
patternIndex + 1,
|
|
452
|
+
matches,
|
|
453
|
+
visited,
|
|
454
|
+
depth + 1
|
|
455
|
+
);
|
|
436
456
|
}
|
|
437
457
|
}
|
|
438
458
|
pruneKnownKeysIfNeeded() {
|
|
@@ -533,7 +553,7 @@ function normalizeUrl(url) {
|
|
|
533
553
|
try {
|
|
534
554
|
const parsed = new URL(url, "http://localhost");
|
|
535
555
|
parsed.searchParams.sort();
|
|
536
|
-
return
|
|
556
|
+
return parsed.pathname + parsed.search;
|
|
537
557
|
} catch {
|
|
538
558
|
return url;
|
|
539
559
|
}
|
package/dist/edge.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.cjs';
|
|
2
2
|
import 'node:events';
|
package/dist/edge.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-
|
|
1
|
+
export { e as CacheGetOptions, f as CacheLayer, h as CacheLayerSetManyEntry, t as CacheMetricsSnapshot, w as CacheRateLimitOptions, B as CacheTtlPolicy, D as CacheTtlPolicyContext, J as CacheWriteOptions, K as EvictionPolicy, M as MemoryLayer, N as MemoryLayerOptions, O as MemoryLayerSnapshotEntry, P as PatternMatcher, T as TagIndex, Q as createHonoCacheMiddleware } from './edge-P07GCO2Y.js';
|
|
2
2
|
import 'node:events';
|
package/dist/edge.js
CHANGED
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,17 @@ 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
|
+
}
|
|
308
320
|
|
|
309
321
|
// src/invalidation/PatternMatcher.ts
|
|
310
322
|
var PatternMatcher = class _PatternMatcher {
|
|
@@ -425,9 +437,7 @@ var CircuitBreakerManager = class {
|
|
|
425
437
|
}
|
|
426
438
|
const now = Date.now();
|
|
427
439
|
if (state.openUntil <= now) {
|
|
428
|
-
|
|
429
|
-
state.failures = 0;
|
|
430
|
-
this.breakers.set(key, state);
|
|
440
|
+
this.breakers.delete(key);
|
|
431
441
|
return;
|
|
432
442
|
}
|
|
433
443
|
const remainingMs = state.openUntil - now;
|
|
@@ -438,15 +448,15 @@ var CircuitBreakerManager = class {
|
|
|
438
448
|
if (!options) {
|
|
439
449
|
return;
|
|
440
450
|
}
|
|
451
|
+
this.pruneIfNeeded();
|
|
441
452
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
442
453
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
443
|
-
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
454
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
444
455
|
state.failures += 1;
|
|
445
456
|
if (state.failures >= failureThreshold) {
|
|
446
457
|
state.openUntil = Date.now() + cooldownMs;
|
|
447
458
|
}
|
|
448
459
|
this.breakers.set(key, state);
|
|
449
|
-
this.pruneIfNeeded();
|
|
450
460
|
}
|
|
451
461
|
recordSuccess(key) {
|
|
452
462
|
this.breakers.delete(key);
|
|
@@ -457,8 +467,7 @@ var CircuitBreakerManager = class {
|
|
|
457
467
|
return false;
|
|
458
468
|
}
|
|
459
469
|
if (state.openUntil <= Date.now()) {
|
|
460
|
-
|
|
461
|
-
state.failures = 0;
|
|
470
|
+
this.breakers.delete(key);
|
|
462
471
|
return false;
|
|
463
472
|
}
|
|
464
473
|
return true;
|
|
@@ -482,15 +491,20 @@ var CircuitBreakerManager = class {
|
|
|
482
491
|
if (this.breakers.size <= this.maxEntries) {
|
|
483
492
|
return;
|
|
484
493
|
}
|
|
494
|
+
const now = Date.now();
|
|
485
495
|
for (const [key, state] of this.breakers.entries()) {
|
|
486
496
|
if (this.breakers.size <= this.maxEntries) {
|
|
487
|
-
|
|
497
|
+
return;
|
|
488
498
|
}
|
|
489
|
-
if (!state.openUntil || state.openUntil <=
|
|
499
|
+
if (!state.openUntil || state.openUntil <= now) {
|
|
490
500
|
this.breakers.delete(key);
|
|
491
501
|
}
|
|
492
502
|
}
|
|
493
|
-
|
|
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) {
|
|
494
508
|
if (this.breakers.size <= this.maxEntries) {
|
|
495
509
|
break;
|
|
496
510
|
}
|
|
@@ -500,6 +514,7 @@ var CircuitBreakerManager = class {
|
|
|
500
514
|
};
|
|
501
515
|
|
|
502
516
|
// src/internal/FetchRateLimiter.ts
|
|
517
|
+
var MAX_BUCKETS = 1e4;
|
|
503
518
|
var FetchRateLimiter = class {
|
|
504
519
|
buckets = /* @__PURE__ */ new Map();
|
|
505
520
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -665,10 +680,25 @@ var FetchRateLimiter = class {
|
|
|
665
680
|
if (existing) {
|
|
666
681
|
return existing;
|
|
667
682
|
}
|
|
683
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
684
|
+
this.evictIdleBuckets();
|
|
685
|
+
}
|
|
668
686
|
const bucket = { active: 0, startedAt: [] };
|
|
669
687
|
this.buckets.set(bucketKey, bucket);
|
|
670
688
|
return bucket;
|
|
671
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
|
+
}
|
|
672
702
|
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
673
703
|
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
674
704
|
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
@@ -995,18 +1025,18 @@ var TtlResolver = class {
|
|
|
995
1025
|
return;
|
|
996
1026
|
}
|
|
997
1027
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
998
|
-
|
|
999
|
-
for (
|
|
1000
|
-
|
|
1001
|
-
|
|
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]);
|
|
1002
1033
|
}
|
|
1003
|
-
this.accessProfiles.delete(key);
|
|
1004
|
-
removed += 1;
|
|
1005
1034
|
}
|
|
1006
1035
|
}
|
|
1007
1036
|
};
|
|
1008
1037
|
|
|
1009
1038
|
// src/invalidation/TagIndex.ts
|
|
1039
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
1010
1040
|
var TagIndex = class {
|
|
1011
1041
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
1012
1042
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -1061,7 +1091,7 @@ var TagIndex = class {
|
|
|
1061
1091
|
}
|
|
1062
1092
|
async matchPattern(pattern) {
|
|
1063
1093
|
const matches = /* @__PURE__ */ new Set();
|
|
1064
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1094
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1065
1095
|
return [...matches];
|
|
1066
1096
|
}
|
|
1067
1097
|
async clear() {
|
|
@@ -1113,7 +1143,10 @@ var TagIndex = class {
|
|
|
1113
1143
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1114
1144
|
}
|
|
1115
1145
|
}
|
|
1116
|
-
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
|
+
}
|
|
1117
1150
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
1118
1151
|
if (visited.has(stateKey)) {
|
|
1119
1152
|
return;
|
|
@@ -1130,21 +1163,37 @@ var TagIndex = class {
|
|
|
1130
1163
|
return;
|
|
1131
1164
|
}
|
|
1132
1165
|
if (patternChar === "*") {
|
|
1133
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1166
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
1134
1167
|
for (const [character, child2] of node.children) {
|
|
1135
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1168
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
1136
1169
|
}
|
|
1137
1170
|
return;
|
|
1138
1171
|
}
|
|
1139
1172
|
if (patternChar === "?") {
|
|
1140
1173
|
for (const [character, child2] of node.children) {
|
|
1141
|
-
this.collectPatternMatches(
|
|
1174
|
+
this.collectPatternMatches(
|
|
1175
|
+
child2,
|
|
1176
|
+
`${prefix}${character}`,
|
|
1177
|
+
pattern,
|
|
1178
|
+
patternIndex + 1,
|
|
1179
|
+
matches,
|
|
1180
|
+
visited,
|
|
1181
|
+
depth + 1
|
|
1182
|
+
);
|
|
1142
1183
|
}
|
|
1143
1184
|
return;
|
|
1144
1185
|
}
|
|
1145
1186
|
const child = node.children.get(patternChar);
|
|
1146
1187
|
if (child) {
|
|
1147
|
-
this.collectPatternMatches(
|
|
1188
|
+
this.collectPatternMatches(
|
|
1189
|
+
child,
|
|
1190
|
+
`${prefix}${patternChar}`,
|
|
1191
|
+
pattern,
|
|
1192
|
+
patternIndex + 1,
|
|
1193
|
+
matches,
|
|
1194
|
+
visited,
|
|
1195
|
+
depth + 1
|
|
1196
|
+
);
|
|
1148
1197
|
}
|
|
1149
1198
|
}
|
|
1150
1199
|
pruneKnownKeysIfNeeded() {
|
|
@@ -1287,6 +1336,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
|
1287
1336
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1288
1337
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1289
1338
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1339
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
1290
1340
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1291
1341
|
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1292
1342
|
var DebugLogger = class {
|
|
@@ -1720,6 +1770,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1720
1770
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1721
1771
|
}
|
|
1722
1772
|
async invalidateByPattern(pattern) {
|
|
1773
|
+
this.validatePattern(pattern);
|
|
1723
1774
|
await this.awaitStartup("invalidateByPattern");
|
|
1724
1775
|
const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
|
|
1725
1776
|
await this.deleteKeys(keys);
|
|
@@ -2613,6 +2664,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2613
2664
|
}
|
|
2614
2665
|
return key;
|
|
2615
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
|
+
}
|
|
2616
2678
|
validateTtlPolicy(name, policy) {
|
|
2617
2679
|
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2618
2680
|
return;
|
|
@@ -2777,7 +2839,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2777
2839
|
}
|
|
2778
2840
|
};
|
|
2779
2841
|
function createInstanceId() {
|
|
2780
|
-
|
|
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("")}`;
|
|
2781
2854
|
}
|
|
2782
2855
|
|
|
2783
2856
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -2819,7 +2892,7 @@ var RedisInvalidationBus = class {
|
|
|
2819
2892
|
async dispatchToHandlers(payload) {
|
|
2820
2893
|
let message;
|
|
2821
2894
|
try {
|
|
2822
|
-
const parsed = JSON.parse(payload);
|
|
2895
|
+
const parsed = sanitizeJsonValue2(JSON.parse(payload));
|
|
2823
2896
|
if (!this.isInvalidationMessage(parsed)) {
|
|
2824
2897
|
throw new Error("Invalid invalidation payload shape.");
|
|
2825
2898
|
}
|
|
@@ -2856,6 +2929,22 @@ var RedisInvalidationBus = class {
|
|
|
2856
2929
|
console.error(`[layercache] ${message}`, error);
|
|
2857
2930
|
}
|
|
2858
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
|
+
}
|
|
2859
2948
|
|
|
2860
2949
|
// src/invalidation/RedisTagIndex.ts
|
|
2861
2950
|
var RedisTagIndex = class {
|
|
@@ -2996,6 +3085,8 @@ function createCacheStatsHandler(cache) {
|
|
|
2996
3085
|
return async (_request, response) => {
|
|
2997
3086
|
response.statusCode = 200;
|
|
2998
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");
|
|
2999
3090
|
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
3000
3091
|
};
|
|
3001
3092
|
}
|
|
@@ -3081,7 +3172,7 @@ function normalizeUrl(url) {
|
|
|
3081
3172
|
try {
|
|
3082
3173
|
const parsed = new URL(url, "http://localhost");
|
|
3083
3174
|
parsed.searchParams.sort();
|
|
3084
|
-
return
|
|
3175
|
+
return parsed.pathname + parsed.search;
|
|
3085
3176
|
} catch {
|
|
3086
3177
|
return url;
|
|
3087
3178
|
}
|
|
@@ -3132,7 +3223,7 @@ function normalizeUrl2(url) {
|
|
|
3132
3223
|
try {
|
|
3133
3224
|
const parsed = new URL(url, "http://localhost");
|
|
3134
3225
|
parsed.searchParams.sort();
|
|
3135
|
-
return
|
|
3226
|
+
return parsed.pathname + parsed.search;
|
|
3136
3227
|
} catch {
|
|
3137
3228
|
return url;
|
|
3138
3229
|
}
|
|
@@ -3595,8 +3686,9 @@ var RedisLayer = class {
|
|
|
3595
3686
|
}
|
|
3596
3687
|
}
|
|
3597
3688
|
try {
|
|
3598
|
-
await this.client.del(this.withPrefix(key))
|
|
3599
|
-
} catch {
|
|
3689
|
+
await this.client.del(this.withPrefix(key));
|
|
3690
|
+
} catch (deleteError) {
|
|
3691
|
+
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
3600
3692
|
}
|
|
3601
3693
|
return null;
|
|
3602
3694
|
}
|
|
@@ -3996,7 +4088,7 @@ var MemcachedLayer = class {
|
|
|
3996
4088
|
|
|
3997
4089
|
// src/serialization/MsgpackSerializer.ts
|
|
3998
4090
|
var import_msgpack = require("@msgpack/msgpack");
|
|
3999
|
-
var
|
|
4091
|
+
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4000
4092
|
var MsgpackSerializer = class {
|
|
4001
4093
|
serialize(value) {
|
|
4002
4094
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
@@ -4015,7 +4107,7 @@ function sanitizeMsgpackValue(value) {
|
|
|
4015
4107
|
}
|
|
4016
4108
|
const sanitized = {};
|
|
4017
4109
|
for (const [key, entry] of Object.entries(value)) {
|
|
4018
|
-
if (
|
|
4110
|
+
if (DANGEROUS_KEYS2.has(key)) {
|
|
4019
4111
|
continue;
|
|
4020
4112
|
}
|
|
4021
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
|
|