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/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  MemoryLayer,
6
6
  TagIndex,
7
7
  createHonoCacheMiddleware
8
- } from "./chunk-KOYGHLVP.js";
8
+ } from "./chunk-JC26W3KK.js";
9
9
  import {
10
10
  PatternMatcher,
11
11
  createStoredValueEnvelope,
@@ -136,6 +136,7 @@ var CacheNamespace = class _CacheNamespace {
136
136
  * ```
137
137
  */
138
138
  namespace(childPrefix) {
139
+ validateNamespaceKey(childPrefix);
139
140
  return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
140
141
  }
141
142
  qualify(key) {
@@ -265,6 +266,17 @@ function addMap(base, delta) {
265
266
  }
266
267
  return result;
267
268
  }
269
+ function validateNamespaceKey(key) {
270
+ if (key.length === 0) {
271
+ throw new Error("Namespace prefix must not be empty.");
272
+ }
273
+ if (key.length > 256) {
274
+ throw new Error("Namespace prefix must be at most 256 characters.");
275
+ }
276
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
277
+ throw new Error("Namespace prefix contains unsupported control characters.");
278
+ }
279
+ }
268
280
 
269
281
  // src/internal/CacheKeyDiscovery.ts
270
282
  var CacheKeyDiscovery = class {
@@ -337,9 +349,7 @@ var CircuitBreakerManager = class {
337
349
  }
338
350
  const now = Date.now();
339
351
  if (state.openUntil <= now) {
340
- state.openUntil = null;
341
- state.failures = 0;
342
- this.breakers.set(key, state);
352
+ this.breakers.delete(key);
343
353
  return;
344
354
  }
345
355
  const remainingMs = state.openUntil - now;
@@ -350,15 +360,15 @@ var CircuitBreakerManager = class {
350
360
  if (!options) {
351
361
  return;
352
362
  }
363
+ this.pruneIfNeeded();
353
364
  const failureThreshold = options.failureThreshold ?? 3;
354
365
  const cooldownMs = options.cooldownMs ?? 3e4;
355
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
366
+ const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
356
367
  state.failures += 1;
357
368
  if (state.failures >= failureThreshold) {
358
369
  state.openUntil = Date.now() + cooldownMs;
359
370
  }
360
371
  this.breakers.set(key, state);
361
- this.pruneIfNeeded();
362
372
  }
363
373
  recordSuccess(key) {
364
374
  this.breakers.delete(key);
@@ -369,8 +379,7 @@ var CircuitBreakerManager = class {
369
379
  return false;
370
380
  }
371
381
  if (state.openUntil <= Date.now()) {
372
- state.openUntil = null;
373
- state.failures = 0;
382
+ this.breakers.delete(key);
374
383
  return false;
375
384
  }
376
385
  return true;
@@ -394,15 +403,20 @@ var CircuitBreakerManager = class {
394
403
  if (this.breakers.size <= this.maxEntries) {
395
404
  return;
396
405
  }
406
+ const now = Date.now();
397
407
  for (const [key, state] of this.breakers.entries()) {
398
408
  if (this.breakers.size <= this.maxEntries) {
399
- break;
409
+ return;
400
410
  }
401
- if (!state.openUntil || state.openUntil <= Date.now()) {
411
+ if (!state.openUntil || state.openUntil <= now) {
402
412
  this.breakers.delete(key);
403
413
  }
404
414
  }
405
- for (const key of this.breakers.keys()) {
415
+ if (this.breakers.size <= this.maxEntries) {
416
+ return;
417
+ }
418
+ const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
419
+ for (const [key] of sorted) {
406
420
  if (this.breakers.size <= this.maxEntries) {
407
421
  break;
408
422
  }
@@ -412,6 +426,7 @@ var CircuitBreakerManager = class {
412
426
  };
413
427
 
414
428
  // src/internal/FetchRateLimiter.ts
429
+ var MAX_BUCKETS = 1e4;
415
430
  var FetchRateLimiter = class {
416
431
  buckets = /* @__PURE__ */ new Map();
417
432
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -577,10 +592,25 @@ var FetchRateLimiter = class {
577
592
  if (existing) {
578
593
  return existing;
579
594
  }
595
+ if (this.buckets.size >= MAX_BUCKETS) {
596
+ this.evictIdleBuckets();
597
+ }
580
598
  const bucket = { active: 0, startedAt: [] };
581
599
  this.buckets.set(bucketKey, bucket);
582
600
  return bucket;
583
601
  }
602
+ evictIdleBuckets() {
603
+ for (const [key, bucket] of this.buckets.entries()) {
604
+ if (this.buckets.size <= MAX_BUCKETS * 0.9) {
605
+ break;
606
+ }
607
+ if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
608
+ this.buckets.delete(key);
609
+ this.queuesByBucket.delete(key);
610
+ this.pendingBuckets.delete(key);
611
+ }
612
+ }
613
+ }
584
614
  cleanupBucket(bucketKey, bucket, intervalMs) {
585
615
  const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
586
616
  if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
@@ -783,13 +813,12 @@ var TtlResolver = class {
783
813
  return;
784
814
  }
785
815
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
786
- let removed = 0;
787
- for (const key of this.accessProfiles.keys()) {
788
- if (removed >= toRemove) {
789
- break;
816
+ const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
817
+ for (let i = 0; i < toRemove && i < sorted.length; i++) {
818
+ const entry = sorted[i];
819
+ if (entry) {
820
+ this.accessProfiles.delete(entry[0]);
790
821
  }
791
- this.accessProfiles.delete(key);
792
- removed += 1;
793
822
  }
794
823
  }
795
824
  };
@@ -872,6 +901,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
872
901
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
873
902
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
874
903
  var MAX_CACHE_KEY_LENGTH = 1024;
904
+ var MAX_PATTERN_LENGTH = 1024;
875
905
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
876
906
  var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
877
907
  var DebugLogger = class {
@@ -1305,6 +1335,7 @@ var CacheStack = class extends EventEmitter {
1305
1335
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1306
1336
  }
1307
1337
  async invalidateByPattern(pattern) {
1338
+ this.validatePattern(pattern);
1308
1339
  await this.awaitStartup("invalidateByPattern");
1309
1340
  const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1310
1341
  await this.deleteKeys(keys);
@@ -2198,6 +2229,17 @@ var CacheStack = class extends EventEmitter {
2198
2229
  }
2199
2230
  return key;
2200
2231
  }
2232
+ validatePattern(pattern) {
2233
+ if (pattern.length === 0) {
2234
+ throw new Error("Pattern must not be empty.");
2235
+ }
2236
+ if (pattern.length > MAX_PATTERN_LENGTH) {
2237
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
2238
+ }
2239
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
2240
+ throw new Error("Pattern contains unsupported control characters.");
2241
+ }
2242
+ }
2201
2243
  validateTtlPolicy(name, policy) {
2202
2244
  if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2203
2245
  return;
@@ -2362,7 +2404,18 @@ var CacheStack = class extends EventEmitter {
2362
2404
  }
2363
2405
  };
2364
2406
  function createInstanceId() {
2365
- return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2407
+ if (globalThis.crypto?.randomUUID) {
2408
+ return globalThis.crypto.randomUUID();
2409
+ }
2410
+ const bytes = new Uint8Array(16);
2411
+ if (globalThis.crypto?.getRandomValues) {
2412
+ globalThis.crypto.getRandomValues(bytes);
2413
+ } else {
2414
+ for (let i = 0; i < bytes.length; i++) {
2415
+ bytes[i] = Math.floor(Math.random() * 256);
2416
+ }
2417
+ }
2418
+ return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
2366
2419
  }
2367
2420
 
2368
2421
  // src/invalidation/RedisInvalidationBus.ts
@@ -2404,7 +2457,7 @@ var RedisInvalidationBus = class {
2404
2457
  async dispatchToHandlers(payload) {
2405
2458
  let message;
2406
2459
  try {
2407
- const parsed = JSON.parse(payload);
2460
+ const parsed = sanitizeJsonValue2(JSON.parse(payload));
2408
2461
  if (!this.isInvalidationMessage(parsed)) {
2409
2462
  throw new Error("Invalid invalidation payload shape.");
2410
2463
  }
@@ -2441,12 +2494,30 @@ var RedisInvalidationBus = class {
2441
2494
  console.error(`[layercache] ${message}`, error);
2442
2495
  }
2443
2496
  };
2497
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2498
+ function sanitizeJsonValue2(value) {
2499
+ if (Array.isArray(value)) {
2500
+ return value.map(sanitizeJsonValue2);
2501
+ }
2502
+ if (value && typeof value === "object") {
2503
+ const result = /* @__PURE__ */ Object.create(null);
2504
+ for (const key of Object.keys(value)) {
2505
+ if (!DANGEROUS_KEYS.has(key)) {
2506
+ result[key] = sanitizeJsonValue2(value[key]);
2507
+ }
2508
+ }
2509
+ return result;
2510
+ }
2511
+ return value;
2512
+ }
2444
2513
 
2445
2514
  // src/http/createCacheStatsHandler.ts
2446
2515
  function createCacheStatsHandler(cache) {
2447
2516
  return async (_request, response) => {
2448
2517
  response.statusCode = 200;
2449
2518
  response.setHeader?.("content-type", "application/json; charset=utf-8");
2519
+ response.setHeader?.("cache-control", "no-store");
2520
+ response.setHeader?.("x-content-type-options", "nosniff");
2450
2521
  response.end(JSON.stringify(cache.getStats(), null, 2));
2451
2522
  };
2452
2523
  }
@@ -2532,7 +2603,7 @@ function normalizeUrl(url) {
2532
2603
  try {
2533
2604
  const parsed = new URL(url, "http://localhost");
2534
2605
  parsed.searchParams.sort();
2535
- return decodeURIComponent(parsed.pathname) + parsed.search;
2606
+ return parsed.pathname + parsed.search;
2536
2607
  } catch {
2537
2608
  return url;
2538
2609
  }
@@ -2822,8 +2893,9 @@ var RedisLayer = class {
2822
2893
  }
2823
2894
  }
2824
2895
  try {
2825
- await this.client.del(this.withPrefix(key)).catch(() => void 0);
2826
- } catch {
2896
+ await this.client.del(this.withPrefix(key));
2897
+ } catch (deleteError) {
2898
+ console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
2827
2899
  }
2828
2900
  return null;
2829
2901
  }
@@ -3223,7 +3295,7 @@ var MemcachedLayer = class {
3223
3295
 
3224
3296
  // src/serialization/MsgpackSerializer.ts
3225
3297
  import { decode, encode } from "@msgpack/msgpack";
3226
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3298
+ var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3227
3299
  var MsgpackSerializer = class {
3228
3300
  serialize(value) {
3229
3301
  return Buffer.from(encode(value));
@@ -3242,7 +3314,7 @@ function sanitizeMsgpackValue(value) {
3242
3314
  }
3243
3315
  const sanitized = {};
3244
3316
  for (const [key, entry] of Object.entries(value)) {
3245
- if (DANGEROUS_KEYS.has(key)) {
3317
+ if (DANGEROUS_KEYS2.has(key)) {
3246
3318
  continue;
3247
3319
  }
3248
3320
  sanitized[key] = sanitizeMsgpackValue(entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layercache",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Hardened multi-layer caching for Node.js with memory, Redis, stampede prevention, and operational invalidation helpers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -374,6 +374,7 @@ var CacheNamespace = class _CacheNamespace {
374
374
  * ```
375
375
  */
376
376
  namespace(childPrefix) {
377
+ validateNamespaceKey(childPrefix);
377
378
  return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
378
379
  }
379
380
  qualify(key) {
@@ -503,6 +504,17 @@ function addMap(base, delta) {
503
504
  }
504
505
  return result;
505
506
  }
507
+ function validateNamespaceKey(key) {
508
+ if (key.length === 0) {
509
+ throw new Error("Namespace prefix must not be empty.");
510
+ }
511
+ if (key.length > 256) {
512
+ throw new Error("Namespace prefix must be at most 256 characters.");
513
+ }
514
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
515
+ throw new Error("Namespace prefix contains unsupported control characters.");
516
+ }
517
+ }
506
518
 
507
519
  // ../../src/invalidation/PatternMatcher.ts
508
520
  var PatternMatcher = class _PatternMatcher {
@@ -623,9 +635,7 @@ var CircuitBreakerManager = class {
623
635
  }
624
636
  const now = Date.now();
625
637
  if (state.openUntil <= now) {
626
- state.openUntil = null;
627
- state.failures = 0;
628
- this.breakers.set(key, state);
638
+ this.breakers.delete(key);
629
639
  return;
630
640
  }
631
641
  const remainingMs = state.openUntil - now;
@@ -636,15 +646,15 @@ var CircuitBreakerManager = class {
636
646
  if (!options) {
637
647
  return;
638
648
  }
649
+ this.pruneIfNeeded();
639
650
  const failureThreshold = options.failureThreshold ?? 3;
640
651
  const cooldownMs = options.cooldownMs ?? 3e4;
641
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
652
+ const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
642
653
  state.failures += 1;
643
654
  if (state.failures >= failureThreshold) {
644
655
  state.openUntil = Date.now() + cooldownMs;
645
656
  }
646
657
  this.breakers.set(key, state);
647
- this.pruneIfNeeded();
648
658
  }
649
659
  recordSuccess(key) {
650
660
  this.breakers.delete(key);
@@ -655,8 +665,7 @@ var CircuitBreakerManager = class {
655
665
  return false;
656
666
  }
657
667
  if (state.openUntil <= Date.now()) {
658
- state.openUntil = null;
659
- state.failures = 0;
668
+ this.breakers.delete(key);
660
669
  return false;
661
670
  }
662
671
  return true;
@@ -680,15 +689,20 @@ var CircuitBreakerManager = class {
680
689
  if (this.breakers.size <= this.maxEntries) {
681
690
  return;
682
691
  }
692
+ const now = Date.now();
683
693
  for (const [key, state] of this.breakers.entries()) {
684
694
  if (this.breakers.size <= this.maxEntries) {
685
- break;
695
+ return;
686
696
  }
687
- if (!state.openUntil || state.openUntil <= Date.now()) {
697
+ if (!state.openUntil || state.openUntil <= now) {
688
698
  this.breakers.delete(key);
689
699
  }
690
700
  }
691
- for (const key of this.breakers.keys()) {
701
+ if (this.breakers.size <= this.maxEntries) {
702
+ return;
703
+ }
704
+ const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
705
+ for (const [key] of sorted) {
692
706
  if (this.breakers.size <= this.maxEntries) {
693
707
  break;
694
708
  }
@@ -698,6 +712,7 @@ var CircuitBreakerManager = class {
698
712
  };
699
713
 
700
714
  // ../../src/internal/FetchRateLimiter.ts
715
+ var MAX_BUCKETS = 1e4;
701
716
  var FetchRateLimiter = class {
702
717
  buckets = /* @__PURE__ */ new Map();
703
718
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -863,10 +878,25 @@ var FetchRateLimiter = class {
863
878
  if (existing) {
864
879
  return existing;
865
880
  }
881
+ if (this.buckets.size >= MAX_BUCKETS) {
882
+ this.evictIdleBuckets();
883
+ }
866
884
  const bucket = { active: 0, startedAt: [] };
867
885
  this.buckets.set(bucketKey, bucket);
868
886
  return bucket;
869
887
  }
888
+ evictIdleBuckets() {
889
+ for (const [key, bucket] of this.buckets.entries()) {
890
+ if (this.buckets.size <= MAX_BUCKETS * 0.9) {
891
+ break;
892
+ }
893
+ if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
894
+ this.buckets.delete(key);
895
+ this.queuesByBucket.delete(key);
896
+ this.pendingBuckets.delete(key);
897
+ }
898
+ }
899
+ }
870
900
  cleanupBucket(bucketKey, bucket, intervalMs) {
871
901
  const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
872
902
  if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
@@ -1193,18 +1223,18 @@ var TtlResolver = class {
1193
1223
  return;
1194
1224
  }
1195
1225
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
1196
- let removed = 0;
1197
- for (const key of this.accessProfiles.keys()) {
1198
- if (removed >= toRemove) {
1199
- break;
1226
+ const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
1227
+ for (let i = 0; i < toRemove && i < sorted.length; i++) {
1228
+ const entry = sorted[i];
1229
+ if (entry) {
1230
+ this.accessProfiles.delete(entry[0]);
1200
1231
  }
1201
- this.accessProfiles.delete(key);
1202
- removed += 1;
1203
1232
  }
1204
1233
  }
1205
1234
  };
1206
1235
 
1207
1236
  // ../../src/invalidation/TagIndex.ts
1237
+ var MAX_PATTERN_RECURSION_DEPTH = 500;
1208
1238
  var TagIndex = class {
1209
1239
  tagToKeys = /* @__PURE__ */ new Map();
1210
1240
  keyToTags = /* @__PURE__ */ new Map();
@@ -1259,7 +1289,7 @@ var TagIndex = class {
1259
1289
  }
1260
1290
  async matchPattern(pattern) {
1261
1291
  const matches = /* @__PURE__ */ new Set();
1262
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
1292
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1263
1293
  return [...matches];
1264
1294
  }
1265
1295
  async clear() {
@@ -1311,7 +1341,10 @@ var TagIndex = class {
1311
1341
  this.collectFromNode(child, `${prefix}${character}`, matches);
1312
1342
  }
1313
1343
  }
1314
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
1344
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
1345
+ if (depth > MAX_PATTERN_RECURSION_DEPTH) {
1346
+ return;
1347
+ }
1315
1348
  const stateKey = `${node.id}:${patternIndex}`;
1316
1349
  if (visited.has(stateKey)) {
1317
1350
  return;
@@ -1328,21 +1361,37 @@ var TagIndex = class {
1328
1361
  return;
1329
1362
  }
1330
1363
  if (patternChar === "*") {
1331
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
1364
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
1332
1365
  for (const [character, child2] of node.children) {
1333
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
1366
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
1334
1367
  }
1335
1368
  return;
1336
1369
  }
1337
1370
  if (patternChar === "?") {
1338
1371
  for (const [character, child2] of node.children) {
1339
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
1372
+ this.collectPatternMatches(
1373
+ child2,
1374
+ `${prefix}${character}`,
1375
+ pattern,
1376
+ patternIndex + 1,
1377
+ matches,
1378
+ visited,
1379
+ depth + 1
1380
+ );
1340
1381
  }
1341
1382
  return;
1342
1383
  }
1343
1384
  const child = node.children.get(patternChar);
1344
1385
  if (child) {
1345
- this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
1386
+ this.collectPatternMatches(
1387
+ child,
1388
+ `${prefix}${patternChar}`,
1389
+ pattern,
1390
+ patternIndex + 1,
1391
+ matches,
1392
+ visited,
1393
+ depth + 1
1394
+ );
1346
1395
  }
1347
1396
  }
1348
1397
  pruneKnownKeysIfNeeded() {
@@ -1484,6 +1533,7 @@ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1484
1533
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1485
1534
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1486
1535
  var MAX_CACHE_KEY_LENGTH = 1024;
1536
+ var MAX_PATTERN_LENGTH = 1024;
1487
1537
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1488
1538
  var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1489
1539
  var DebugLogger = class {
@@ -1917,6 +1967,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1917
1967
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1918
1968
  }
1919
1969
  async invalidateByPattern(pattern) {
1970
+ this.validatePattern(pattern);
1920
1971
  await this.awaitStartup("invalidateByPattern");
1921
1972
  const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1922
1973
  await this.deleteKeys(keys);
@@ -2810,6 +2861,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
2810
2861
  }
2811
2862
  return key;
2812
2863
  }
2864
+ validatePattern(pattern) {
2865
+ if (pattern.length === 0) {
2866
+ throw new Error("Pattern must not be empty.");
2867
+ }
2868
+ if (pattern.length > MAX_PATTERN_LENGTH) {
2869
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
2870
+ }
2871
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
2872
+ throw new Error("Pattern contains unsupported control characters.");
2873
+ }
2874
+ }
2813
2875
  validateTtlPolicy(name, policy) {
2814
2876
  if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2815
2877
  return;
@@ -2974,7 +3036,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
2974
3036
  }
2975
3037
  };
2976
3038
  function createInstanceId() {
2977
- return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
3039
+ if (globalThis.crypto?.randomUUID) {
3040
+ return globalThis.crypto.randomUUID();
3041
+ }
3042
+ const bytes = new Uint8Array(16);
3043
+ if (globalThis.crypto?.getRandomValues) {
3044
+ globalThis.crypto.getRandomValues(bytes);
3045
+ } else {
3046
+ for (let i = 0; i < bytes.length; i++) {
3047
+ bytes[i] = Math.floor(Math.random() * 256);
3048
+ }
3049
+ }
3050
+ return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
2978
3051
  }
2979
3052
 
2980
3053
  // src/module.ts
@@ -555,6 +555,7 @@ declare class CacheStack extends EventEmitter {
555
555
  private validateRateLimitOptions;
556
556
  private validateNonNegativeNumber;
557
557
  private validateCacheKey;
558
+ private validatePattern;
558
559
  private validateTtlPolicy;
559
560
  private assertActive;
560
561
  private awaitStartup;
@@ -555,6 +555,7 @@ declare class CacheStack extends EventEmitter {
555
555
  private validateRateLimitOptions;
556
556
  private validateNonNegativeNumber;
557
557
  private validateCacheKey;
558
+ private validatePattern;
558
559
  private validateTtlPolicy;
559
560
  private assertActive;
560
561
  private awaitStartup;