@leejpsd/nextjs-cache-handler 0.2.0 → 0.3.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/README.md +47 -25
  3. package/dist/cache-components/index.cjs +235 -46
  4. package/dist/cache-components/index.cjs.map +1 -1
  5. package/dist/cache-components/index.d.cts +1 -1
  6. package/dist/cache-components/index.d.ts +1 -1
  7. package/dist/cache-components/index.js +227 -46
  8. package/dist/cache-components/index.js.map +1 -1
  9. package/dist/incremental/index.cjs +231 -41
  10. package/dist/incremental/index.cjs.map +1 -1
  11. package/dist/incremental/index.d.cts +42 -3
  12. package/dist/incremental/index.d.ts +42 -3
  13. package/dist/incremental/index.js +223 -41
  14. package/dist/incremental/index.js.map +1 -1
  15. package/dist/index.cjs +313 -70
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +305 -70
  20. package/dist/index.js.map +1 -1
  21. package/dist/ops/index.d.cts +1 -1
  22. package/dist/ops/index.d.ts +1 -1
  23. package/dist/otel/index.cjs +72 -0
  24. package/dist/otel/index.cjs.map +1 -0
  25. package/dist/otel/index.d.cts +72 -0
  26. package/dist/otel/index.d.ts +72 -0
  27. package/dist/otel/index.js +64 -0
  28. package/dist/otel/index.js.map +1 -0
  29. package/dist/shared/client/adapter-ioredis.cjs +49 -10
  30. package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
  31. package/dist/shared/client/adapter-ioredis.d.cts +5 -2
  32. package/dist/shared/client/adapter-ioredis.d.ts +5 -2
  33. package/dist/shared/client/adapter-ioredis.js +43 -11
  34. package/dist/shared/client/adapter-ioredis.js.map +1 -1
  35. package/dist/shared/client/adapter-redis.cjs +22 -7
  36. package/dist/shared/client/adapter-redis.cjs.map +1 -1
  37. package/dist/shared/client/adapter-redis.d.cts +1 -1
  38. package/dist/shared/client/adapter-redis.d.ts +1 -1
  39. package/dist/shared/client/adapter-redis.js +16 -7
  40. package/dist/shared/client/adapter-redis.js.map +1 -1
  41. package/dist/{types-Dq1q6qXd.d.cts → types-Bl-rkLKX.d.cts} +37 -1
  42. package/dist/{types-Dq1q6qXd.d.ts → types-Bl-rkLKX.d.ts} +37 -1
  43. package/package.json +27 -5
package/dist/index.cjs CHANGED
@@ -1,15 +1,17 @@
1
1
  'use strict';
2
2
 
3
+ var module$1 = require('module');
4
+ var path = require('path');
5
+ var util = require('util');
6
+ var zlib = require('zlib');
3
7
  var crypto = require('crypto');
4
8
 
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var path__default = /*#__PURE__*/_interopDefault(path);
12
+
5
13
  var __defProp = Object.defineProperty;
6
14
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
- }) : x)(function(x) {
10
- if (typeof require !== "undefined") return require.apply(this, arguments);
11
- throw Error('Dynamic require of "' + x + '" is not supported');
12
- });
13
15
  var __esm = (fn, res) => function __init() {
14
16
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
17
  };
@@ -18,6 +20,32 @@ var __export = (target, all) => {
18
20
  __defProp(target, name, { get: all[name], enumerable: true });
19
21
  };
20
22
 
23
+ // node_modules/tsup/assets/cjs_shims.js
24
+ var getImportMetaUrl, importMetaUrl;
25
+ var init_cjs_shims = __esm({
26
+ "node_modules/tsup/assets/cjs_shims.js"() {
27
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
28
+ importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
+ }
30
+ });
31
+ function nodeRequire(id) {
32
+ try {
33
+ return packageRequire(id);
34
+ } catch {
35
+ return cwdRequire(id);
36
+ }
37
+ }
38
+ var packageRequire, cwdRequire;
39
+ var init_node_require = __esm({
40
+ "src/shared/node-require.ts"() {
41
+ init_cjs_shims();
42
+ packageRequire = module$1.createRequire(importMetaUrl);
43
+ cwdRequire = module$1.createRequire(
44
+ path__default.default.join(process.cwd(), "__resolve-anchor__.js")
45
+ );
46
+ }
47
+ });
48
+
21
49
  // src/shared/client/adapter-redis.ts
22
50
  var adapter_redis_exports = {};
23
51
  __export(adapter_redis_exports, {
@@ -28,7 +56,7 @@ function adaptRedisV5(client) {
28
56
  return client;
29
57
  }
30
58
  function createRedisV5Client(config) {
31
- const { createClient } = __require("redis");
59
+ const { createClient } = nodeRequire("redis");
32
60
  const useTls = config.tls ?? config.url.startsWith("rediss://");
33
61
  const opts = {
34
62
  url: config.url,
@@ -43,6 +71,8 @@ function createRedisV5Client(config) {
43
71
  }
44
72
  var init_adapter_redis = __esm({
45
73
  "src/shared/client/adapter-redis.ts"() {
74
+ init_cjs_shims();
75
+ init_node_require();
46
76
  }
47
77
  });
48
78
 
@@ -52,7 +82,8 @@ __export(adapter_ioredis_exports, {
52
82
  adaptCluster: () => adaptCluster,
53
83
  adaptIoredis: () => adaptIoredis,
54
84
  createIoredisClient: () => createIoredisClient,
55
- createIoredisCluster: () => createIoredisCluster
85
+ createIoredisCluster: () => createIoredisCluster,
86
+ createIoredisSentinel: () => createIoredisSentinel
56
87
  });
57
88
  function adapt(client, isClusterClient = false) {
58
89
  return {
@@ -135,6 +166,9 @@ function adapt(client, isClusterClient = false) {
135
166
  on: (event, listener) => {
136
167
  client.on(event, listener);
137
168
  return client;
169
+ },
170
+ dispose: () => {
171
+ client.disconnect();
138
172
  }
139
173
  };
140
174
  }
@@ -145,18 +179,36 @@ function adaptCluster(cluster) {
145
179
  return adapt(cluster, true);
146
180
  }
147
181
  function createIoredisClient(config) {
148
- const IORedis = __require("ioredis");
182
+ const IORedis = nodeRequire("ioredis");
149
183
  const Ctor = IORedis.default ?? IORedis;
150
184
  return new Ctor(config.url, {
151
185
  password: config.password,
152
186
  connectTimeout: config.connectTimeout ?? 1500,
153
187
  tls: config.tls ? {} : void 0,
154
188
  lazyConnect: true,
155
- maxRetriesPerRequest: 1
189
+ maxRetriesPerRequest: 1,
190
+ // Batch same-tick commands into one RTT (matches redis@5's default
191
+ // behavior) — makes Promise.all fan-outs a single pipeline.
192
+ enableAutoPipelining: true
193
+ });
194
+ }
195
+ function createIoredisSentinel(config) {
196
+ const IORedis = nodeRequire("ioredis");
197
+ const Ctor = IORedis.default ?? IORedis;
198
+ return new Ctor({
199
+ sentinels: config.sentinels,
200
+ name: config.name,
201
+ password: config.password,
202
+ sentinelPassword: config.sentinelPassword,
203
+ connectTimeout: config.connectTimeout ?? 1500,
204
+ tls: config.tls ? {} : void 0,
205
+ lazyConnect: true,
206
+ maxRetriesPerRequest: 1,
207
+ enableAutoPipelining: true
156
208
  });
157
209
  }
158
210
  function createIoredisCluster(config) {
159
- const IORedis = __require("ioredis");
211
+ const IORedis = nodeRequire("ioredis");
160
212
  const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
161
213
  return new ClusterCtor(config.nodes, {
162
214
  redisOptions: {
@@ -164,15 +216,29 @@ function createIoredisCluster(config) {
164
216
  tls: config.tls ? {} : void 0
165
217
  },
166
218
  enableReadyCheck: true,
167
- lazyConnect: true
219
+ lazyConnect: true,
220
+ // Same-slot commands issued in the same tick share one pipeline.
221
+ enableAutoPipelining: true
168
222
  });
169
223
  }
170
224
  var init_adapter_ioredis = __esm({
171
225
  "src/shared/client/adapter-ioredis.ts"() {
226
+ init_cjs_shims();
227
+ init_node_require();
172
228
  }
173
229
  });
174
230
 
231
+ // src/index.ts
232
+ init_cjs_shims();
233
+
234
+ // src/cache-components/index.ts
235
+ init_cjs_shims();
236
+
237
+ // src/cache-components/handler.ts
238
+ init_cjs_shims();
239
+
175
240
  // src/shared/abort.ts
241
+ init_cjs_shims();
176
242
  var CacheTimeoutError = class extends Error {
177
243
  constructor(opName, ms) {
178
244
  super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
@@ -204,7 +270,41 @@ async function withAbortSignal(opName, ms, fn) {
204
270
  }
205
271
  }
206
272
 
273
+ // src/shared/compress.ts
274
+ init_cjs_shims();
275
+ var gzipAsync = util.promisify(zlib.gzip);
276
+ var gunzipAsync = util.promisify(zlib.gunzip);
277
+ var brotliCompressAsync = util.promisify(zlib.brotliCompress);
278
+ var brotliDecompressAsync = util.promisify(zlib.brotliDecompress);
279
+ var GZIP_MARKER = "__ncgz__:";
280
+ var BROTLI_MARKER = "__ncbr__:";
281
+ var MIN_COMPRESS_BYTES = 1024;
282
+ var BROTLI_OPTS = {
283
+ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 }
284
+ };
285
+ async function compressValue(value, algo) {
286
+ if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
287
+ const input = Buffer.from(value);
288
+ if (algo === "gzip") {
289
+ return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
290
+ }
291
+ return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
292
+ }
293
+ async function decompressValue(value) {
294
+ if (value === null) return null;
295
+ if (value.startsWith(GZIP_MARKER)) {
296
+ const buf = Buffer.from(value.slice(GZIP_MARKER.length), "base64");
297
+ return (await gunzipAsync(buf)).toString("utf8");
298
+ }
299
+ if (value.startsWith(BROTLI_MARKER)) {
300
+ const buf = Buffer.from(value.slice(BROTLI_MARKER.length), "base64");
301
+ return (await brotliDecompressAsync(buf)).toString("utf8");
302
+ }
303
+ return value;
304
+ }
305
+
207
306
  // src/shared/client/index.ts
307
+ init_cjs_shims();
208
308
  async function buildClient(source) {
209
309
  if (typeof source === "function") {
210
310
  return await source();
@@ -222,6 +322,10 @@ async function buildClient(source) {
222
322
  const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
223
323
  return adaptCluster2(createIoredisCluster2(source));
224
324
  }
325
+ case "sentinel": {
326
+ const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
327
+ return adaptIoredis2(createIoredisSentinel2(source));
328
+ }
225
329
  default: {
226
330
  const exhaustive = source;
227
331
  throw new Error(
@@ -231,28 +335,45 @@ async function buildClient(source) {
231
335
  }
232
336
  }
233
337
  var ConnectionManager = class {
234
- constructor(source, onError) {
338
+ constructor(source, onError, retry = {}) {
235
339
  this.source = source;
236
340
  this.onError = onError;
341
+ this.baseCooldownMs = retry.baseCooldownMs ?? 1e3;
342
+ this.maxCooldownMs = retry.maxCooldownMs ?? 3e4;
237
343
  }
238
344
  source;
239
345
  onError;
240
346
  client = null;
241
347
  connecting = null;
242
- failed = false;
348
+ consecutiveFailures = 0;
349
+ lastFailureAt = 0;
350
+ baseCooldownMs;
351
+ maxCooldownMs;
352
+ cooldownMs() {
353
+ if (this.consecutiveFailures === 0) return 0;
354
+ return Math.min(
355
+ this.maxCooldownMs,
356
+ this.baseCooldownMs * 2 ** (this.consecutiveFailures - 1)
357
+ );
358
+ }
243
359
  async getOrConnect() {
244
360
  if (this.client?.isOpen) return this.client;
245
- if (this.failed) return null;
246
361
  if (this.connecting) return this.connecting;
362
+ if (Date.now() - this.lastFailureAt < this.cooldownMs()) return null;
363
+ const stale = this.client;
364
+ this.client = null;
247
365
  this.connecting = (async () => {
366
+ disposeQuietly(stale);
248
367
  try {
249
368
  const c = await buildClient(this.source);
250
369
  c.on("error", (err) => this.onError(err));
251
370
  if (!c.isOpen) await c.connect();
252
371
  this.client = c;
372
+ this.consecutiveFailures = 0;
253
373
  return c;
254
374
  } catch (err) {
255
- this.failed = true;
375
+ this.consecutiveFailures += 1;
376
+ this.lastFailureAt = Date.now();
256
377
  this.onError(err);
257
378
  return null;
258
379
  } finally {
@@ -262,13 +383,24 @@ var ConnectionManager = class {
262
383
  return this.connecting;
263
384
  }
264
385
  reset() {
386
+ disposeQuietly(this.client);
265
387
  this.client = null;
266
388
  this.connecting = null;
267
- this.failed = false;
389
+ this.consecutiveFailures = 0;
390
+ this.lastFailureAt = 0;
268
391
  }
269
392
  };
393
+ function disposeQuietly(client) {
394
+ if (!client) return;
395
+ const c = client;
396
+ try {
397
+ (c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
398
+ } catch {
399
+ }
400
+ }
270
401
 
271
402
  // src/shared/logger.ts
403
+ init_cjs_shims();
272
404
  var defaultLogger = {
273
405
  debug: () => {
274
406
  },
@@ -284,6 +416,9 @@ var defaultLogger = {
284
416
  }
285
417
  };
286
418
 
419
+ // src/shared/lua/index.ts
420
+ init_cjs_shims();
421
+
287
422
  // src/shared/lua/set-with-tags.lua
288
423
  var set_with_tags_default = `-- set-with-tags.lua
289
424
  --
@@ -387,13 +522,19 @@ async function execLuaScript(client, name, keys, args) {
387
522
  }
388
523
 
389
524
  // src/shared/memory-fallback.ts
525
+ init_cjs_shims();
390
526
  var MAX_TIMER_MS = 2147483647;
391
527
  function clampTimerMs(ms) {
392
528
  return Math.min(Math.max(0, ms), MAX_TIMER_MS);
393
529
  }
530
+ var DEFAULT_MAX_ENTRIES = 1e3;
394
531
  var MemoryStore = class {
395
532
  kv = /* @__PURE__ */ new Map();
396
533
  timers = /* @__PURE__ */ new Map();
534
+ maxEntries;
535
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
536
+ this.maxEntries = Math.max(1, maxEntries);
537
+ }
397
538
  set(key, value, ttlSeconds) {
398
539
  this.delete(key);
399
540
  const expiresAt = Date.now() + ttlSeconds * 1e3;
@@ -403,6 +544,10 @@ var MemoryStore = class {
403
544
  timer.unref();
404
545
  }
405
546
  this.timers.set(key, timer);
547
+ while (this.kv.size > this.maxEntries) {
548
+ const oldest = this.kv.keys().next().value;
549
+ this.delete(oldest);
550
+ }
406
551
  }
407
552
  get(key) {
408
553
  const entry = this.kv.get(key);
@@ -411,6 +556,8 @@ var MemoryStore = class {
411
556
  this.delete(key);
412
557
  return null;
413
558
  }
559
+ this.kv.delete(key);
560
+ this.kv.set(key, entry);
414
561
  return entry.value;
415
562
  }
416
563
  delete(key) {
@@ -445,13 +592,19 @@ var MemorySetStore = class {
445
592
  sets = /* @__PURE__ */ new Map();
446
593
  expiresAt = /* @__PURE__ */ new Map();
447
594
  timers = /* @__PURE__ */ new Map();
595
+ maxSets;
596
+ constructor(maxSets = DEFAULT_MAX_ENTRIES) {
597
+ this.maxSets = Math.max(1, maxSets);
598
+ }
448
599
  sAdd(key, member) {
449
600
  const members = Array.isArray(member) ? member : [member];
450
601
  let set = this.sets.get(key);
451
602
  if (!set) {
452
603
  set = /* @__PURE__ */ new Set();
453
- this.sets.set(key, set);
604
+ } else {
605
+ this.sets.delete(key);
454
606
  }
607
+ this.sets.set(key, set);
455
608
  let added = 0;
456
609
  for (const m of members) {
457
610
  if (!set.has(m)) {
@@ -459,6 +612,10 @@ var MemorySetStore = class {
459
612
  added += 1;
460
613
  }
461
614
  }
615
+ while (this.sets.size > this.maxSets) {
616
+ const oldest = this.sets.keys().next().value;
617
+ this.delete(oldest);
618
+ }
462
619
  return added;
463
620
  }
464
621
  sMembers(key) {
@@ -511,6 +668,7 @@ var MemorySetStore = class {
511
668
  };
512
669
 
513
670
  // src/shared/metrics.ts
671
+ init_cjs_shims();
514
672
  var counters = /* @__PURE__ */ new Map();
515
673
  function bump(event) {
516
674
  const existing = counters.get(event.type);
@@ -550,6 +708,7 @@ function getMetricSnapshot() {
550
708
  }
551
709
 
552
710
  // src/shared/namespace.ts
711
+ init_cjs_shims();
553
712
  var FALLBACK = "unversioned";
554
713
  function resolveBuildNamespace(override) {
555
714
  if (override !== void 0) {
@@ -563,6 +722,7 @@ function buildKey(prefix, namespace, rest, hashTag = false) {
563
722
  }
564
723
 
565
724
  // src/shared/build-phase.ts
725
+ init_cjs_shims();
566
726
  var PHASE_PRODUCTION_BUILD = "phase-production-build";
567
727
  function isBuildPhase() {
568
728
  return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
@@ -575,6 +735,7 @@ function shouldUseRedis(opts) {
575
735
  }
576
736
 
577
737
  // src/cache-components/serialize.ts
738
+ init_cjs_shims();
578
739
  async function readStreamFully(stream) {
579
740
  const reader = stream.getReader();
580
741
  const chunks = [];
@@ -617,6 +778,7 @@ function decodeEnvelope(raw) {
617
778
  }
618
779
 
619
780
  // src/cache-components/swr.ts
781
+ init_cjs_shims();
620
782
  var SECOND_MS = 1e3;
621
783
  function partitionEntry(entry, now = Date.now()) {
622
784
  const ageMs = now - entry.timestamp;
@@ -675,8 +837,8 @@ function init(opts) {
675
837
  logger,
676
838
  emit,
677
839
  conn,
678
- memEntries: new MemoryStore(),
679
- memTags: new MemorySetStore(),
840
+ memEntries: new MemoryStore(opts.memoryMaxEntries),
841
+ memTags: new MemorySetStore(opts.memoryMaxEntries),
680
842
  memTagExp: /* @__PURE__ */ new Map(),
681
843
  localTagTimestamps: /* @__PURE__ */ new Map(),
682
844
  resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
@@ -793,6 +955,12 @@ async function getImpl(state, cacheKey, softTags) {
793
955
  state.emit({ type: "cache.miss", ms: Date.now() - start });
794
956
  return void 0;
795
957
  }
958
+ try {
959
+ raw = await decompressValue(raw);
960
+ } catch {
961
+ state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
962
+ return void 0;
963
+ }
796
964
  envelope = decodeEnvelope(raw);
797
965
  if (!envelope) {
798
966
  state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
@@ -921,10 +1089,14 @@ async function setImpl(state, cacheKey, pendingEntry) {
921
1089
  }
922
1090
  return;
923
1091
  }
1092
+ const wirePayload = await compressValue(
1093
+ payload,
1094
+ state.opts.rest.compression
1095
+ );
924
1096
  if (entry.tags.length === 0) {
925
- await client.set(eKey, payload, { EX: ttl });
1097
+ await client.set(eKey, wirePayload, { EX: ttl });
926
1098
  } else {
927
- await luaSetWithTags(client, state, eKey, payload, ttl, entry.tags);
1099
+ await luaSetWithTags(client, state, eKey, wirePayload, ttl, entry.tags);
928
1100
  }
929
1101
  }
930
1102
  );
@@ -960,28 +1132,36 @@ async function refreshTagsImpl(state) {
960
1132
  async () => {
961
1133
  const client = await state.conn.getOrConnect();
962
1134
  if (!client) return;
963
- const pattern = `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}*`;
964
- const seen = [];
1135
+ const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
1136
+ const pattern = `${escapeMatch(
1137
+ `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`
1138
+ )}*`;
1139
+ const scanStartMs = Date.now();
1140
+ const fresh = /* @__PURE__ */ new Map();
965
1141
  for await (const chunk of client.scanIterator({
966
1142
  MATCH: pattern,
967
1143
  COUNT: 100
968
1144
  })) {
969
1145
  const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
970
- seen.push(...keys);
971
- if (seen.length >= 500) break;
1146
+ if (keys.length === 0) continue;
1147
+ const values = await client.mGet(keys);
1148
+ for (let i = 0; i < keys.length; i += 1) {
1149
+ const v = values[i];
1150
+ if (!v) continue;
1151
+ const ts = Number.parseInt(v, 10);
1152
+ if (!Number.isFinite(ts)) continue;
1153
+ const key = keys[i];
1154
+ if (key === void 0) continue;
1155
+ const tag = stripExpPrefix(state, key);
1156
+ if (tag) fresh.set(tag, ts);
1157
+ }
972
1158
  }
973
- if (seen.length === 0) return;
974
- const values = await client.mGet(seen);
975
- for (let i = 0; i < seen.length; i += 1) {
976
- const v = values[i];
977
- if (!v) continue;
978
- const ts = Number.parseInt(v, 10);
979
- if (!Number.isFinite(ts)) continue;
980
- const key = seen[i];
981
- if (key === void 0) continue;
982
- const tag = stripExpPrefix(state, key);
983
- if (tag) state.localTagTimestamps.set(tag, ts);
1159
+ for (const [tag, ts] of state.localTagTimestamps) {
1160
+ if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
1161
+ fresh.set(tag, ts);
1162
+ }
984
1163
  }
1164
+ state.localTagTimestamps = fresh;
985
1165
  }
986
1166
  );
987
1167
  } catch (err) {
@@ -990,6 +1170,19 @@ async function refreshTagsImpl(state) {
990
1170
  });
991
1171
  }
992
1172
  }
1173
+ function escapeMatch(s) {
1174
+ return s.replace(/[\\*?[\]]/g, "\\$&");
1175
+ }
1176
+ var MAX_LOCAL_TAG_ENTRIES = 1e4;
1177
+ function pruneOldestByTimestamp(map, maxEntries) {
1178
+ if (map.size <= maxEntries) return;
1179
+ const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
1180
+ const drop = map.size - maxEntries;
1181
+ for (let i = 0; i < drop; i += 1) {
1182
+ const entry = byAge[i];
1183
+ if (entry) map.delete(entry[0]);
1184
+ }
1185
+ }
993
1186
  function stripExpPrefix(state, fullKey) {
994
1187
  const ns = state.resolveNs();
995
1188
  const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
@@ -1041,6 +1234,7 @@ async function updateTagsImpl(state, tags, durations) {
1041
1234
  state.localTagTimestamps.set(t, now);
1042
1235
  state.memTagExp.set(t, now);
1043
1236
  }
1237
+ pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
1044
1238
  if (!gateRedis(state)) {
1045
1239
  if (isHardExpire) {
1046
1240
  for (const t of tags) {
@@ -1070,20 +1264,18 @@ async function updateTagsImpl(state, tags, durations) {
1070
1264
  }
1071
1265
  return;
1072
1266
  }
1073
- for (const t of tags) {
1074
- if (isHardExpire) {
1075
- await execLuaScript(
1267
+ await Promise.all(
1268
+ tags.map(
1269
+ (t) => isHardExpire ? execLuaScript(
1076
1270
  client,
1077
1271
  "revalidateHard",
1078
1272
  [tagKey(state, t), tagExpKey(state, t)],
1079
1273
  [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1080
- );
1081
- } else {
1082
- await client.set(tagExpKey(state, t), String(now), {
1274
+ ) : client.set(tagExpKey(state, t), String(now), {
1083
1275
  EX: TAG_EXP_MARKER_TTL_SEC
1084
- });
1085
- }
1086
- }
1276
+ })
1277
+ )
1278
+ );
1087
1279
  }
1088
1280
  );
1089
1281
  state.emit({
@@ -1113,7 +1305,14 @@ function createCacheComponentsHandler(opts) {
1113
1305
  };
1114
1306
  }
1115
1307
 
1308
+ // src/incremental/index.ts
1309
+ init_cjs_shims();
1310
+
1311
+ // src/incremental/handler.ts
1312
+ init_cjs_shims();
1313
+
1116
1314
  // src/incremental/serialize.ts
1315
+ init_cjs_shims();
1117
1316
  function isEnvelope(value) {
1118
1317
  return typeof value === "object" && value !== null && "__type" in value && typeof value.__type === "string";
1119
1318
  }
@@ -1195,7 +1394,7 @@ function init2(opts) {
1195
1394
  logger,
1196
1395
  emit,
1197
1396
  conn,
1198
- memEntries: new MemoryStore(),
1397
+ memEntries: new MemoryStore(opts.memoryMaxEntries),
1199
1398
  memTagStates: /* @__PURE__ */ new Map(),
1200
1399
  resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
1201
1400
  };
@@ -1254,7 +1453,7 @@ function normalizeTtlSeconds(data, ctx) {
1254
1453
  return ONE_YEAR_SEC2;
1255
1454
  }
1256
1455
  }
1257
- const revalidate = ctx?.cacheControl?.revalidate;
1456
+ const revalidate = ctx?.cacheControl?.revalidate ?? ctx?.revalidate;
1258
1457
  if (revalidate === false || revalidate === void 0) return ONE_YEAR_SEC2;
1259
1458
  if (typeof revalidate === "number" && revalidate > 0) {
1260
1459
  return Math.min(Math.max(Math.ceil(revalidate), MIN_TTL_SEC), ONE_YEAR_SEC2);
@@ -1292,10 +1491,49 @@ async function readTagStates(state, tags) {
1292
1491
  var IncrementalRedisCacheHandler = class {
1293
1492
  state;
1294
1493
  revalidatedTags;
1494
+ /**
1495
+ * Request-scoped memo of Redis entry reads. Next.js instantiates this class
1496
+ * per request (and calls resetRequestCache() at request boundaries when it
1497
+ * reuses an instance), so entries live at most one request. Bounded by the
1498
+ * number of distinct cache keys a single request touches.
1499
+ */
1500
+ requestMemo = /* @__PURE__ */ new Map();
1295
1501
  constructor(state, ctx) {
1296
1502
  this.state = state;
1297
1503
  this.revalidatedTags = normalizeTags(ctx?.revalidatedTags);
1298
1504
  }
1505
+ /**
1506
+ * Redis entry read, deduplicated per request: parallel RSC renders or
1507
+ * repeated fetch() calls hitting the same cache key share one GET. The
1508
+ * memoized promise never rejects — errors are converted to a miss (with
1509
+ * the same timeout metric the inline path emitted before).
1510
+ */
1511
+ readEntryDeduped(eKey) {
1512
+ const memoized = this.requestMemo.get(eKey);
1513
+ if (memoized) {
1514
+ this.state.emit({ type: "cache.get.deduped" });
1515
+ return memoized;
1516
+ }
1517
+ const read = withAbortSignal(
1518
+ "incremental.get",
1519
+ this.state.opts.abortTimeoutMs,
1520
+ async () => {
1521
+ const client = await this.state.conn.getOrConnect();
1522
+ if (!client) return null;
1523
+ return await decompressValue(await client.get(eKey));
1524
+ }
1525
+ ).catch((err) => {
1526
+ if (err instanceof CacheTimeoutError) {
1527
+ this.state.emit({
1528
+ type: "redis.timeout",
1529
+ meta: { op: "incremental.get" }
1530
+ });
1531
+ }
1532
+ return null;
1533
+ });
1534
+ this.requestMemo.set(eKey, read);
1535
+ return read;
1536
+ }
1299
1537
  async get(cacheKey, ctx) {
1300
1538
  try {
1301
1539
  const requestedTags = normalizeTags([
@@ -1307,24 +1545,11 @@ var IncrementalRedisCacheHandler = class {
1307
1545
  const useRedis = !useLocal && gateRedis2(this.state);
1308
1546
  let raw = null;
1309
1547
  if (useRedis) {
1310
- try {
1311
- raw = await withAbortSignal(
1312
- "incremental.get",
1313
- this.state.opts.abortTimeoutMs,
1314
- async () => {
1315
- const client = await this.state.conn.getOrConnect();
1316
- if (!client) return null;
1317
- return await client.get(eKey);
1318
- }
1319
- );
1320
- } catch (err) {
1321
- if (err instanceof CacheTimeoutError) {
1322
- this.state.emit({ type: "redis.timeout", meta: { op: "incremental.get" } });
1323
- }
1324
- raw = null;
1325
- }
1548
+ raw = await this.readEntryDeduped(eKey);
1549
+ }
1550
+ if (raw === null && (useLocal || this.state.opts.fallback !== "never")) {
1551
+ raw = this.state.memEntries.get(eKey);
1326
1552
  }
1327
- if (raw === null) raw = this.state.memEntries.get(eKey);
1328
1553
  if (raw === null) {
1329
1554
  this.state.emit({ type: "cache.miss" });
1330
1555
  return null;
@@ -1337,7 +1562,7 @@ var IncrementalRedisCacheHandler = class {
1337
1562
  return null;
1338
1563
  }
1339
1564
  const tagStates = await readTagStates(this.state, tags);
1340
- const isFetch = ctx?.kind === "FETCH";
1565
+ const isFetch = ctx?.kind === "FETCH" || ctx?.kindHint?.toLowerCase() === "fetch" || typeof parsed.value === "object" && parsed.value !== null && parsed.value.kind === "FETCH";
1341
1566
  if (tagStates.some((s) => isTagStateExpired(s, parsed.lastModified, isFetch))) {
1342
1567
  this.state.emit({ type: "cache.miss", meta: { reason: "tag-state-expired" } });
1343
1568
  return null;
@@ -1360,6 +1585,7 @@ var IncrementalRedisCacheHandler = class {
1360
1585
  const record = { lastModified: Date.now(), value: data };
1361
1586
  const serialized = serializeCacheRecord(record);
1362
1587
  const eKey = entryKey2(this.state, cacheKey);
1588
+ this.requestMemo.delete(eKey);
1363
1589
  const useRedis = !useLocal && gateRedis2(this.state);
1364
1590
  if (useRedis) {
1365
1591
  try {
@@ -1369,10 +1595,19 @@ var IncrementalRedisCacheHandler = class {
1369
1595
  async () => {
1370
1596
  const client = await this.state.conn.getOrConnect();
1371
1597
  if (!client) {
1372
- this.state.memEntries.set(eKey, serialized, ttl);
1598
+ if (this.state.opts.fallback !== "never") {
1599
+ this.state.memEntries.set(eKey, serialized, ttl);
1600
+ }
1373
1601
  return;
1374
1602
  }
1375
- await client.set(eKey, serialized, { EX: ttl });
1603
+ await client.set(
1604
+ eKey,
1605
+ await compressValue(
1606
+ serialized,
1607
+ this.state.opts.rest.compression
1608
+ ),
1609
+ { EX: ttl }
1610
+ );
1376
1611
  }
1377
1612
  );
1378
1613
  this.state.emit({ type: "cache.set", meta: { backend: "redis" } });
@@ -1383,8 +1618,15 @@ var IncrementalRedisCacheHandler = class {
1383
1618
  }
1384
1619
  }
1385
1620
  }
1386
- this.state.memEntries.set(eKey, serialized, ttl);
1387
- this.state.emit({ type: "cache.set", meta: { backend: "memory" } });
1621
+ if (useLocal || this.state.opts.fallback !== "never") {
1622
+ this.state.memEntries.set(eKey, serialized, ttl);
1623
+ this.state.emit({ type: "cache.set", meta: { backend: "memory" } });
1624
+ } else {
1625
+ this.state.emit({
1626
+ type: "cache.set.failed",
1627
+ meta: { reason: "no-fallback" }
1628
+ });
1629
+ }
1388
1630
  } catch (err) {
1389
1631
  this.state.logger.error("set() error", {
1390
1632
  message: err.message
@@ -1438,6 +1680,7 @@ var IncrementalRedisCacheHandler = class {
1438
1680
  }
1439
1681
  }
1440
1682
  resetRequestCache() {
1683
+ this.requestMemo.clear();
1441
1684
  }
1442
1685
  };
1443
1686
  function createIncrementalCacheHandler(opts) {