layercache 1.2.2 → 1.2.3

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.
@@ -177,6 +177,7 @@ interface CacheStackOptions {
177
177
  invalidationBus?: InvalidationBus;
178
178
  tagIndex?: CacheTagIndex;
179
179
  generation?: number;
180
+ generationCleanup?: boolean | CacheGenerationCleanupOptions;
180
181
  broadcastL1Invalidation?: boolean;
181
182
  /**
182
183
  * @deprecated Use `broadcastL1Invalidation` instead.
@@ -195,11 +196,13 @@ interface CacheStackOptions {
195
196
  writeStrategy?: 'write-through' | 'write-behind';
196
197
  writeBehind?: CacheWriteBehindOptions;
197
198
  fetcherRateLimit?: CacheRateLimitOptions;
199
+ backgroundRefreshTimeoutMs?: number;
198
200
  singleFlightCoordinator?: CacheSingleFlightCoordinator;
199
201
  singleFlightLeaseMs?: number;
200
202
  singleFlightTimeoutMs?: number;
201
203
  singleFlightPollMs?: number;
202
204
  singleFlightRenewIntervalMs?: number;
205
+ snapshotBaseDir?: string | false;
203
206
  /**
204
207
  * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
205
208
  * before the oldest entries are pruned. Prevents unbounded memory growth.
@@ -212,6 +215,9 @@ interface CacheAdaptiveTtlOptions {
212
215
  step?: number | LayerTtlMap;
213
216
  maxTtl?: number | LayerTtlMap;
214
217
  }
218
+ interface CacheGenerationCleanupOptions {
219
+ batchSize?: number;
220
+ }
215
221
  type CacheTtlPolicy = 'until-midnight' | 'next-hour' | {
216
222
  alignTo: number;
217
223
  } | ((context: CacheTtlPolicyContext) => number | undefined);
@@ -415,7 +421,7 @@ interface TagIndexOptions {
415
421
  /**
416
422
  * Maximum number of keys tracked in `knownKeys`. When exceeded, the oldest
417
423
  * 10 % of keys are pruned to keep memory bounded.
418
- * Defaults to unlimited.
424
+ * Defaults to 100,000.
419
425
  */
420
426
  maxKnownKeys?: number;
421
427
  }
@@ -424,6 +430,8 @@ declare class TagIndex implements CacheTagIndex {
424
430
  private readonly keyToTags;
425
431
  private readonly knownKeys;
426
432
  private readonly maxKnownKeys;
433
+ private nextNodeId;
434
+ private readonly root;
427
435
  constructor(options?: TagIndexOptions);
428
436
  touch(key: string): Promise<void>;
429
437
  track(key: string, tags: string[]): Promise<void>;
@@ -433,8 +441,14 @@ declare class TagIndex implements CacheTagIndex {
433
441
  tagsForKey(key: string): Promise<string[]>;
434
442
  matchPattern(pattern: string): Promise<string[]>;
435
443
  clear(): Promise<void>;
444
+ private createTrieNode;
445
+ private insertKnownKey;
446
+ private findNode;
447
+ private collectFromNode;
448
+ private collectPatternMatches;
436
449
  private pruneKnownKeysIfNeeded;
437
450
  private removeKey;
451
+ private removeKnownKey;
438
452
  }
439
453
 
440
454
  declare class CacheNamespace {
@@ -505,6 +519,7 @@ declare class CacheStack extends EventEmitter {
505
519
  private readonly logger;
506
520
  private readonly tagIndex;
507
521
  private readonly fetchRateLimiter;
522
+ private readonly snapshotSerializer;
508
523
  private readonly backgroundRefreshes;
509
524
  private readonly layerDegradedUntil;
510
525
  private readonly ttlResolver;
@@ -513,6 +528,7 @@ declare class CacheStack extends EventEmitter {
513
528
  private readonly writeBehindQueue;
514
529
  private writeBehindTimer?;
515
530
  private writeBehindFlushPromise?;
531
+ private generationCleanupPromise?;
516
532
  private isDisconnecting;
517
533
  private disconnectPromise?;
518
534
  constructor(layers: CacheLayer[], options?: CacheStackOptions);
@@ -523,6 +539,7 @@ declare class CacheStack extends EventEmitter {
523
539
  * and no `fetcher` is provided.
524
540
  */
525
541
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
542
+ private getPrepared;
526
543
  /**
527
544
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
528
545
  * Fetches and caches the value if not already present.
@@ -581,6 +598,11 @@ declare class CacheStack extends EventEmitter {
581
598
  */
582
599
  getHitRate(): CacheHitRateSnapshot;
583
600
  healthCheck(): Promise<CacheHealthCheckResult[]>;
601
+ /**
602
+ * Rotates the active generation prefix used for all future cache keys.
603
+ * Previous-generation keys remain in the underlying layers until they expire,
604
+ * unless `generationCleanup` is enabled to prune them in the background.
605
+ */
584
606
  bumpGeneration(nextGeneration?: number): number;
585
607
  /**
586
608
  * Returns detailed metadata about a single cache key: which layers contain it,
@@ -608,6 +630,7 @@ declare class CacheStack extends EventEmitter {
608
630
  private resolveLayerSeconds;
609
631
  private shouldNegativeCache;
610
632
  private scheduleBackgroundRefresh;
633
+ private runBackgroundRefresh;
611
634
  private resolveSingleFlightOptions;
612
635
  private deleteKeys;
613
636
  private publishInvalidation;
@@ -615,7 +638,14 @@ declare class CacheStack extends EventEmitter {
615
638
  private getTagsForKey;
616
639
  private formatError;
617
640
  private sleep;
641
+ private withTimeout;
618
642
  private shouldBroadcastL1Invalidation;
643
+ private collectKeysWithPrefix;
644
+ private collectKeysMatchingPattern;
645
+ private shouldCleanupGenerations;
646
+ private generationCleanupBatchSize;
647
+ private scheduleGenerationCleanup;
648
+ private cleanupGeneration;
619
649
  private initializeWriteBehind;
620
650
  private shouldWriteBehind;
621
651
  private enqueueWriteBehind;
@@ -643,12 +673,15 @@ declare class CacheStack extends EventEmitter {
643
673
  private applyFreshReadPolicies;
644
674
  private shouldSkipLayer;
645
675
  private handleLayerFailure;
676
+ private reportRecoverableLayerFailure;
646
677
  private isGracefulDegradationEnabled;
647
678
  private recordCircuitFailure;
648
679
  private isNegativeStoredValue;
649
680
  private emitError;
650
681
  private serializeKeyPart;
651
682
  private isCacheSnapshotEntries;
683
+ private sanitizeSnapshotValue;
684
+ private validateSnapshotFilePath;
652
685
  private normalizeForSerialization;
653
686
  }
654
687
 
package/dist/edge.cjs CHANGED
@@ -29,7 +29,30 @@ module.exports = __toCommonJS(edge_exports);
29
29
 
30
30
  // src/internal/StoredValue.ts
31
31
  function isStoredValueEnvelope(value) {
32
- return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
32
+ if (typeof value !== "object" || value === null) {
33
+ return false;
34
+ }
35
+ const v = value;
36
+ if (v.__layercache !== 1) {
37
+ return false;
38
+ }
39
+ if (v.kind !== "value" && v.kind !== "empty") {
40
+ return false;
41
+ }
42
+ if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
43
+ return false;
44
+ }
45
+ if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
46
+ return false;
47
+ }
48
+ if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
49
+ return false;
50
+ }
51
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
52
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
53
+ return false;
54
+ }
55
+ return true;
33
56
  }
34
57
  function unwrapStoredValue(stored) {
35
58
  if (!isStoredValueEnvelope(stored)) {
@@ -87,16 +110,10 @@ var MemoryLayer = class {
87
110
  return entry.value;
88
111
  }
89
112
  async getMany(keys) {
90
- const values = [];
91
- for (const key of keys) {
92
- values.push(await this.getEntry(key));
93
- }
94
- return values;
113
+ return Promise.all(keys.map((key) => this.getEntry(key)));
95
114
  }
96
115
  async setMany(entries) {
97
- for (const entry of entries) {
98
- await this.set(entry.key, entry.value, entry.ttl);
99
- }
116
+ await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
100
117
  }
101
118
  async set(key, value, ttl = this.defaultTtl) {
102
119
  this.entries.delete(key);
@@ -283,15 +300,17 @@ var TagIndex = class {
283
300
  keyToTags = /* @__PURE__ */ new Map();
284
301
  knownKeys = /* @__PURE__ */ new Set();
285
302
  maxKnownKeys;
303
+ nextNodeId = 1;
304
+ root = this.createTrieNode();
286
305
  constructor(options = {}) {
287
- this.maxKnownKeys = options.maxKnownKeys;
306
+ this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
288
307
  }
289
308
  async touch(key) {
290
- this.knownKeys.add(key);
309
+ this.insertKnownKey(key);
291
310
  this.pruneKnownKeysIfNeeded();
292
311
  }
293
312
  async track(key, tags) {
294
- this.knownKeys.add(key);
313
+ this.insertKnownKey(key);
295
314
  this.pruneKnownKeysIfNeeded();
296
315
  if (tags.length === 0) {
297
316
  return;
@@ -317,18 +336,104 @@ var TagIndex = class {
317
336
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
318
337
  }
319
338
  async keysForPrefix(prefix) {
320
- return [...this.knownKeys].filter((key) => key.startsWith(prefix));
339
+ const node = this.findNode(prefix);
340
+ if (!node) {
341
+ return [];
342
+ }
343
+ const matches = [];
344
+ this.collectFromNode(node, prefix, matches);
345
+ return matches;
321
346
  }
322
347
  async tagsForKey(key) {
323
348
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
324
349
  }
325
350
  async matchPattern(pattern) {
326
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
351
+ const matches = /* @__PURE__ */ new Set();
352
+ this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
353
+ return [...matches];
327
354
  }
328
355
  async clear() {
329
356
  this.tagToKeys.clear();
330
357
  this.keyToTags.clear();
331
358
  this.knownKeys.clear();
359
+ this.root.children.clear();
360
+ this.root.terminal = false;
361
+ this.nextNodeId = this.root.id + 1;
362
+ }
363
+ createTrieNode() {
364
+ return {
365
+ id: this.nextNodeId++,
366
+ terminal: false,
367
+ children: /* @__PURE__ */ new Map()
368
+ };
369
+ }
370
+ insertKnownKey(key) {
371
+ if (this.knownKeys.has(key)) {
372
+ return;
373
+ }
374
+ this.knownKeys.add(key);
375
+ let node = this.root;
376
+ for (const character of key) {
377
+ let child = node.children.get(character);
378
+ if (!child) {
379
+ child = this.createTrieNode();
380
+ node.children.set(character, child);
381
+ }
382
+ node = child;
383
+ }
384
+ node.terminal = true;
385
+ }
386
+ findNode(prefix) {
387
+ let node = this.root;
388
+ for (const character of prefix) {
389
+ node = node.children.get(character);
390
+ if (!node) {
391
+ return void 0;
392
+ }
393
+ }
394
+ return node;
395
+ }
396
+ collectFromNode(node, prefix, matches) {
397
+ if (node.terminal) {
398
+ matches.push(prefix);
399
+ }
400
+ for (const [character, child] of node.children) {
401
+ this.collectFromNode(child, `${prefix}${character}`, matches);
402
+ }
403
+ }
404
+ collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited) {
405
+ const stateKey = `${node.id}:${patternIndex}`;
406
+ if (visited.has(stateKey)) {
407
+ return;
408
+ }
409
+ visited.add(stateKey);
410
+ if (patternIndex === pattern.length) {
411
+ if (node.terminal) {
412
+ matches.add(prefix);
413
+ }
414
+ return;
415
+ }
416
+ const patternChar = pattern[patternIndex];
417
+ if (patternChar === void 0) {
418
+ return;
419
+ }
420
+ if (patternChar === "*") {
421
+ this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
422
+ for (const [character, child2] of node.children) {
423
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
424
+ }
425
+ return;
426
+ }
427
+ if (patternChar === "?") {
428
+ for (const [character, child2] of node.children) {
429
+ this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex + 1, matches, visited);
430
+ }
431
+ return;
432
+ }
433
+ const child = node.children.get(patternChar);
434
+ if (child) {
435
+ this.collectPatternMatches(child, `${prefix}${patternChar}`, pattern, patternIndex + 1, matches, visited);
436
+ }
332
437
  }
333
438
  pruneKnownKeysIfNeeded() {
334
439
  if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
@@ -345,7 +450,7 @@ var TagIndex = class {
345
450
  }
346
451
  }
347
452
  removeKey(key) {
348
- this.knownKeys.delete(key);
453
+ this.removeKnownKey(key);
349
454
  const tags = this.keyToTags.get(key);
350
455
  if (!tags) {
351
456
  return;
@@ -362,6 +467,34 @@ var TagIndex = class {
362
467
  }
363
468
  this.keyToTags.delete(key);
364
469
  }
470
+ removeKnownKey(key) {
471
+ if (!this.knownKeys.delete(key)) {
472
+ return;
473
+ }
474
+ const path = [];
475
+ let node = this.root;
476
+ for (const character of key) {
477
+ const child = node.children.get(character);
478
+ if (!child) {
479
+ return;
480
+ }
481
+ path.push([node, character]);
482
+ node = child;
483
+ }
484
+ node.terminal = false;
485
+ for (let index = path.length - 1; index >= 0; index -= 1) {
486
+ const entry = path[index];
487
+ if (!entry) {
488
+ continue;
489
+ }
490
+ const [parent, character] = entry;
491
+ const child = parent.children.get(character);
492
+ if (!child || child.terminal || child.children.size > 0) {
493
+ break;
494
+ }
495
+ parent.children.delete(character);
496
+ }
497
+ }
365
498
  };
366
499
 
367
500
  // src/integrations/hono.ts
@@ -373,7 +506,8 @@ function createHonoCacheMiddleware(cache, options = {}) {
373
506
  await next();
374
507
  return;
375
508
  }
376
- const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
509
+ const rawPath = context.req.path ?? context.req.url ?? "/";
510
+ const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl(rawPath)}`;
377
511
  const cached = await cache.get(key, void 0, options);
378
512
  if (cached !== null) {
379
513
  context.header?.("x-cache", "HIT");
@@ -384,12 +518,26 @@ function createHonoCacheMiddleware(cache, options = {}) {
384
518
  const originalJson = context.json.bind(context);
385
519
  context.json = (body, status) => {
386
520
  context.header?.("x-cache", "MISS");
387
- void cache.set(key, body, options);
521
+ cache.set(key, body, options).catch((err) => {
522
+ cache.emit("error", {
523
+ operation: "set",
524
+ error: err instanceof Error ? err.message : String(err)
525
+ });
526
+ });
388
527
  return originalJson(body, status);
389
528
  };
390
529
  await next();
391
530
  };
392
531
  }
532
+ function normalizeUrl(url) {
533
+ try {
534
+ const parsed = new URL(url, "http://localhost");
535
+ parsed.searchParams.sort();
536
+ return decodeURIComponent(parsed.pathname) + parsed.search;
537
+ } catch {
538
+ return url;
539
+ }
540
+ }
393
541
  // Annotate the CommonJS export names for ESM import in node:
394
542
  0 && (module.exports = {
395
543
  MemoryLayer,
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-DLpdQN0W.cjs';
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-B_rUqDy6.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-DLpdQN0W.js';
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-B_rUqDy6.js';
2
2
  import 'node:events';
package/dist/edge.js CHANGED
@@ -2,10 +2,10 @@ import {
2
2
  MemoryLayer,
3
3
  TagIndex,
4
4
  createHonoCacheMiddleware
5
- } from "./chunk-46UH7LNM.js";
5
+ } from "./chunk-KOYGHLVP.js";
6
6
  import {
7
7
  PatternMatcher
8
- } from "./chunk-ZMDB5KOK.js";
8
+ } from "./chunk-7V7XAB74.js";
9
9
  export {
10
10
  MemoryLayer,
11
11
  PatternMatcher,