layercache 2.0.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.js CHANGED
@@ -12,12 +12,13 @@ import {
12
12
  validateTag,
13
13
  validateTags,
14
14
  validateTtlPolicy
15
- } from "./chunk-7KMKQ6QZ.js";
15
+ } from "./chunk-NBMG7DHT.js";
16
16
  import {
17
17
  MemoryLayer,
18
18
  TagIndex,
19
- createHonoCacheMiddleware
20
- } from "./chunk-FFZCC7EQ.js";
19
+ createHonoCacheMiddleware,
20
+ normalizeHttpCacheUrl
21
+ } from "./chunk-5CIBABDH.js";
21
22
  import {
22
23
  PatternMatcher,
23
24
  createStoredValueEnvelope,
@@ -157,6 +158,9 @@ function addMetricMap(base, delta) {
157
158
 
158
159
  // src/CacheNamespace.ts
159
160
  var CacheNamespace = class _CacheNamespace {
161
+ /**
162
+ * Creates a namespace backed by an existing cache stack.
163
+ */
160
164
  constructor(cache, prefix) {
161
165
  this.cache = cache;
162
166
  this.prefix = prefix;
@@ -166,9 +170,16 @@ var CacheNamespace = class _CacheNamespace {
166
170
  prefix;
167
171
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
168
172
  metrics = createEmptyNamespaceMetrics();
173
+ /**
174
+ * Reads a key inside this namespace and optionally runs a read-through fetcher
175
+ * on miss or refresh.
176
+ */
169
177
  async get(key, fetcher, options) {
170
178
  return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
171
179
  }
180
+ /**
181
+ * Alias for `get(key, fetcher, options)` that makes the get-or-set behavior explicit.
182
+ */
172
183
  async getOrSet(key, fetcher, options) {
173
184
  return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
174
185
  }
@@ -178,24 +189,69 @@ var CacheNamespace = class _CacheNamespace {
178
189
  async getOrThrow(key, fetcher, options) {
179
190
  return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
180
191
  }
192
+ /**
193
+ * Returns true when the namespaced key exists and has not expired in any layer.
194
+ */
181
195
  async has(key) {
182
196
  return this.trackMetrics(() => this.cache.has(this.qualify(key)));
183
197
  }
198
+ /**
199
+ * Returns the remaining TTL in milliseconds for the namespaced key.
200
+ */
184
201
  async ttl(key) {
185
202
  return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
186
203
  }
204
+ /**
205
+ * Stores a value under a namespaced key.
206
+ */
187
207
  async set(key, value, options) {
188
208
  await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
189
209
  }
210
+ /**
211
+ * Deletes a namespaced key from all layers.
212
+ */
190
213
  async delete(key) {
191
214
  await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
192
215
  }
216
+ /**
217
+ * Deletes multiple namespaced keys from all layers.
218
+ */
193
219
  async mdelete(keys) {
194
220
  await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
195
221
  }
222
+ /**
223
+ * Alias for `delete(key)` scoped to this namespace.
224
+ */
225
+ async invalidateByKey(key) {
226
+ await this.trackMetrics(() => this.cache.invalidateByKey(this.qualify(key)));
227
+ }
228
+ /**
229
+ * Alias for `mdelete(keys)` scoped to this namespace.
230
+ */
231
+ async invalidateByKeys(keys) {
232
+ await this.trackMetrics(() => this.cache.invalidateByKeys(keys.map((k) => this.qualify(k))));
233
+ }
234
+ /**
235
+ * Marks one exact namespaced key expired without deleting its stale value.
236
+ */
237
+ async expireByKey(key) {
238
+ await this.trackMetrics(() => this.cache.expireByKey(this.qualify(key)));
239
+ }
240
+ /**
241
+ * Marks multiple exact namespaced keys expired without deleting their stale values.
242
+ */
243
+ async expireByKeys(keys) {
244
+ await this.trackMetrics(() => this.cache.expireByKeys(keys.map((k) => this.qualify(k))));
245
+ }
246
+ /**
247
+ * Clears all keys in this namespace by invalidating the namespace prefix.
248
+ */
196
249
  async clear() {
197
250
  await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
198
251
  }
252
+ /**
253
+ * Reads many namespaced keys concurrently.
254
+ */
199
255
  async mget(entries) {
200
256
  return this.trackMetrics(
201
257
  () => this.cache.mget(
@@ -207,6 +263,9 @@ var CacheNamespace = class _CacheNamespace {
207
263
  )
208
264
  );
209
265
  }
266
+ /**
267
+ * Writes many namespaced entries concurrently.
268
+ */
210
269
  async mset(entries) {
211
270
  await this.trackMetrics(
212
271
  () => this.cache.mset(
@@ -218,12 +277,21 @@ var CacheNamespace = class _CacheNamespace {
218
277
  )
219
278
  );
220
279
  }
280
+ /**
281
+ * Deletes keys associated with a tag scoped to this namespace.
282
+ */
221
283
  async invalidateByTag(tag) {
222
284
  await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
223
285
  }
286
+ /**
287
+ * Expires keys associated with a tag scoped to this namespace while preserving stale windows.
288
+ */
224
289
  async expireByTag(tag) {
225
290
  await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
226
291
  }
292
+ /**
293
+ * Deletes keys associated with any or all namespace-scoped tags.
294
+ */
227
295
  async invalidateByTags(tags, mode = "any") {
228
296
  await this.trackMetrics(
229
297
  () => this.cache.invalidateByTags(
@@ -232,6 +300,9 @@ var CacheNamespace = class _CacheNamespace {
232
300
  )
233
301
  );
234
302
  }
303
+ /**
304
+ * Expires keys associated with any or all namespace-scoped tags while preserving stale windows.
305
+ */
235
306
  async expireByTags(tags, mode = "any") {
236
307
  await this.trackMetrics(
237
308
  () => this.cache.expireByTags(
@@ -240,15 +311,27 @@ var CacheNamespace = class _CacheNamespace {
240
311
  )
241
312
  );
242
313
  }
314
+ /**
315
+ * Deletes namespaced keys matching a wildcard pattern.
316
+ */
243
317
  async invalidateByPattern(pattern) {
244
318
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
245
319
  }
320
+ /**
321
+ * Expires namespaced keys matching a wildcard pattern while preserving stale windows.
322
+ */
246
323
  async expireByPattern(pattern) {
247
324
  await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
248
325
  }
326
+ /**
327
+ * Deletes namespaced keys with the provided prefix.
328
+ */
249
329
  async invalidateByPrefix(prefix) {
250
330
  await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
251
331
  }
332
+ /**
333
+ * Expires namespaced keys with the provided prefix while preserving stale windows.
334
+ */
252
335
  async expireByPrefix(prefix) {
253
336
  await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
254
337
  }
@@ -265,9 +348,15 @@ var CacheNamespace = class _CacheNamespace {
265
348
  tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
266
349
  };
267
350
  }
351
+ /**
352
+ * Returns a cached wrapper whose generated keys are scoped to this namespace.
353
+ */
268
354
  wrap(keyPrefix, fetcher, options) {
269
355
  return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
270
356
  }
357
+ /**
358
+ * Warms entries after qualifying each key and tag with this namespace prefix.
359
+ */
271
360
  warm(entries, options) {
272
361
  return this.cache.warm(
273
362
  entries.map((entry) => ({
@@ -278,9 +367,15 @@ var CacheNamespace = class _CacheNamespace {
278
367
  options
279
368
  );
280
369
  }
370
+ /**
371
+ * Returns metrics accumulated by operations performed through this namespace.
372
+ */
281
373
  getMetrics() {
282
374
  return cloneNamespaceMetrics(this.metrics);
283
375
  }
376
+ /**
377
+ * Returns hit-rate statistics for operations performed through this namespace.
378
+ */
284
379
  getHitRate() {
285
380
  return computeNamespaceHitRate(this.metrics);
286
381
  }
@@ -297,6 +392,9 @@ var CacheNamespace = class _CacheNamespace {
297
392
  validateNamespaceKey(childPrefix);
298
393
  return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
299
394
  }
395
+ /**
396
+ * Qualifies a raw key with this namespace prefix.
397
+ */
300
398
  qualify(key) {
301
399
  return `${this.prefix}:${key}`;
302
400
  }
@@ -816,7 +914,9 @@ var CacheStackMaintenance = class {
816
914
  }
817
915
  bumpKeyEpochs(keys) {
818
916
  for (const key of keys) {
819
- 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);
820
920
  }
821
921
  this.pruneKeyEpochsIfNeeded();
822
922
  }
@@ -875,10 +975,13 @@ var CacheStackMaintenance = class {
875
975
  if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
876
976
  return;
877
977
  }
878
- const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
879
- const toDelete = Math.ceil(sorted.length * 0.1);
978
+ const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
880
979
  for (let i = 0; i < toDelete; i++) {
881
- 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);
882
985
  }
883
986
  }
884
987
  };
@@ -1012,22 +1115,28 @@ var CacheStackReader = class {
1012
1115
  if (upToIndex < 0) {
1013
1116
  return;
1014
1117
  }
1118
+ const operations = [];
1015
1119
  for (let index = 0; index <= upToIndex; index += 1) {
1016
1120
  const layer = this.options.layers[index];
1017
1121
  if (!layer || this.options.shouldSkipLayer(layer)) {
1018
1122
  continue;
1019
1123
  }
1020
1124
  const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
1021
- try {
1022
- await layer.set(key, stored, ttl);
1023
- } catch (error) {
1024
- await this.options.handleLayerFailure(layer, "backfill", error);
1025
- continue;
1026
- }
1027
- this.options.metricsCollector.increment("backfills");
1028
- this.options.logger.debug?.("backfill", { key, layer: layer.name });
1029
- 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
+ );
1030
1138
  }
1139
+ await Promise.all(operations);
1031
1140
  }
1032
1141
  abortAllRefreshes() {
1033
1142
  for (const key of this.backgroundRefreshAbort.keys()) {
@@ -1151,7 +1260,15 @@ var CacheStackReader = class {
1151
1260
  }
1152
1261
  await this.options.sleep(pollIntervalMs);
1153
1262
  }
1154
- 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
+ );
1155
1272
  }
1156
1273
  async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1157
1274
  key,
@@ -2066,6 +2183,7 @@ var TtlResolver = class {
2066
2183
  const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
2067
2184
  profile.hits += 1;
2068
2185
  profile.lastAccessAt = Date.now();
2186
+ this.accessProfiles.delete(key);
2069
2187
  this.accessProfiles.set(key, profile);
2070
2188
  this.pruneIfNeeded();
2071
2189
  }
@@ -2155,21 +2273,27 @@ var TtlResolver = class {
2155
2273
  return;
2156
2274
  }
2157
2275
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
2158
- const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
2159
- for (let i = 0; i < toRemove && i < sorted.length; i++) {
2160
- const entry = sorted[i];
2161
- if (entry) {
2162
- 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;
2163
2280
  }
2281
+ this.accessProfiles.delete(oldestKey);
2164
2282
  }
2165
2283
  }
2166
2284
  };
2167
2285
 
2168
2286
  // src/serialization/JsonSerializer.ts
2169
2287
  var JsonSerializer = class {
2288
+ /**
2289
+ * Serializes a value to JSON.
2290
+ */
2170
2291
  serialize(value) {
2171
2292
  return JSON.stringify(value);
2172
2293
  }
2294
+ /**
2295
+ * Parses JSON and sanitizes the result before returning it.
2296
+ */
2173
2297
  deserialize(payload) {
2174
2298
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2175
2299
  let parsed;
@@ -2196,6 +2320,9 @@ var StampedeGuard = class {
2196
2320
  this.maxInFlight = options.maxInFlight ?? 1e4;
2197
2321
  this.entryTimeoutMs = options.entryTimeoutMs;
2198
2322
  }
2323
+ /**
2324
+ * Deduplicates concurrent work for the same key in this process.
2325
+ */
2199
2326
  async execute(key, task) {
2200
2327
  const existing = this.inFlight.get(key);
2201
2328
  if (existing) {
@@ -2295,6 +2422,9 @@ var DebugLogger = class {
2295
2422
  }
2296
2423
  };
2297
2424
  var CacheStack = class extends EventEmitter {
2425
+ /**
2426
+ * Creates a cache stack from ordered layers and optional global behavior settings.
2427
+ */
2298
2428
  constructor(layers, options = {}) {
2299
2429
  super();
2300
2430
  this.layers = layers;
@@ -2560,6 +2690,10 @@ var CacheStack = class extends EventEmitter {
2560
2690
  });
2561
2691
  });
2562
2692
  }
2693
+ /**
2694
+ * Clears every configured layer, removes tag metadata, resets internal TTL
2695
+ * profiles, and broadcasts a clear invalidation message.
2696
+ */
2563
2697
  async clear() {
2564
2698
  await this.awaitStartup("clear");
2565
2699
  this.maintenance.beginClearEpoch();
@@ -2589,6 +2723,58 @@ var CacheStack = class extends EventEmitter {
2589
2723
  operation: "delete"
2590
2724
  });
2591
2725
  }
2726
+ /**
2727
+ * Alias for `delete(key)` that matches the `invalidateBy*` API family.
2728
+ */
2729
+ async invalidateByKey(key) {
2730
+ await this.delete(key);
2731
+ }
2732
+ /**
2733
+ * Alias for `mdelete(keys)` that matches the `invalidateBy*` API family.
2734
+ */
2735
+ async invalidateByKeys(keys) {
2736
+ await this.mdelete(keys);
2737
+ }
2738
+ /**
2739
+ * Marks one exact key expired without deleting its stale value.
2740
+ */
2741
+ async expireByKey(key) {
2742
+ await this.observeOperation("layercache.expire_by_key", { "layercache.key": String(key ?? "") }, async () => {
2743
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2744
+ await this.awaitStartup("expireByKey");
2745
+ await this.expireKeys([normalizedKey]);
2746
+ await this.publishInvalidation({
2747
+ scope: "key",
2748
+ keys: [normalizedKey],
2749
+ sourceId: this.instanceId,
2750
+ operation: "expire"
2751
+ });
2752
+ });
2753
+ }
2754
+ /**
2755
+ * Marks multiple exact keys expired without deleting their stale values.
2756
+ */
2757
+ async expireByKeys(keys) {
2758
+ await this.observeOperation("layercache.expire_by_keys", void 0, async () => {
2759
+ if (keys.length === 0) {
2760
+ return;
2761
+ }
2762
+ const normalizedKeys = keys.map((k) => validateCacheKey(k));
2763
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
2764
+ await this.awaitStartup("expireByKeys");
2765
+ await this.expireKeys(cacheKeys);
2766
+ await this.publishInvalidation({
2767
+ scope: "keys",
2768
+ keys: cacheKeys,
2769
+ sourceId: this.instanceId,
2770
+ operation: "expire"
2771
+ });
2772
+ });
2773
+ }
2774
+ /**
2775
+ * Reads many keys concurrently. Simple reads use layer-level bulk fast paths;
2776
+ * entries with fetchers or options fall back to per-entry read-through logic.
2777
+ */
2592
2778
  async mget(entries) {
2593
2779
  return this.observeOperation("layercache.mget", void 0, async () => {
2594
2780
  this.assertActive("mget");
@@ -2676,6 +2862,10 @@ var CacheStack = class extends EventEmitter {
2676
2862
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2677
2863
  });
2678
2864
  }
2865
+ /**
2866
+ * Writes many entries concurrently using each layer's bulk write fast path
2867
+ * when available.
2868
+ */
2679
2869
  async mset(entries) {
2680
2870
  await this.observeOperation("layercache.mset", void 0, async () => {
2681
2871
  this.assertActive("mset");
@@ -2688,6 +2878,10 @@ var CacheStack = class extends EventEmitter {
2688
2878
  await this.writeBatch(normalizedEntries);
2689
2879
  });
2690
2880
  }
2881
+ /**
2882
+ * Pre-populates cache entries by running their fetchers with bounded
2883
+ * concurrency. Higher-priority entries run first.
2884
+ */
2691
2885
  async warm(entries, options = {}) {
2692
2886
  this.assertActive("warm");
2693
2887
  const concurrency = Math.max(1, options.concurrency ?? 4);
@@ -2738,6 +2932,10 @@ var CacheStack = class extends EventEmitter {
2738
2932
  validateNamespaceKey(prefix);
2739
2933
  return new CacheNamespace(this, prefix);
2740
2934
  }
2935
+ /**
2936
+ * Deletes every key currently associated with `tag` and broadcasts an
2937
+ * invalidation message.
2938
+ */
2741
2939
  async invalidateByTag(tag) {
2742
2940
  await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
2743
2941
  validateTag(tag);
@@ -2747,6 +2945,10 @@ var CacheStack = class extends EventEmitter {
2747
2945
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2748
2946
  });
2749
2947
  }
2948
+ /**
2949
+ * Marks every key associated with `tag` as expired while preserving stale
2950
+ * windows for stale serving.
2951
+ */
2750
2952
  async expireByTag(tag) {
2751
2953
  await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
2752
2954
  validateTag(tag);
@@ -2756,6 +2958,10 @@ var CacheStack = class extends EventEmitter {
2756
2958
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2757
2959
  });
2758
2960
  }
2961
+ /**
2962
+ * Deletes keys associated with any or all of the provided tags and broadcasts
2963
+ * an invalidation message.
2964
+ */
2759
2965
  async invalidateByTags(tags, mode = "any") {
2760
2966
  await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2761
2967
  if (tags.length === 0) {
@@ -2772,6 +2978,10 @@ var CacheStack = class extends EventEmitter {
2772
2978
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2773
2979
  });
2774
2980
  }
2981
+ /**
2982
+ * Marks keys associated with any or all of the provided tags as expired while
2983
+ * preserving stale windows for stale serving.
2984
+ */
2775
2985
  async expireByTags(tags, mode = "any") {
2776
2986
  await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
2777
2987
  if (tags.length === 0) {
@@ -2788,6 +2998,9 @@ var CacheStack = class extends EventEmitter {
2788
2998
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2789
2999
  });
2790
3000
  }
3001
+ /**
3002
+ * Deletes keys matching a wildcard pattern such as `user:*`.
3003
+ */
2791
3004
  async invalidateByPattern(pattern) {
2792
3005
  await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
2793
3006
  validatePattern(pattern);
@@ -2800,6 +3013,10 @@ var CacheStack = class extends EventEmitter {
2800
3013
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2801
3014
  });
2802
3015
  }
3016
+ /**
3017
+ * Marks keys matching a wildcard pattern as expired while preserving stale
3018
+ * windows for stale serving.
3019
+ */
2803
3020
  async expireByPattern(pattern) {
2804
3021
  await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
2805
3022
  validatePattern(pattern);
@@ -2812,6 +3029,9 @@ var CacheStack = class extends EventEmitter {
2812
3029
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2813
3030
  });
2814
3031
  }
3032
+ /**
3033
+ * Deletes keys that start with the provided prefix.
3034
+ */
2815
3035
  async invalidateByPrefix(prefix) {
2816
3036
  await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2817
3037
  await this.awaitStartup("invalidateByPrefix");
@@ -2821,6 +3041,10 @@ var CacheStack = class extends EventEmitter {
2821
3041
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2822
3042
  });
2823
3043
  }
3044
+ /**
3045
+ * Marks keys that start with the provided prefix as expired while preserving
3046
+ * stale windows for stale serving.
3047
+ */
2824
3048
  async expireByPrefix(prefix) {
2825
3049
  await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
2826
3050
  await this.awaitStartup("expireByPrefix");
@@ -2830,9 +3054,15 @@ var CacheStack = class extends EventEmitter {
2830
3054
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2831
3055
  });
2832
3056
  }
3057
+ /**
3058
+ * Returns cumulative cache metrics since startup or the last `resetMetrics()`.
3059
+ */
2833
3060
  getMetrics() {
2834
3061
  return this.metricsCollector.snapshot;
2835
3062
  }
3063
+ /**
3064
+ * Returns metrics plus layer degradation state and active background refresh count.
3065
+ */
2836
3066
  getStats() {
2837
3067
  return {
2838
3068
  metrics: this.getMetrics(),
@@ -2844,6 +3074,9 @@ var CacheStack = class extends EventEmitter {
2844
3074
  backgroundRefreshes: this.reader.activeRefreshCount
2845
3075
  };
2846
3076
  }
3077
+ /**
3078
+ * Resets cumulative metrics counters.
3079
+ */
2847
3080
  resetMetrics() {
2848
3081
  this.metricsCollector.reset();
2849
3082
  }
@@ -2853,6 +3086,10 @@ var CacheStack = class extends EventEmitter {
2853
3086
  getHitRate() {
2854
3087
  return this.metricsCollector.hitRate();
2855
3088
  }
3089
+ /**
3090
+ * Runs each layer's `ping()` hook when available and returns per-layer health
3091
+ * and latency information.
3092
+ */
2856
3093
  async healthCheck() {
2857
3094
  await this.startup;
2858
3095
  return Promise.all(
@@ -2896,6 +3133,12 @@ var CacheStack = class extends EventEmitter {
2896
3133
  }
2897
3134
  return this.currentGeneration;
2898
3135
  }
3136
+ /**
3137
+ * Returns the active generation prefix number used for future cache keys.
3138
+ */
3139
+ getGeneration() {
3140
+ return this.currentGeneration;
3141
+ }
2899
3142
  /**
2900
3143
  * Returns detailed metadata about a single cache key: which layers contain it,
2901
3144
  * remaining fresh/stale/error TTLs, and associated tags.
@@ -2937,22 +3180,38 @@ var CacheStack = class extends EventEmitter {
2937
3180
  const tags = await this.getTagsForKey(normalizedKey);
2938
3181
  return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
2939
3182
  }
3183
+ /**
3184
+ * Exports cache entries from configured layers for process-local snapshots.
3185
+ */
2940
3186
  async exportState() {
2941
3187
  await this.awaitStartup("exportState");
2942
3188
  return this.snapshots.exportState(this.snapshotMaxEntries());
2943
3189
  }
3190
+ /**
3191
+ * Imports entries produced by `exportState()` into the configured layers.
3192
+ */
2944
3193
  async importState(entries) {
2945
3194
  await this.awaitStartup("importState");
2946
3195
  await this.snapshots.importState(entries);
2947
3196
  }
3197
+ /**
3198
+ * Writes a snapshot file containing current cache entries.
3199
+ */
2948
3200
  async persistToFile(filePath) {
2949
3201
  this.assertActive("persistToFile");
2950
3202
  await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
2951
3203
  }
3204
+ /**
3205
+ * Restores cache entries from a snapshot file.
3206
+ */
2952
3207
  async restoreFromFile(filePath) {
2953
3208
  this.assertActive("restoreFromFile");
2954
3209
  await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
2955
3210
  }
3211
+ /**
3212
+ * Flushes background work, unsubscribes from buses, disposes timers, and then
3213
+ * disposes each layer that provides `dispose()`.
3214
+ */
2956
3215
  async disconnect() {
2957
3216
  if (!this.disconnectPromise) {
2958
3217
  this.isDisconnecting = true;
@@ -3437,12 +3696,65 @@ var CacheStack = class extends EventEmitter {
3437
3696
  }
3438
3697
  };
3439
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
+
3440
3750
  // src/invalidation/RedisInvalidationBus.ts
3751
+ import { createHash, createHmac, timingSafeEqual } from "crypto";
3441
3752
  var RedisInvalidationBus = class {
3442
3753
  channel;
3443
3754
  publisher;
3444
3755
  subscriber;
3445
3756
  logger;
3757
+ signingKey;
3446
3758
  handlers = /* @__PURE__ */ new Set();
3447
3759
  sharedListener;
3448
3760
  subscribePromise;
@@ -3451,7 +3763,11 @@ var RedisInvalidationBus = class {
3451
3763
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
3452
3764
  this.channel = options.channel ?? "layercache:invalidation";
3453
3765
  this.logger = options.logger;
3766
+ this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
3454
3767
  }
3768
+ /**
3769
+ * Subscribes to invalidation messages and returns an unsubscribe function.
3770
+ */
3455
3771
  async subscribe(handler) {
3456
3772
  const previousPromise = this.subscribePromise;
3457
3773
  let resolveThis;
@@ -3483,8 +3799,11 @@ var RedisInvalidationBus = class {
3483
3799
  }
3484
3800
  };
3485
3801
  }
3802
+ /**
3803
+ * Publishes an invalidation message to other subscribers.
3804
+ */
3486
3805
  async publish(message) {
3487
- await this.publisher.publish(this.channel, JSON.stringify(message));
3806
+ await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
3488
3807
  }
3489
3808
  async dispatchToHandlers(payload) {
3490
3809
  let message;
@@ -3495,10 +3814,11 @@ var RedisInvalidationBus = class {
3495
3814
  maxNodes: 1e4,
3496
3815
  createObject: () => /* @__PURE__ */ Object.create(null)
3497
3816
  });
3498
- if (!this.isInvalidationMessage(parsed)) {
3817
+ const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
3818
+ if (!this.isInvalidationMessage(candidate)) {
3499
3819
  throw new Error("Invalid invalidation payload shape.");
3500
3820
  }
3501
- message = parsed;
3821
+ message = candidate;
3502
3822
  } catch (error) {
3503
3823
  this.reportError("invalid invalidation payload", error);
3504
3824
  return;
@@ -3523,6 +3843,34 @@ var RedisInvalidationBus = class {
3523
3843
  const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
3524
3844
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
3525
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
+ }
3526
3874
  reportError(message, error) {
3527
3875
  if (this.logger?.error) {
3528
3876
  this.logger.error(message, { error });
@@ -3531,6 +3879,15 @@ var RedisInvalidationBus = class {
3531
3879
  console.error(`[layercache] ${message}`, error);
3532
3880
  }
3533
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
+ }
3534
3891
 
3535
3892
  // src/http/createCacheStatsHandler.ts
3536
3893
  function createCacheStatsHandler(cache, options = {}) {
@@ -3628,7 +3985,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
3628
3985
  return;
3629
3986
  }
3630
3987
  const rawUrl = req.originalUrl ?? req.url ?? "/";
3631
- const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
3988
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
3632
3989
  const cached = await cache.get(key, void 0, options);
3633
3990
  if (cached !== null) {
3634
3991
  res.setHeader?.("content-type", "application/json; charset=utf-8");
@@ -3644,12 +4001,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
3644
4001
  if (originalJson) {
3645
4002
  res.json = (body) => {
3646
4003
  res.setHeader?.("x-cache", "MISS");
3647
- cache.set(key, body, options).catch((err) => {
3648
- cache.emit("error", {
3649
- operation: "set",
3650
- 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
+ });
3651
4010
  });
3652
- });
4011
+ }
3653
4012
  return originalJson(body);
3654
4013
  };
3655
4014
  }
@@ -3659,14 +4018,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
3659
4018
  }
3660
4019
  };
3661
4020
  }
3662
- function normalizeUrl(url) {
3663
- try {
3664
- const parsed = new URL(url, "http://localhost");
3665
- parsed.searchParams.sort();
3666
- return parsed.pathname + parsed.search;
3667
- } catch {
3668
- return url;
3669
- }
4021
+ function isSuccessfulStatus(statusCode) {
4022
+ return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
3670
4023
  }
3671
4024
 
3672
4025
  // src/integrations/graphql.ts
@@ -3783,6 +4136,9 @@ var RedisLayer = class {
3783
4136
  decompressionMaxBytes;
3784
4137
  commandTimeoutMs;
3785
4138
  disconnectOnDispose;
4139
+ /**
4140
+ * Creates a Redis cache layer using an existing ioredis client.
4141
+ */
3786
4142
  constructor(options) {
3787
4143
  this.client = options.client;
3788
4144
  this.defaultTtl = options.ttl;
@@ -3797,10 +4153,16 @@ var RedisLayer = class {
3797
4153
  this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
3798
4154
  this.disconnectOnDispose = options.disconnectOnDispose ?? false;
3799
4155
  }
4156
+ /**
4157
+ * Reads and unwraps a fresh value from Redis.
4158
+ */
3800
4159
  async get(key) {
3801
4160
  const payload = await this.getEntry(key);
3802
4161
  return unwrapStoredValue(payload);
3803
4162
  }
4163
+ /**
4164
+ * Reads the raw stored value or envelope from Redis.
4165
+ */
3804
4166
  async getEntry(key) {
3805
4167
  this.validateKey(key);
3806
4168
  const payload = await this.runCommand(
@@ -3812,6 +4174,9 @@ var RedisLayer = class {
3812
4174
  }
3813
4175
  return this.deserializeOrDelete(key, payload);
3814
4176
  }
4177
+ /**
4178
+ * Reads many raw entries from Redis using a pipeline.
4179
+ */
3815
4180
  async getMany(keys) {
3816
4181
  if (keys.length === 0) {
3817
4182
  return [];
@@ -3837,6 +4202,9 @@ var RedisLayer = class {
3837
4202
  })
3838
4203
  );
3839
4204
  }
4205
+ /**
4206
+ * Writes many entries to Redis using a pipeline.
4207
+ */
3840
4208
  async setMany(entries) {
3841
4209
  if (entries.length === 0) {
3842
4210
  return;
@@ -3857,6 +4225,9 @@ var RedisLayer = class {
3857
4225
  }
3858
4226
  await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
3859
4227
  }
4228
+ /**
4229
+ * Stores a value in Redis using the provided TTL or layer default TTL.
4230
+ */
3860
4231
  async set(key, value, ttl = this.defaultTtl) {
3861
4232
  this.validateKey(key);
3862
4233
  const serialized = this.primarySerializer().serialize(value);
@@ -3871,10 +4242,16 @@ var RedisLayer = class {
3871
4242
  }
3872
4243
  await this.runCommand(`set(${this.displayKey(key)})`, () => this.client.set(normalizedKey, payload));
3873
4244
  }
4245
+ /**
4246
+ * Deletes a key from Redis.
4247
+ */
3874
4248
  async delete(key) {
3875
4249
  this.validateKey(key);
3876
4250
  await this.runCommand(`delete(${this.displayKey(key)})`, () => this.client.del(this.withPrefix(key)));
3877
4251
  }
4252
+ /**
4253
+ * Deletes multiple keys from Redis in batches.
4254
+ */
3878
4255
  async deleteMany(keys) {
3879
4256
  if (keys.length === 0) {
3880
4257
  return;
@@ -3887,11 +4264,17 @@ var RedisLayer = class {
3887
4264
  () => this.client.del(...keys.map((key) => this.withPrefix(key)))
3888
4265
  );
3889
4266
  }
4267
+ /**
4268
+ * Returns true when the key exists in Redis.
4269
+ */
3890
4270
  async has(key) {
3891
4271
  this.validateKey(key);
3892
4272
  const exists = await this.runCommand(`has(${this.displayKey(key)})`, () => this.client.exists(this.withPrefix(key)));
3893
4273
  return exists > 0;
3894
4274
  }
4275
+ /**
4276
+ * Returns remaining Redis TTL in milliseconds, or null when absent or non-expiring.
4277
+ */
3895
4278
  async ttl(key) {
3896
4279
  this.validateKey(key);
3897
4280
  const remaining = await this.runCommand(
@@ -3903,6 +4286,9 @@ var RedisLayer = class {
3903
4286
  }
3904
4287
  return remaining;
3905
4288
  }
4289
+ /**
4290
+ * Returns the number of keys under this layer's prefix.
4291
+ */
3906
4292
  async size() {
3907
4293
  if (!this.prefix) {
3908
4294
  return this.runCommand("dbsize()", () => this.client.dbsize());
@@ -3920,6 +4306,9 @@ var RedisLayer = class {
3920
4306
  } while (cursor !== "0");
3921
4307
  return count;
3922
4308
  }
4309
+ /**
4310
+ * Runs a Redis ping command.
4311
+ */
3923
4312
  async ping() {
3924
4313
  try {
3925
4314
  return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
@@ -3927,6 +4316,9 @@ var RedisLayer = class {
3927
4316
  return false;
3928
4317
  }
3929
4318
  }
4319
+ /**
4320
+ * Disconnects the Redis client when `disconnectOnDispose` is enabled.
4321
+ */
3930
4322
  async dispose() {
3931
4323
  if (this.disconnectOnDispose) {
3932
4324
  this.client.disconnect();
@@ -3959,6 +4351,9 @@ var RedisLayer = class {
3959
4351
  }
3960
4352
  } while (cursor !== "0");
3961
4353
  }
4354
+ /**
4355
+ * Returns keys under this layer's prefix without the prefix included.
4356
+ */
3962
4357
  async keys() {
3963
4358
  const pattern = `${this.prefix}*`;
3964
4359
  const keys = await this.scanKeys(pattern);
@@ -3967,6 +4362,9 @@ var RedisLayer = class {
3967
4362
  }
3968
4363
  return keys.map((key) => key.slice(this.prefix.length));
3969
4364
  }
4365
+ /**
4366
+ * Visits keys under this layer's prefix without materializing all results.
4367
+ */
3970
4368
  async forEachKey(visitor) {
3971
4369
  const pattern = `${this.prefix}*`;
3972
4370
  let cursor = "0";
@@ -4188,12 +4586,12 @@ var RedisLayer = class {
4188
4586
  };
4189
4587
 
4190
4588
  // src/layers/DiskLayer.ts
4191
- import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
4589
+ import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
4192
4590
  import { promises as fs2 } from "fs";
4193
4591
  import { join, resolve } from "path";
4194
4592
 
4195
4593
  // src/internal/PayloadProtection.ts
4196
- 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";
4197
4595
  var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
4198
4596
  var MAGIC_SIGNED = Buffer.from("LCS1:");
4199
4597
  var ALGORITHM = "aes-256-gcm";
@@ -4206,11 +4604,11 @@ var PayloadProtection = class {
4206
4604
  constructor(options) {
4207
4605
  if (options.encryptionKey) {
4208
4606
  const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
4209
- this.encryptionKey = createHash("sha256").update(raw).digest();
4607
+ this.encryptionKey = createHash2("sha256").update(raw).digest();
4210
4608
  }
4211
4609
  if (options.signingKey && !options.encryptionKey) {
4212
4610
  const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
4213
- this.signingKey = createHash("sha256").update(raw).digest();
4611
+ this.signingKey = createHash2("sha256").update(raw).digest();
4214
4612
  }
4215
4613
  }
4216
4614
  /** Returns `true` when any protection (encryption or signing) is configured. */
@@ -4282,15 +4680,15 @@ var PayloadProtection = class {
4282
4680
  }
4283
4681
  // ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
4284
4682
  sign(payload, key) {
4285
- const hmac = createHmac("sha256", key).update(payload).digest();
4683
+ const hmac = createHmac2("sha256", key).update(payload).digest();
4286
4684
  return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
4287
4685
  }
4288
4686
  verify(payload, key) {
4289
4687
  const headerEnd = MAGIC_SIGNED.length;
4290
4688
  const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
4291
4689
  const data = payload.subarray(headerEnd + HMAC_LENGTH);
4292
- const expectedHmac = createHmac("sha256", key).update(data).digest();
4293
- 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)) {
4294
4692
  throw new PayloadProtectionError(
4295
4693
  "HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
4296
4694
  );
@@ -4324,6 +4722,9 @@ var DiskLayer = class {
4324
4722
  maxEntryBytes;
4325
4723
  protection;
4326
4724
  writeQueue = Promise.resolve();
4725
+ /**
4726
+ * Creates a disk-backed cache layer.
4727
+ */
4327
4728
  constructor(options) {
4328
4729
  this.directory = this.resolveDirectory(options.directory);
4329
4730
  this.defaultTtl = options.ttl;
@@ -4336,9 +4737,15 @@ var DiskLayer = class {
4336
4737
  signingKey: options.signingKey
4337
4738
  });
4338
4739
  }
4740
+ /**
4741
+ * Reads and unwraps a fresh value from disk.
4742
+ */
4339
4743
  async get(key) {
4340
4744
  return unwrapStoredValue(await this.getEntry(key));
4341
4745
  }
4746
+ /**
4747
+ * Reads the raw stored value or envelope from disk.
4748
+ */
4342
4749
  async getEntry(key) {
4343
4750
  const filePath = this.keyToPath(key);
4344
4751
  const raw = await this.readEntryFile(filePath);
@@ -4358,6 +4765,9 @@ var DiskLayer = class {
4358
4765
  }
4359
4766
  return entry.value;
4360
4767
  }
4768
+ /**
4769
+ * Stores a value on disk using the provided TTL or layer default TTL.
4770
+ */
4361
4771
  async set(key, value, ttl = this.defaultTtl) {
4362
4772
  await this.enqueueWrite(async () => {
4363
4773
  await fs2.mkdir(this.directory, { recursive: true });
@@ -4383,16 +4793,28 @@ var DiskLayer = class {
4383
4793
  }
4384
4794
  });
4385
4795
  }
4796
+ /**
4797
+ * Reads many raw entries from disk.
4798
+ */
4386
4799
  async getMany(keys) {
4387
4800
  return Promise.all(keys.map((key) => this.getEntry(key)));
4388
4801
  }
4802
+ /**
4803
+ * Writes many entries to disk.
4804
+ */
4389
4805
  async setMany(entries) {
4390
4806
  await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
4391
4807
  }
4808
+ /**
4809
+ * Returns true when the key exists and has not expired.
4810
+ */
4392
4811
  async has(key) {
4393
4812
  const value = await this.getEntry(key);
4394
4813
  return value !== null;
4395
4814
  }
4815
+ /**
4816
+ * Returns remaining TTL in milliseconds, or null when absent or non-expiring.
4817
+ */
4396
4818
  async ttl(key) {
4397
4819
  const filePath = this.keyToPath(key);
4398
4820
  const raw = await this.readEntryFile(filePath);
@@ -4415,14 +4837,23 @@ var DiskLayer = class {
4415
4837
  }
4416
4838
  return remaining;
4417
4839
  }
4840
+ /**
4841
+ * Deletes a key from disk.
4842
+ */
4418
4843
  async delete(key) {
4419
4844
  await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
4420
4845
  }
4846
+ /**
4847
+ * Deletes multiple keys from disk.
4848
+ */
4421
4849
  async deleteMany(keys) {
4422
4850
  await this.enqueueWrite(async () => {
4423
4851
  await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
4424
4852
  });
4425
4853
  }
4854
+ /**
4855
+ * Removes all cache entry files from this layer's directory.
4856
+ */
4426
4857
  async clear() {
4427
4858
  await this.enqueueWrite(async () => {
4428
4859
  let entries;
@@ -4447,11 +4878,17 @@ var DiskLayer = class {
4447
4878
  });
4448
4879
  return keys;
4449
4880
  }
4881
+ /**
4882
+ * Visits all non-expired keys stored on disk.
4883
+ */
4450
4884
  async forEachKey(visitor) {
4451
4885
  await this.scanEntries(async (entry) => {
4452
4886
  await visitor(entry.key);
4453
4887
  });
4454
4888
  }
4889
+ /**
4890
+ * Returns the number of non-expired entries stored on disk.
4891
+ */
4455
4892
  async size() {
4456
4893
  let count = 0;
4457
4894
  await this.scanEntries(async () => {
@@ -4459,6 +4896,9 @@ var DiskLayer = class {
4459
4896
  });
4460
4897
  return count;
4461
4898
  }
4899
+ /**
4900
+ * Verifies the cache directory can be created.
4901
+ */
4462
4902
  async ping() {
4463
4903
  try {
4464
4904
  await fs2.mkdir(this.directory, { recursive: true });
@@ -4467,10 +4907,13 @@ var DiskLayer = class {
4467
4907
  return false;
4468
4908
  }
4469
4909
  }
4910
+ /**
4911
+ * Reserved for interface compatibility; DiskLayer does not hold persistent handles.
4912
+ */
4470
4913
  async dispose() {
4471
4914
  }
4472
4915
  keyToPath(key) {
4473
- const hash = createHash2("sha256").update(key).digest("hex");
4916
+ const hash = createHash3("sha256").update(key).digest("hex");
4474
4917
  return join(this.directory, `${hash}.lc`);
4475
4918
  }
4476
4919
  resolveDirectory(directory) {
@@ -4670,6 +5113,9 @@ var MemcachedLayer = class {
4670
5113
  client;
4671
5114
  keyPrefix;
4672
5115
  serializer;
5116
+ /**
5117
+ * Creates a Memcached cache layer using a compatible client.
5118
+ */
4673
5119
  constructor(options) {
4674
5120
  this.client = options.client;
4675
5121
  this.defaultTtl = options.ttl;
@@ -4677,9 +5123,15 @@ var MemcachedLayer = class {
4677
5123
  this.keyPrefix = options.keyPrefix ?? "";
4678
5124
  this.serializer = options.serializer ?? new JsonSerializer();
4679
5125
  }
5126
+ /**
5127
+ * Reads and unwraps a fresh value from Memcached.
5128
+ */
4680
5129
  async get(key) {
4681
5130
  return unwrapStoredValue(await this.getEntry(key));
4682
5131
  }
5132
+ /**
5133
+ * Reads the raw stored value or envelope from Memcached.
5134
+ */
4683
5135
  async getEntry(key) {
4684
5136
  this.validateKey(key);
4685
5137
  const result = await this.client.get(this.withPrefix(key));
@@ -4692,9 +5144,15 @@ var MemcachedLayer = class {
4692
5144
  return null;
4693
5145
  }
4694
5146
  }
5147
+ /**
5148
+ * Reads many raw entries from Memcached.
5149
+ */
4695
5150
  async getMany(keys) {
4696
5151
  return Promise.all(keys.map((key) => this.getEntry(key)));
4697
5152
  }
5153
+ /**
5154
+ * Stores a value in Memcached using the provided TTL or layer default TTL.
5155
+ */
4698
5156
  async set(key, value, ttl = this.defaultTtl) {
4699
5157
  this.validateKey(key);
4700
5158
  const payload = this.serializer.serialize(value);
@@ -4702,18 +5160,30 @@ var MemcachedLayer = class {
4702
5160
  expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
4703
5161
  });
4704
5162
  }
5163
+ /**
5164
+ * Returns true when the key exists in Memcached.
5165
+ */
4705
5166
  async has(key) {
4706
5167
  this.validateKey(key);
4707
5168
  const result = await this.client.get(this.withPrefix(key));
4708
5169
  return result !== null && result.value !== null;
4709
5170
  }
5171
+ /**
5172
+ * Deletes a key from Memcached.
5173
+ */
4710
5174
  async delete(key) {
4711
5175
  this.validateKey(key);
4712
5176
  await this.client.delete(this.withPrefix(key));
4713
5177
  }
5178
+ /**
5179
+ * Deletes multiple keys from Memcached.
5180
+ */
4714
5181
  async deleteMany(keys) {
4715
5182
  await Promise.all(keys.map((key) => this.delete(key)));
4716
5183
  }
5184
+ /**
5185
+ * Always throws because Memcached has no safe prefix clear primitive.
5186
+ */
4717
5187
  async clear() {
4718
5188
  throw new Error(
4719
5189
  "MemcachedLayer.clear() is not supported. Use a key prefix and rotate it to effectively invalidate all keys."
@@ -4741,9 +5211,15 @@ var MemcachedLayer = class {
4741
5211
  // src/serialization/MsgpackSerializer.ts
4742
5212
  import { decode, encode } from "@msgpack/msgpack";
4743
5213
  var MsgpackSerializer = class {
5214
+ /**
5215
+ * Serializes a value to MessagePack bytes.
5216
+ */
4744
5217
  serialize(value) {
4745
5218
  return Buffer.from(encode(value));
4746
5219
  }
5220
+ /**
5221
+ * Decodes MessagePack bytes and sanitizes the result before returning it.
5222
+ */
4747
5223
  deserialize(payload) {
4748
5224
  const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
4749
5225
  return sanitizeStructuredData(decode(normalized), {
@@ -4777,6 +5253,10 @@ var RedisSingleFlightCoordinator = class {
4777
5253
  this.prefix = options.prefix ?? "layercache:singleflight";
4778
5254
  this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
4779
5255
  }
5256
+ /**
5257
+ * Executes `worker` when this process acquires the Redis lock; otherwise runs
5258
+ * `waiter` while another process owns the work.
5259
+ */
4780
5260
  async execute(key, options, worker, waiter) {
4781
5261
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
4782
5262
  const token = randomUUID();
@@ -4932,6 +5412,7 @@ export {
4932
5412
  MemoryLayer,
4933
5413
  MsgpackSerializer,
4934
5414
  PatternMatcher,
5415
+ RedisGenerationStore,
4935
5416
  RedisInvalidationBus,
4936
5417
  RedisLayer,
4937
5418
  RedisSingleFlightCoordinator,