layercache 2.1.0 → 3.0.0

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.d.cts CHANGED
@@ -1,8 +1,32 @@
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-BCU8D-Yd.cjs';
2
- export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BCU8D-Yd.cjs';
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-BDyuPmIq.cjs';
2
+ export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BDyuPmIq.cjs';
3
3
  import Redis from 'ioredis';
4
4
  import 'node:events';
5
5
 
6
+ interface RedisGenerationClient {
7
+ get(key: string): Promise<string | null>;
8
+ set(key: string, value: string, mode?: 'NX'): Promise<unknown>;
9
+ incr(key: string): Promise<number>;
10
+ }
11
+ interface RedisGenerationStoreOptions {
12
+ /** Redis client used to persist and atomically bump the generation. */
13
+ client: RedisGenerationClient;
14
+ /** Redis key storing the active generation. Defaults to `layercache:generation`. */
15
+ key?: string;
16
+ }
17
+ declare class RedisGenerationStore {
18
+ private readonly client;
19
+ private readonly key;
20
+ constructor(options: RedisGenerationStoreOptions);
21
+ get(): Promise<number | undefined>;
22
+ getOrInitialize(initialGeneration?: number): Promise<number>;
23
+ set(generation: number): Promise<void>;
24
+ bump(): Promise<number>;
25
+ private parseGeneration;
26
+ private assertGeneration;
27
+ private isGeneration;
28
+ }
29
+
6
30
  interface RedisInvalidationBusOptions {
7
31
  /** Redis client used to publish invalidation messages. */
8
32
  publisher: Redis;
@@ -10,6 +34,11 @@ interface RedisInvalidationBusOptions {
10
34
  subscriber?: Redis;
11
35
  /** Pub/sub channel name. Defaults to `layercache:invalidation`. */
12
36
  channel?: string;
37
+ /**
38
+ * Optional shared secret used to sign and verify invalidation messages.
39
+ * When configured, unsigned or invalidly signed messages are rejected.
40
+ */
41
+ signingSecret?: string | Buffer;
13
42
  /** Optional logger for invalid payloads or subscriber errors. */
14
43
  logger?: CacheLogger;
15
44
  }
@@ -25,6 +54,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
25
54
  private readonly publisher;
26
55
  private readonly subscriber;
27
56
  private readonly logger?;
57
+ private readonly signingKey?;
28
58
  private readonly handlers;
29
59
  private sharedListener?;
30
60
  private subscribePromise;
@@ -39,6 +69,9 @@ declare class RedisInvalidationBus implements InvalidationBus {
39
69
  publish(message: InvalidationMessage): Promise<void>;
40
70
  private dispatchToHandlers;
41
71
  private isInvalidationMessage;
72
+ private signMessage;
73
+ private verifySignedEnvelope;
74
+ private createSignature;
42
75
  private reportError;
43
76
  }
44
77
 
@@ -51,12 +84,19 @@ interface RedisTagIndexOptions {
51
84
  scanCount?: number;
52
85
  /** Number of shards for known-key sets. Defaults to 16. */
53
86
  knownKeysShards?: number;
87
+ /** Optional logger for legacy index warnings. */
88
+ logger?: CacheLogger;
89
+ }
90
+ interface RedisTagIndexMigrationResult {
91
+ migratedKeys: number;
54
92
  }
55
93
  declare class RedisTagIndex implements CacheTagIndex {
56
94
  private readonly client;
57
95
  private readonly prefix;
58
96
  private readonly scanCount;
59
97
  private readonly knownKeysShards;
98
+ private readonly logger?;
99
+ private warnedLegacyKnownKeys;
60
100
  constructor(options: RedisTagIndexOptions);
61
101
  /**
62
102
  * Records a key as known without changing tag assignments.
@@ -102,9 +142,13 @@ declare class RedisTagIndex implements CacheTagIndex {
102
142
  * Clears all Redis tag-index state under this prefix.
103
143
  */
104
144
  clear(): Promise<void>;
145
+ migrateLegacyKnownKeys(): Promise<RedisTagIndexMigrationResult>;
105
146
  private scanIndexKeys;
106
147
  private knownKeysKeyFor;
148
+ private knownKeysKeysForRead;
107
149
  private knownKeysKeys;
150
+ private legacyKnownKeysKey;
151
+ private warnLegacyKnownKeys;
108
152
  private keyTagsKey;
109
153
  private tagKeysKey;
110
154
  }
@@ -746,4 +790,4 @@ declare function createPrometheusMetricsExporter(stacks: CacheStack | Array<{
746
790
  name: string;
747
791
  }>): () => string;
748
792
 
749
- export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
793
+ export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisGenerationStore, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,32 @@
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-BCU8D-Yd.js';
2
- export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BCU8D-Yd.js';
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-BDyuPmIq.js';
2
+ export { k as CacheAdaptiveTtlOptions, l as CacheCircuitBreakerOptions, m as CacheContextOptionsContext, n as CacheDegradationOptions, o as CacheEntryWriteKind, p as CacheEntryWriteOptions, q as CacheFetcher, r as CacheFetcherContext, s as CacheHealthCheckResult, t as CacheHitRateSnapshot, u as CacheInspectResult, v as CacheLayerLatency, w as CacheMGetEntry, x as CacheMSetEntry, y as CacheMetricsSnapshot, z as CacheMissError, A as CacheNamespace, B as CacheRateLimitOptions, D as CacheSnapshotEntry, E as CacheStackEvents, F as CacheStackOptions, G as CacheStatsSnapshot, H as CacheTtlPolicy, J as CacheTtlPolicyContext, K as CacheWarmEntry, L as CacheWarmOptions, M as CacheWarmProgress, N as CacheWriteBehindOptions, O as CacheWriteOptions, P as EvictionPolicy, Q as LayerTtlMap, R as MemoryLayer, S as MemoryLayerOptions, T as MemoryLayerSnapshotEntry, U as PatternMatcher, V as TagIndex, W as createHonoCacheMiddleware } from './edge-BDyuPmIq.js';
3
3
  import Redis from 'ioredis';
4
4
  import 'node:events';
5
5
 
6
+ interface RedisGenerationClient {
7
+ get(key: string): Promise<string | null>;
8
+ set(key: string, value: string, mode?: 'NX'): Promise<unknown>;
9
+ incr(key: string): Promise<number>;
10
+ }
11
+ interface RedisGenerationStoreOptions {
12
+ /** Redis client used to persist and atomically bump the generation. */
13
+ client: RedisGenerationClient;
14
+ /** Redis key storing the active generation. Defaults to `layercache:generation`. */
15
+ key?: string;
16
+ }
17
+ declare class RedisGenerationStore {
18
+ private readonly client;
19
+ private readonly key;
20
+ constructor(options: RedisGenerationStoreOptions);
21
+ get(): Promise<number | undefined>;
22
+ getOrInitialize(initialGeneration?: number): Promise<number>;
23
+ set(generation: number): Promise<void>;
24
+ bump(): Promise<number>;
25
+ private parseGeneration;
26
+ private assertGeneration;
27
+ private isGeneration;
28
+ }
29
+
6
30
  interface RedisInvalidationBusOptions {
7
31
  /** Redis client used to publish invalidation messages. */
8
32
  publisher: Redis;
@@ -10,6 +34,11 @@ interface RedisInvalidationBusOptions {
10
34
  subscriber?: Redis;
11
35
  /** Pub/sub channel name. Defaults to `layercache:invalidation`. */
12
36
  channel?: string;
37
+ /**
38
+ * Optional shared secret used to sign and verify invalidation messages.
39
+ * When configured, unsigned or invalidly signed messages are rejected.
40
+ */
41
+ signingSecret?: string | Buffer;
13
42
  /** Optional logger for invalid payloads or subscriber errors. */
14
43
  logger?: CacheLogger;
15
44
  }
@@ -25,6 +54,7 @@ declare class RedisInvalidationBus implements InvalidationBus {
25
54
  private readonly publisher;
26
55
  private readonly subscriber;
27
56
  private readonly logger?;
57
+ private readonly signingKey?;
28
58
  private readonly handlers;
29
59
  private sharedListener?;
30
60
  private subscribePromise;
@@ -39,6 +69,9 @@ declare class RedisInvalidationBus implements InvalidationBus {
39
69
  publish(message: InvalidationMessage): Promise<void>;
40
70
  private dispatchToHandlers;
41
71
  private isInvalidationMessage;
72
+ private signMessage;
73
+ private verifySignedEnvelope;
74
+ private createSignature;
42
75
  private reportError;
43
76
  }
44
77
 
@@ -51,12 +84,19 @@ interface RedisTagIndexOptions {
51
84
  scanCount?: number;
52
85
  /** Number of shards for known-key sets. Defaults to 16. */
53
86
  knownKeysShards?: number;
87
+ /** Optional logger for legacy index warnings. */
88
+ logger?: CacheLogger;
89
+ }
90
+ interface RedisTagIndexMigrationResult {
91
+ migratedKeys: number;
54
92
  }
55
93
  declare class RedisTagIndex implements CacheTagIndex {
56
94
  private readonly client;
57
95
  private readonly prefix;
58
96
  private readonly scanCount;
59
97
  private readonly knownKeysShards;
98
+ private readonly logger?;
99
+ private warnedLegacyKnownKeys;
60
100
  constructor(options: RedisTagIndexOptions);
61
101
  /**
62
102
  * Records a key as known without changing tag assignments.
@@ -102,9 +142,13 @@ declare class RedisTagIndex implements CacheTagIndex {
102
142
  * Clears all Redis tag-index state under this prefix.
103
143
  */
104
144
  clear(): Promise<void>;
145
+ migrateLegacyKnownKeys(): Promise<RedisTagIndexMigrationResult>;
105
146
  private scanIndexKeys;
106
147
  private knownKeysKeyFor;
148
+ private knownKeysKeysForRead;
107
149
  private knownKeysKeys;
150
+ private legacyKnownKeysKey;
151
+ private warnLegacyKnownKeys;
108
152
  private keyTagsKey;
109
153
  private tagKeysKey;
110
154
  }
@@ -746,4 +790,4 @@ declare function createPrometheusMetricsExporter(stacks: CacheStack | Array<{
746
790
  name: string;
747
791
  }>): () => string;
748
792
 
749
- export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
793
+ export { CacheGetOptions, CacheLayer, CacheLayerSetManyEntry, CacheLogger, CacheSerializer, CacheSingleFlightCoordinator, CacheSingleFlightExecutionOptions, CacheStack, CacheTagIndex, CacheWrapOptions, DiskLayer, InvalidationBus, InvalidationMessage, JsonSerializer, type MemcachedClient, MemcachedLayer, MsgpackSerializer, RedisGenerationStore, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createOpenTelemetryPlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };
package/dist/index.js CHANGED
@@ -12,12 +12,13 @@ import {
12
12
  validateTag,
13
13
  validateTags,
14
14
  validateTtlPolicy
15
- } from "./chunk-6X7NV5BG.js";
15
+ } from "./chunk-NBMG7DHT.js";
16
16
  import {
17
17
  MemoryLayer,
18
18
  TagIndex,
19
- createHonoCacheMiddleware
20
- } from "./chunk-IVX6ABFX.js";
19
+ createHonoCacheMiddleware,
20
+ normalizeHttpCacheUrl
21
+ } from "./chunk-5CIBABDH.js";
21
22
  import {
22
23
  PatternMatcher,
23
24
  createStoredValueEnvelope,
@@ -913,7 +914,9 @@ var CacheStackMaintenance = class {
913
914
  }
914
915
  bumpKeyEpochs(keys) {
915
916
  for (const key of keys) {
916
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
917
+ const nextEpoch = this.currentKeyEpoch(key) + 1;
918
+ this.keyEpochs.delete(key);
919
+ this.keyEpochs.set(key, nextEpoch);
917
920
  }
918
921
  this.pruneKeyEpochsIfNeeded();
919
922
  }
@@ -972,10 +975,13 @@ var CacheStackMaintenance = class {
972
975
  if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
973
976
  return;
974
977
  }
975
- const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
976
- const toDelete = Math.ceil(sorted.length * 0.1);
978
+ const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
977
979
  for (let i = 0; i < toDelete; i++) {
978
- this.keyEpochs.delete(sorted[i][0]);
980
+ const oldestKey = this.keyEpochs.keys().next().value;
981
+ if (oldestKey === void 0) {
982
+ break;
983
+ }
984
+ this.keyEpochs.delete(oldestKey);
979
985
  }
980
986
  }
981
987
  };
@@ -1109,22 +1115,28 @@ var CacheStackReader = class {
1109
1115
  if (upToIndex < 0) {
1110
1116
  return;
1111
1117
  }
1118
+ const operations = [];
1112
1119
  for (let index = 0; index <= upToIndex; index += 1) {
1113
1120
  const layer = this.options.layers[index];
1114
1121
  if (!layer || this.options.shouldSkipLayer(layer)) {
1115
1122
  continue;
1116
1123
  }
1117
1124
  const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
1118
- try {
1119
- await layer.set(key, stored, ttl);
1120
- } catch (error) {
1121
- await this.options.handleLayerFailure(layer, "backfill", error);
1122
- continue;
1123
- }
1124
- this.options.metricsCollector.increment("backfills");
1125
- this.options.logger.debug?.("backfill", { key, layer: layer.name });
1126
- this.options.emit("backfill", { key, layer: layer.name });
1125
+ operations.push(
1126
+ (async () => {
1127
+ try {
1128
+ await layer.set(key, stored, ttl);
1129
+ } catch (error) {
1130
+ await this.options.handleLayerFailure(layer, "backfill", error);
1131
+ return;
1132
+ }
1133
+ this.options.metricsCollector.increment("backfills");
1134
+ this.options.logger.debug?.("backfill", { key, layer: layer.name });
1135
+ this.options.emit("backfill", { key, layer: layer.name });
1136
+ })()
1137
+ );
1127
1138
  }
1139
+ await Promise.all(operations);
1128
1140
  }
1129
1141
  abortAllRefreshes() {
1130
1142
  for (const key of this.backgroundRefreshAbort.keys()) {
@@ -1248,7 +1260,15 @@ var CacheStackReader = class {
1248
1260
  }
1249
1261
  await this.options.sleep(pollIntervalMs);
1250
1262
  }
1251
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1263
+ if (!this.options.singleFlightCoordinator) {
1264
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1265
+ }
1266
+ return this.options.singleFlightCoordinator.execute(
1267
+ key,
1268
+ this.resolveSingleFlightOptions(),
1269
+ () => this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext),
1270
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
1271
+ );
1252
1272
  }
1253
1273
  async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1254
1274
  key,
@@ -2163,6 +2183,7 @@ var TtlResolver = class {
2163
2183
  const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
2164
2184
  profile.hits += 1;
2165
2185
  profile.lastAccessAt = Date.now();
2186
+ this.accessProfiles.delete(key);
2166
2187
  this.accessProfiles.set(key, profile);
2167
2188
  this.pruneIfNeeded();
2168
2189
  }
@@ -2252,12 +2273,12 @@ var TtlResolver = class {
2252
2273
  return;
2253
2274
  }
2254
2275
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
2255
- const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
2256
- for (let i = 0; i < toRemove && i < sorted.length; i++) {
2257
- const entry = sorted[i];
2258
- if (entry) {
2259
- this.accessProfiles.delete(entry[0]);
2276
+ for (let i = 0; i < toRemove; i++) {
2277
+ const oldestKey = this.accessProfiles.keys().next().value;
2278
+ if (oldestKey === void 0) {
2279
+ break;
2260
2280
  }
2281
+ this.accessProfiles.delete(oldestKey);
2261
2282
  }
2262
2283
  }
2263
2284
  };
@@ -3112,6 +3133,12 @@ var CacheStack = class extends EventEmitter {
3112
3133
  }
3113
3134
  return this.currentGeneration;
3114
3135
  }
3136
+ /**
3137
+ * Returns the active generation prefix number used for future cache keys.
3138
+ */
3139
+ getGeneration() {
3140
+ return this.currentGeneration;
3141
+ }
3115
3142
  /**
3116
3143
  * Returns detailed metadata about a single cache key: which layers contain it,
3117
3144
  * remaining fresh/stale/error TTLs, and associated tags.
@@ -3669,12 +3696,65 @@ var CacheStack = class extends EventEmitter {
3669
3696
  }
3670
3697
  };
3671
3698
 
3699
+ // src/generation/RedisGenerationStore.ts
3700
+ var DEFAULT_GENERATION_KEY = "layercache:generation";
3701
+ var RedisGenerationStore = class {
3702
+ client;
3703
+ key;
3704
+ constructor(options) {
3705
+ this.client = options.client;
3706
+ this.key = options.key ?? DEFAULT_GENERATION_KEY;
3707
+ }
3708
+ async get() {
3709
+ const stored = await this.client.get(this.key);
3710
+ if (stored === null) {
3711
+ return void 0;
3712
+ }
3713
+ return this.parseGeneration(stored);
3714
+ }
3715
+ async getOrInitialize(initialGeneration = 0) {
3716
+ this.assertGeneration(initialGeneration);
3717
+ await this.client.set(this.key, String(initialGeneration), "NX");
3718
+ const generation = await this.get();
3719
+ if (generation === void 0) {
3720
+ throw new Error(`RedisGenerationStore failed to initialize generation key "${this.key}".`);
3721
+ }
3722
+ return generation;
3723
+ }
3724
+ async set(generation) {
3725
+ this.assertGeneration(generation);
3726
+ await this.client.set(this.key, String(generation));
3727
+ }
3728
+ async bump() {
3729
+ const generation = await this.client.incr(this.key);
3730
+ this.assertGeneration(generation);
3731
+ return generation;
3732
+ }
3733
+ parseGeneration(value) {
3734
+ const generation = Number.parseInt(value, 10);
3735
+ if (String(generation) !== value || !this.isGeneration(generation)) {
3736
+ throw new Error(`RedisGenerationStore found invalid persisted generation value for key "${this.key}".`);
3737
+ }
3738
+ return generation;
3739
+ }
3740
+ assertGeneration(value) {
3741
+ if (!this.isGeneration(value)) {
3742
+ throw new Error("RedisGenerationStore generation must be a non-negative safe integer.");
3743
+ }
3744
+ }
3745
+ isGeneration(value) {
3746
+ return Number.isSafeInteger(value) && value >= 0;
3747
+ }
3748
+ };
3749
+
3672
3750
  // src/invalidation/RedisInvalidationBus.ts
3751
+ import { createHash, createHmac, timingSafeEqual } from "crypto";
3673
3752
  var RedisInvalidationBus = class {
3674
3753
  channel;
3675
3754
  publisher;
3676
3755
  subscriber;
3677
3756
  logger;
3757
+ signingKey;
3678
3758
  handlers = /* @__PURE__ */ new Set();
3679
3759
  sharedListener;
3680
3760
  subscribePromise;
@@ -3683,6 +3763,7 @@ var RedisInvalidationBus = class {
3683
3763
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
3684
3764
  this.channel = options.channel ?? "layercache:invalidation";
3685
3765
  this.logger = options.logger;
3766
+ this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
3686
3767
  }
3687
3768
  /**
3688
3769
  * Subscribes to invalidation messages and returns an unsubscribe function.
@@ -3722,7 +3803,7 @@ var RedisInvalidationBus = class {
3722
3803
  * Publishes an invalidation message to other subscribers.
3723
3804
  */
3724
3805
  async publish(message) {
3725
- await this.publisher.publish(this.channel, JSON.stringify(message));
3806
+ await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
3726
3807
  }
3727
3808
  async dispatchToHandlers(payload) {
3728
3809
  let message;
@@ -3733,10 +3814,11 @@ var RedisInvalidationBus = class {
3733
3814
  maxNodes: 1e4,
3734
3815
  createObject: () => /* @__PURE__ */ Object.create(null)
3735
3816
  });
3736
- if (!this.isInvalidationMessage(parsed)) {
3817
+ const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
3818
+ if (!this.isInvalidationMessage(candidate)) {
3737
3819
  throw new Error("Invalid invalidation payload shape.");
3738
3820
  }
3739
- message = parsed;
3821
+ message = candidate;
3740
3822
  } catch (error) {
3741
3823
  this.reportError("invalid invalidation payload", error);
3742
3824
  return;
@@ -3761,6 +3843,34 @@ var RedisInvalidationBus = class {
3761
3843
  const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
3762
3844
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
3763
3845
  }
3846
+ signMessage(message) {
3847
+ const payload = JSON.stringify(message);
3848
+ return {
3849
+ payload: message,
3850
+ signature: this.createSignature(payload)
3851
+ };
3852
+ }
3853
+ verifySignedEnvelope(value) {
3854
+ if (!value || typeof value !== "object") {
3855
+ throw new Error("Signed invalidation envelope must be an object.");
3856
+ }
3857
+ const envelope = value;
3858
+ if (!envelope.payload || typeof envelope.payload !== "object" || typeof envelope.signature !== "string") {
3859
+ throw new Error("Signed invalidation envelope is missing payload or signature.");
3860
+ }
3861
+ const payload = JSON.stringify(envelope.payload);
3862
+ const expected = this.createSignature(payload);
3863
+ if (!isEqualSignature(envelope.signature, expected)) {
3864
+ throw new Error("Invalid invalidation message signature.");
3865
+ }
3866
+ return envelope.payload;
3867
+ }
3868
+ createSignature(payload) {
3869
+ if (!this.signingKey) {
3870
+ throw new Error("RedisInvalidationBus signing key is not configured.");
3871
+ }
3872
+ return createHmac("sha256", this.signingKey).update(payload).digest("hex");
3873
+ }
3764
3874
  reportError(message, error) {
3765
3875
  if (this.logger?.error) {
3766
3876
  this.logger.error(message, { error });
@@ -3769,6 +3879,15 @@ var RedisInvalidationBus = class {
3769
3879
  console.error(`[layercache] ${message}`, error);
3770
3880
  }
3771
3881
  };
3882
+ function normalizeSigningSecret(secret) {
3883
+ const raw = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
3884
+ return createHash("sha256").update(raw).digest();
3885
+ }
3886
+ function isEqualSignature(actual, expected) {
3887
+ const actualBuffer = Buffer.from(actual, "hex");
3888
+ const expectedBuffer = Buffer.from(expected, "hex");
3889
+ return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
3890
+ }
3772
3891
 
3773
3892
  // src/http/createCacheStatsHandler.ts
3774
3893
  function createCacheStatsHandler(cache, options = {}) {
@@ -3866,7 +3985,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
3866
3985
  return;
3867
3986
  }
3868
3987
  const rawUrl = req.originalUrl ?? req.url ?? "/";
3869
- const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
3988
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
3870
3989
  const cached = await cache.get(key, void 0, options);
3871
3990
  if (cached !== null) {
3872
3991
  res.setHeader?.("content-type", "application/json; charset=utf-8");
@@ -3882,12 +4001,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
3882
4001
  if (originalJson) {
3883
4002
  res.json = (body) => {
3884
4003
  res.setHeader?.("x-cache", "MISS");
3885
- cache.set(key, body, options).catch((err) => {
3886
- cache.emit("error", {
3887
- operation: "set",
3888
- error: err instanceof Error ? err.message : String(err)
4004
+ if (isSuccessfulStatus(res.statusCode)) {
4005
+ cache.set(key, body, options).catch((err) => {
4006
+ cache.emit("error", {
4007
+ operation: "set",
4008
+ error: err instanceof Error ? err.message : String(err)
4009
+ });
3889
4010
  });
3890
- });
4011
+ }
3891
4012
  return originalJson(body);
3892
4013
  };
3893
4014
  }
@@ -3897,14 +4018,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
3897
4018
  }
3898
4019
  };
3899
4020
  }
3900
- function normalizeUrl(url) {
3901
- try {
3902
- const parsed = new URL(url, "http://localhost");
3903
- parsed.searchParams.sort();
3904
- return parsed.pathname + parsed.search;
3905
- } catch {
3906
- return url;
3907
- }
4021
+ function isSuccessfulStatus(statusCode) {
4022
+ return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
3908
4023
  }
3909
4024
 
3910
4025
  // src/integrations/graphql.ts
@@ -4471,12 +4586,12 @@ var RedisLayer = class {
4471
4586
  };
4472
4587
 
4473
4588
  // src/layers/DiskLayer.ts
4474
- import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
4589
+ import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
4475
4590
  import { promises as fs2 } from "fs";
4476
4591
  import { join, resolve } from "path";
4477
4592
 
4478
4593
  // src/internal/PayloadProtection.ts
4479
- import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
4594
+ import { createCipheriv, createDecipheriv, createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
4480
4595
  var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
4481
4596
  var MAGIC_SIGNED = Buffer.from("LCS1:");
4482
4597
  var ALGORITHM = "aes-256-gcm";
@@ -4489,11 +4604,11 @@ var PayloadProtection = class {
4489
4604
  constructor(options) {
4490
4605
  if (options.encryptionKey) {
4491
4606
  const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
4492
- this.encryptionKey = createHash("sha256").update(raw).digest();
4607
+ this.encryptionKey = createHash2("sha256").update(raw).digest();
4493
4608
  }
4494
4609
  if (options.signingKey && !options.encryptionKey) {
4495
4610
  const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
4496
- this.signingKey = createHash("sha256").update(raw).digest();
4611
+ this.signingKey = createHash2("sha256").update(raw).digest();
4497
4612
  }
4498
4613
  }
4499
4614
  /** Returns `true` when any protection (encryption or signing) is configured. */
@@ -4565,15 +4680,15 @@ var PayloadProtection = class {
4565
4680
  }
4566
4681
  // ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
4567
4682
  sign(payload, key) {
4568
- const hmac = createHmac("sha256", key).update(payload).digest();
4683
+ const hmac = createHmac2("sha256", key).update(payload).digest();
4569
4684
  return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
4570
4685
  }
4571
4686
  verify(payload, key) {
4572
4687
  const headerEnd = MAGIC_SIGNED.length;
4573
4688
  const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
4574
4689
  const data = payload.subarray(headerEnd + HMAC_LENGTH);
4575
- const expectedHmac = createHmac("sha256", key).update(data).digest();
4576
- if (receivedHmac.length !== HMAC_LENGTH || !timingSafeEqual(receivedHmac, expectedHmac)) {
4690
+ const expectedHmac = createHmac2("sha256", key).update(data).digest();
4691
+ if (receivedHmac.length !== HMAC_LENGTH || !timingSafeEqual2(receivedHmac, expectedHmac)) {
4577
4692
  throw new PayloadProtectionError(
4578
4693
  "HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
4579
4694
  );
@@ -4798,7 +4913,7 @@ var DiskLayer = class {
4798
4913
  async dispose() {
4799
4914
  }
4800
4915
  keyToPath(key) {
4801
- const hash = createHash2("sha256").update(key).digest("hex");
4916
+ const hash = createHash3("sha256").update(key).digest("hex");
4802
4917
  return join(this.directory, `${hash}.lc`);
4803
4918
  }
4804
4919
  resolveDirectory(directory) {
@@ -5297,6 +5412,7 @@ export {
5297
5412
  MemoryLayer,
5298
5413
  MsgpackSerializer,
5299
5414
  PatternMatcher,
5415
+ RedisGenerationStore,
5300
5416
  RedisInvalidationBus,
5301
5417
  RedisLayer,
5302
5418
  RedisSingleFlightCoordinator,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layercache",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Production-ready multi-layer caching for Node.js. Stack memory + Redis + disk behind one API with stampede prevention, tag invalidation, stale serving, and full observability.",
5
5
  "keywords": [
6
6
  "cache",