layercache 1.2.7 → 1.2.9

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 CHANGED
@@ -15,8 +15,8 @@
15
15
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache_2.0-green" alt="license"></a>
16
16
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-first-3178C6?logo=typescript&logoColor=white" alt="TypeScript"></a>
17
17
  <img src="https://img.shields.io/badge/Node.js-%E2%89%A5_20-339933?logo=nodedotjs&logoColor=white" alt="Node.js >= 20">
18
- <img src="https://img.shields.io/badge/tests-393_passing-brightgreen" alt="tests">
19
- <a href="https://coveralls.io/github/flyingsquirrel0419/layercache?branch=main"><img src="https://coveralls.io/repos/github/flyingsquirrel0419/layercache/badge.svg?branch=main" alt="Coveralls"></a>
18
+ <img src="https://img.shields.io/badge/tests-411_passing-brightgreen" alt="tests">
19
+ <a href="https://coveralls.io/github/flyingsquirrel0419/layercache?branch=main"><img src="https://coveralls.io/repos/github/flyingsquirrel0419/layercache/badge.svg?branch=main&t=20260410" alt="Coveralls"></a>
20
20
  </p>
21
21
 
22
22
  <p align="center">
@@ -163,7 +163,7 @@ const cache = new CacheStack([
163
163
  | **Per-layer latency** | Avg, max, and sample count using Welford's algorithm |
164
164
  | **Health checks** | Async health endpoint per layer with latency measurement |
165
165
  | **Event hooks** | `hit`, `miss`, `set`, `delete`, `stale-serve`, `stampede-dedupe`, `backfill`, `warm`, `error` |
166
- | **OpenTelemetry** | Distributed tracing support |
166
+ | **OpenTelemetry** | Hook-based distributed tracing support without method monkey-patching |
167
167
  | **Prometheus exporter** | Metrics export including latency gauges |
168
168
  | **HTTP stats handler** | JSON endpoint for dashboards |
169
169
  | **Admin CLI** | `npx layercache stats\|keys\|invalidate` for Redis-backed caches |
@@ -183,7 +183,7 @@ layercache plugs into the frameworks you already use:
183
183
  | **GraphQL** | `cacheGraphqlResolver(cache, prefix, resolver, opts)` - field resolver wrapper |
184
184
  | **NestJS** | `@cachestack/nestjs` - `CacheStackModule.forRoot()`, `@Cacheable()` decorator |
185
185
  | **Next.js** | Works natively with App Router and API routes |
186
- | **OpenTelemetry** | `createOpenTelemetryPlugin(cache, tracer)` - distributed tracing spans |
186
+ | **OpenTelemetry** | `createOpenTelemetryPlugin(cache, tracer)` - event-driven tracing spans without monkey-patching |
187
187
 
188
188
  <details>
189
189
  <summary><b>Express example</b></summary>
package/dist/cli.cjs CHANGED
@@ -373,7 +373,7 @@ async function main(argv = process.argv.slice(2)) {
373
373
  try {
374
374
  await redis.connect().catch((error) => {
375
375
  const message = error instanceof Error ? error.message : String(error);
376
- throw new Error(`Failed to connect to Redis: ${message}`);
376
+ throw new Error(`Failed to connect to Redis at ${maskRedisUrl(redisUrl)}: ${message}`);
377
377
  });
378
378
  if (args.command === "stats") {
379
379
  const keys = await scanKeys(redis, args.pattern ?? "*");
@@ -465,6 +465,11 @@ function parseArgs(argv) {
465
465
  const token = rest[index];
466
466
  const value = rest[index + 1];
467
467
  if (token === "--redis") {
468
+ if (!value || value.startsWith("--")) {
469
+ process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
470
+ process.exitCode = 1;
471
+ return parsed;
472
+ }
468
473
  parsed.redisUrl = value;
469
474
  index += 1;
470
475
  } else if (token === "--pattern") {
@@ -484,6 +489,7 @@ function parseArgs(argv) {
484
489
  return parsed;
485
490
  }
486
491
  var BATCH_DELETE_SIZE = 500;
492
+ var SCAN_MAX_KEYS = 1e6;
487
493
  async function batchDelete(redis, keys) {
488
494
  for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
489
495
  const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
@@ -497,6 +503,13 @@ async function scanKeys(redis, pattern) {
497
503
  const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
498
504
  cursor = nextCursor;
499
505
  keys.push(...batch);
506
+ if (keys.length >= SCAN_MAX_KEYS) {
507
+ process.stderr.write(
508
+ `Warning: stopped scanning after ${SCAN_MAX_KEYS} keys. Use a more specific --pattern to narrow results.
509
+ `
510
+ );
511
+ return keys;
512
+ }
500
513
  } while (cursor !== "0");
501
514
  return keys;
502
515
  }
@@ -528,7 +541,18 @@ function summarizeInspectableValue(value) {
528
541
  }
529
542
  return value;
530
543
  }
531
- if (process.argv[1]?.includes("cli.")) {
544
+ function maskRedisUrl(url) {
545
+ try {
546
+ const parsed = new URL(url);
547
+ if (parsed.password) {
548
+ parsed.password = "***";
549
+ }
550
+ return parsed.toString();
551
+ } catch {
552
+ return url.replace(/:([^@/]+)@/, ":***@");
553
+ }
554
+ }
555
+ if (process.argv[1]?.endsWith("cli.cjs") || process.argv[1]?.endsWith("cli.js")) {
532
556
  void main();
533
557
  }
534
558
  // Annotate the CommonJS export names for ESM import in node:
package/dist/cli.js CHANGED
@@ -31,7 +31,7 @@ async function main(argv = process.argv.slice(2)) {
31
31
  try {
32
32
  await redis.connect().catch((error) => {
33
33
  const message = error instanceof Error ? error.message : String(error);
34
- throw new Error(`Failed to connect to Redis: ${message}`);
34
+ throw new Error(`Failed to connect to Redis at ${maskRedisUrl(redisUrl)}: ${message}`);
35
35
  });
36
36
  if (args.command === "stats") {
37
37
  const keys = await scanKeys(redis, args.pattern ?? "*");
@@ -123,6 +123,11 @@ function parseArgs(argv) {
123
123
  const token = rest[index];
124
124
  const value = rest[index + 1];
125
125
  if (token === "--redis") {
126
+ if (!value || value.startsWith("--")) {
127
+ process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
128
+ process.exitCode = 1;
129
+ return parsed;
130
+ }
126
131
  parsed.redisUrl = value;
127
132
  index += 1;
128
133
  } else if (token === "--pattern") {
@@ -142,6 +147,7 @@ function parseArgs(argv) {
142
147
  return parsed;
143
148
  }
144
149
  var BATCH_DELETE_SIZE = 500;
150
+ var SCAN_MAX_KEYS = 1e6;
145
151
  async function batchDelete(redis, keys) {
146
152
  for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
147
153
  const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
@@ -155,6 +161,13 @@ async function scanKeys(redis, pattern) {
155
161
  const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
156
162
  cursor = nextCursor;
157
163
  keys.push(...batch);
164
+ if (keys.length >= SCAN_MAX_KEYS) {
165
+ process.stderr.write(
166
+ `Warning: stopped scanning after ${SCAN_MAX_KEYS} keys. Use a more specific --pattern to narrow results.
167
+ `
168
+ );
169
+ return keys;
170
+ }
158
171
  } while (cursor !== "0");
159
172
  return keys;
160
173
  }
@@ -186,7 +199,18 @@ function summarizeInspectableValue(value) {
186
199
  }
187
200
  return value;
188
201
  }
189
- if (process.argv[1]?.includes("cli.")) {
202
+ function maskRedisUrl(url) {
203
+ try {
204
+ const parsed = new URL(url);
205
+ if (parsed.password) {
206
+ parsed.password = "***";
207
+ }
208
+ return parsed.toString();
209
+ } catch {
210
+ return url.replace(/:([^@/]+)@/, ":***@");
211
+ }
212
+ }
213
+ if (process.argv[1]?.endsWith("cli.cjs") || process.argv[1]?.endsWith("cli.js")) {
190
214
  void main();
191
215
  }
192
216
  export {
@@ -352,6 +352,21 @@ interface CacheStackEvents {
352
352
  warm: {
353
353
  key: string;
354
354
  };
355
+ /** Fired immediately before a high-level cache operation begins. */
356
+ 'operation-start': {
357
+ id: number;
358
+ name: string;
359
+ attributes?: Record<string, unknown>;
360
+ };
361
+ /** Fired after a high-level cache operation finishes. */
362
+ 'operation-end': {
363
+ id: number;
364
+ name: string;
365
+ attributes?: Record<string, unknown>;
366
+ success: boolean;
367
+ result?: 'null';
368
+ error?: unknown;
369
+ };
355
370
  /** Fired when an error occurs (layer failure, circuit breaker, etc.). */
356
371
  error: {
357
372
  operation: string;
@@ -537,11 +552,16 @@ declare class CacheStack extends EventEmitter {
537
552
  private readonly keyDiscovery;
538
553
  private readonly fetchRateLimiter;
539
554
  private readonly snapshotSerializer;
555
+ private readonly invalidation;
556
+ private readonly layerWriter;
557
+ private readonly snapshots;
540
558
  private readonly backgroundRefreshes;
559
+ private readonly backgroundRefreshAbort;
541
560
  private readonly layerDegradedUntil;
542
561
  private readonly maintenance;
543
562
  private readonly ttlResolver;
544
563
  private readonly circuitBreakerManager;
564
+ private nextOperationId;
545
565
  private currentGeneration?;
546
566
  private isDisconnecting;
547
567
  private disconnectPromise?;
@@ -638,8 +658,6 @@ declare class CacheStack extends EventEmitter {
638
658
  private readFromLayers;
639
659
  private readLayerEntry;
640
660
  private backfill;
641
- private writeAcrossLayers;
642
- private executeLayerOperations;
643
661
  private resolveFreshTtl;
644
662
  private resolveLayerSeconds;
645
663
  private shouldNegativeCache;
@@ -654,6 +672,7 @@ declare class CacheStack extends EventEmitter {
654
672
  private sleep;
655
673
  private withTimeout;
656
674
  private shouldBroadcastL1Invalidation;
675
+ private observeOperation;
657
676
  private scheduleGenerationCleanup;
658
677
  private cleanupGeneration;
659
678
  private initializeWriteBehind;
@@ -661,12 +680,9 @@ declare class CacheStack extends EventEmitter {
661
680
  private enqueueWriteBehind;
662
681
  private flushWriteBehindQueue;
663
682
  private runWriteBehindBatch;
664
- private buildLayerSetEntry;
665
- private intersectKeys;
666
683
  private qualifyKey;
667
684
  private qualifyPattern;
668
685
  private stripQualifiedKey;
669
- private deleteKeysFromLayers;
670
686
  private validateConfiguration;
671
687
  private validateWriteOptions;
672
688
  private assertActive;
@@ -679,14 +695,9 @@ declare class CacheStack extends EventEmitter {
679
695
  private recordCircuitFailure;
680
696
  private isNegativeStoredValue;
681
697
  private emitError;
682
- private isCacheSnapshotEntries;
683
- private sanitizeSnapshotValue;
684
698
  private snapshotMaxBytes;
685
699
  private snapshotMaxEntries;
686
700
  private invalidationMaxKeys;
687
- private collectKeysForTag;
688
- private assertWithinInvalidationKeyLimit;
689
- private visitExportEntries;
690
701
  }
691
702
 
692
703
  interface HonoLikeRequest {
@@ -352,6 +352,21 @@ interface CacheStackEvents {
352
352
  warm: {
353
353
  key: string;
354
354
  };
355
+ /** Fired immediately before a high-level cache operation begins. */
356
+ 'operation-start': {
357
+ id: number;
358
+ name: string;
359
+ attributes?: Record<string, unknown>;
360
+ };
361
+ /** Fired after a high-level cache operation finishes. */
362
+ 'operation-end': {
363
+ id: number;
364
+ name: string;
365
+ attributes?: Record<string, unknown>;
366
+ success: boolean;
367
+ result?: 'null';
368
+ error?: unknown;
369
+ };
355
370
  /** Fired when an error occurs (layer failure, circuit breaker, etc.). */
356
371
  error: {
357
372
  operation: string;
@@ -537,11 +552,16 @@ declare class CacheStack extends EventEmitter {
537
552
  private readonly keyDiscovery;
538
553
  private readonly fetchRateLimiter;
539
554
  private readonly snapshotSerializer;
555
+ private readonly invalidation;
556
+ private readonly layerWriter;
557
+ private readonly snapshots;
540
558
  private readonly backgroundRefreshes;
559
+ private readonly backgroundRefreshAbort;
541
560
  private readonly layerDegradedUntil;
542
561
  private readonly maintenance;
543
562
  private readonly ttlResolver;
544
563
  private readonly circuitBreakerManager;
564
+ private nextOperationId;
545
565
  private currentGeneration?;
546
566
  private isDisconnecting;
547
567
  private disconnectPromise?;
@@ -638,8 +658,6 @@ declare class CacheStack extends EventEmitter {
638
658
  private readFromLayers;
639
659
  private readLayerEntry;
640
660
  private backfill;
641
- private writeAcrossLayers;
642
- private executeLayerOperations;
643
661
  private resolveFreshTtl;
644
662
  private resolveLayerSeconds;
645
663
  private shouldNegativeCache;
@@ -654,6 +672,7 @@ declare class CacheStack extends EventEmitter {
654
672
  private sleep;
655
673
  private withTimeout;
656
674
  private shouldBroadcastL1Invalidation;
675
+ private observeOperation;
657
676
  private scheduleGenerationCleanup;
658
677
  private cleanupGeneration;
659
678
  private initializeWriteBehind;
@@ -661,12 +680,9 @@ declare class CacheStack extends EventEmitter {
661
680
  private enqueueWriteBehind;
662
681
  private flushWriteBehindQueue;
663
682
  private runWriteBehindBatch;
664
- private buildLayerSetEntry;
665
- private intersectKeys;
666
683
  private qualifyKey;
667
684
  private qualifyPattern;
668
685
  private stripQualifiedKey;
669
- private deleteKeysFromLayers;
670
686
  private validateConfiguration;
671
687
  private validateWriteOptions;
672
688
  private assertActive;
@@ -679,14 +695,9 @@ declare class CacheStack extends EventEmitter {
679
695
  private recordCircuitFailure;
680
696
  private isNegativeStoredValue;
681
697
  private emitError;
682
- private isCacheSnapshotEntries;
683
- private sanitizeSnapshotValue;
684
698
  private snapshotMaxBytes;
685
699
  private snapshotMaxEntries;
686
700
  private invalidationMaxKeys;
687
- private collectKeysForTag;
688
- private assertWithinInvalidationKeyLimit;
689
- private visitExportEntries;
690
701
  }
691
702
 
692
703
  interface HonoLikeRequest {
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-BMmPVqaD.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-BXWTKlI1.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-BMmPVqaD.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-BXWTKlI1.js';
2
2
  import 'node:events';