@leejpsd/nextjs-cache-handler 0.4.0 → 0.4.2

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 (41) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +46 -15
  3. package/dist/cache-components/index.cjs +59 -37
  4. package/dist/cache-components/index.cjs.map +1 -1
  5. package/dist/cache-components/index.d.cts +2 -2
  6. package/dist/cache-components/index.d.ts +2 -2
  7. package/dist/cache-components/index.js +59 -37
  8. package/dist/cache-components/index.js.map +1 -1
  9. package/dist/cli/index.cjs +4 -3
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +4 -3
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/incremental/index.cjs +4 -3
  14. package/dist/incremental/index.cjs.map +1 -1
  15. package/dist/incremental/index.d.cts +2 -2
  16. package/dist/incremental/index.d.ts +2 -2
  17. package/dist/incremental/index.js +4 -3
  18. package/dist/incremental/index.js.map +1 -1
  19. package/dist/index.cjs +63 -40
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +63 -40
  24. package/dist/index.js.map +1 -1
  25. package/dist/ops/index.d.cts +1 -1
  26. package/dist/ops/index.d.ts +1 -1
  27. package/dist/otel/index.d.cts +1 -1
  28. package/dist/otel/index.d.ts +1 -1
  29. package/dist/seed/index.cjs +4 -3
  30. package/dist/seed/index.cjs.map +1 -1
  31. package/dist/seed/index.d.cts +1 -1
  32. package/dist/seed/index.d.ts +1 -1
  33. package/dist/seed/index.js +4 -3
  34. package/dist/seed/index.js.map +1 -1
  35. package/dist/shared/client/adapter-ioredis.d.cts +1 -1
  36. package/dist/shared/client/adapter-ioredis.d.ts +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/{types-C1vJ_sED.d.cts → types-YbDAllyp.d.cts} +7 -1
  40. package/dist/{types-C1vJ_sED.d.ts → types-YbDAllyp.d.ts} +7 -1
  41. package/package.json +7 -5
package/dist/index.cjs CHANGED
@@ -528,7 +528,7 @@ return 1
528
528
  `;
529
529
 
530
530
  // src/shared/lua/revalidate-hard.lua
531
- var revalidate_hard_default = "-- revalidate-hard.lua\n--\n-- Atomically remove every entry referenced by a tag and stamp the tag's\n-- expiration marker. Used by `updateTags(tags, { expire: 0 })` \u2014 the hard\n-- invalidation path triggered by `updateTag()` server actions in Next.js 16.\n--\n-- KEYS[1] : tag set key (e.g. \"next-cache:tag:posts\")\n-- KEYS[2] : tag-expiration marker key (e.g. \"next-cache:tag-expiration:posts\")\n-- ARGV[1] : current timestamp in milliseconds\n-- ARGV[2] : marker TTL in seconds (default: 1 day so cross-deployment\n-- readers can still observe the invalidation)\n--\n-- Returns: number of entries that were deleted.\n\nlocal tagKey = KEYS[1]\nlocal markerKey = KEYS[2]\nlocal nowMs = ARGV[1]\nlocal markerTtl = tonumber(ARGV[2])\n\nlocal entries = redis.call('SMEMBERS', tagKey)\nlocal deleted = 0\n\nif #entries > 0 then\n -- DEL accepts varargs; unpack the entry list.\n redis.call('DEL', unpack(entries))\n deleted = #entries\nend\n\n-- Drop the tag set itself; on next SET it'll be recreated by set-with-tags.\nredis.call('DEL', tagKey)\n\n-- Stamp the invalidation timestamp. Readers compare this against an entry's\n-- timestamp inside `getExpiration` to decide whether soft-tag-only entries\n-- should be considered stale.\nredis.call('SET', markerKey, nowMs, 'EX', markerTtl)\n\nreturn deleted\n";
531
+ var revalidate_hard_default = "-- revalidate-hard.lua\n--\n-- Atomically remove every entry referenced by a tag and stamp the tag's\n-- expiration marker. Used by the hard invalidation path \u2014 `updateTags(tags)`\n-- with no durations (updateTag(), single-arg revalidateTag()) or an explicit\n-- `{ expire: 0 }`.\n--\n-- KEYS[1] : tag set key (e.g. \"next-cache:tag:posts\")\n-- KEYS[2] : tag-expiration marker key (e.g. \"next-cache:tag-expiration:posts\")\n-- ARGV[1] : marker value \"<staleMs>|<expiredMs>\" (hard: both equal)\n-- ARGV[2] : marker TTL in seconds (default: 1 day so cross-deployment\n-- readers can still observe the invalidation)\n--\n-- Returns: number of entries that were deleted.\n\nlocal tagKey = KEYS[1]\nlocal markerKey = KEYS[2]\nlocal marker = ARGV[1]\nlocal markerTtl = tonumber(ARGV[2])\n\nlocal entries = redis.call('SMEMBERS', tagKey)\nlocal deleted = 0\n\nif #entries > 0 then\n -- DEL accepts varargs; unpack the entry list.\n redis.call('DEL', unpack(entries))\n deleted = #entries\nend\n\n-- Drop the tag set itself; on next SET it'll be recreated by set-with-tags.\nredis.call('DEL', tagKey)\n\n-- Stamp the invalidation timestamp. Readers compare this against an entry's\n-- timestamp inside `getExpiration` to decide whether soft-tag-only entries\n-- should be considered stale.\nredis.call('SET', markerKey, marker, 'EX', markerTtl)\n\nreturn deleted\n";
532
532
 
533
533
  // src/shared/lua/refresh-tag-lock.lua
534
534
  var refresh_tag_lock_default = `-- refresh-tag-lock.lua
@@ -883,6 +883,23 @@ var DEFAULT_LOCK_TTL_SEC = 10;
883
883
  var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
884
884
  var FALLBACK_TTL_SEC = 3600;
885
885
  var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
886
+ function encodeTagMarker(state) {
887
+ return `${state.s}|${state.e === Infinity ? "inf" : state.e}`;
888
+ }
889
+ function decodeTagMarker(raw) {
890
+ const s = Number.parseInt(raw, 10);
891
+ if (!Number.isFinite(s)) return null;
892
+ const sep = raw.indexOf("|");
893
+ if (sep === -1) return { s, e: s };
894
+ const tail = raw.slice(sep + 1);
895
+ if (tail === "inf") return { s, e: Infinity };
896
+ const e = Number.parseInt(tail, 10);
897
+ return { s, e: Number.isFinite(e) ? e : s };
898
+ }
899
+ function mergeTagState(map, tag, next) {
900
+ const prev = map.get(tag);
901
+ if (!prev || next.s > prev.s) map.set(tag, next);
902
+ }
886
903
  function gateRedis(state) {
887
904
  const fb = state.opts.fallback;
888
905
  const ibp = state.opts.rest.isBuildPhase;
@@ -1067,21 +1084,17 @@ async function getImpl(state, cacheKey, softTags) {
1067
1084
  const freshnessTags = softTags.length > 0 ? [...softTags, ...envelope.tags] : envelope.tags;
1068
1085
  let tagStale = false;
1069
1086
  if (freshnessTags.length > 0) {
1070
- let mostRecent = 0;
1087
+ const now = Date.now();
1071
1088
  for (const t of freshnessTags) {
1072
- const ts = Math.max(
1073
- state.localTagTimestamps.get(t) ?? 0,
1074
- state.memTagExp.get(t) ?? 0
1075
- );
1076
- if (ts > mostRecent) mostRecent = ts;
1077
- }
1078
- if (mostRecent > envelope.timestamp) {
1079
- if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
1080
- tagStale = true;
1081
- } else {
1089
+ const local = state.localTagTimestamps.get(t);
1090
+ const mem = state.memTagExp.get(t);
1091
+ const st = local && mem ? local.s >= mem.s ? local : mem : local ?? mem;
1092
+ if (!st || st.s <= envelope.timestamp) continue;
1093
+ if (now >= st.e || partition.freshness === "expired") {
1082
1094
  state.emit({ type: "cache.miss", meta: { reason: "tag-invalidated" } });
1083
1095
  return void 0;
1084
1096
  }
1097
+ tagStale = true;
1085
1098
  }
1086
1099
  }
1087
1100
  const freshness = tagStale ? "stale" : partition.freshness;
@@ -1108,15 +1121,13 @@ async function getImpl(state, cacheKey, softTags) {
1108
1121
  meta: { freshness }
1109
1122
  });
1110
1123
  }
1111
- const revalidateMs = Math.max(0, envelope.revalidate) * 1e3;
1112
- const outTimestamp = tagStale ? Math.min(envelope.timestamp, Date.now() - revalidateMs - 1) : envelope.timestamp;
1113
1124
  return {
1114
1125
  value: bufferToStream(Buffer.from(envelope.value, "base64")),
1115
1126
  tags: envelope.tags,
1116
1127
  stale: envelope.stale,
1117
- timestamp: outTimestamp,
1128
+ timestamp: envelope.timestamp,
1118
1129
  expire: envelope.expire,
1119
- revalidate: envelope.revalidate
1130
+ revalidate: tagStale ? -1 : envelope.revalidate
1120
1131
  };
1121
1132
  }
1122
1133
  async function setImpl(state, cacheKey, pendingEntry) {
@@ -1130,6 +1141,10 @@ async function setImpl(state, cacheKey, pendingEntry) {
1130
1141
  state.emit({ type: "cache.set.failed", meta: { reason: "pending-rejected" } });
1131
1142
  return;
1132
1143
  }
1144
+ if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) {
1145
+ state.emit({ type: "cache.set.skipped", meta: { reason: "dynamic-entry" } });
1146
+ return;
1147
+ }
1133
1148
  let buf;
1134
1149
  try {
1135
1150
  buf = await readStreamFully(entry.value);
@@ -1235,11 +1250,13 @@ async function ensureTagSubscription(state) {
1235
1250
  try {
1236
1251
  const parsed = JSON.parse(message);
1237
1252
  if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
1253
+ const incoming = {
1254
+ s: parsed.ts,
1255
+ e: typeof parsed.e === "number" ? parsed.e : Infinity
1256
+ };
1238
1257
  for (const tag of parsed.t) {
1239
- const prev = state.localTagTimestamps.get(tag) ?? 0;
1240
- if (parsed.ts > prev) state.localTagTimestamps.set(tag, parsed.ts);
1241
- const prevMem = state.memTagExp.get(tag) ?? 0;
1242
- if (parsed.ts > prevMem) state.memTagExp.set(tag, parsed.ts);
1258
+ mergeTagState(state.localTagTimestamps, tag, incoming);
1259
+ mergeTagState(state.memTagExp, tag, incoming);
1243
1260
  }
1244
1261
  pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
1245
1262
  pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
@@ -1291,17 +1308,17 @@ async function refreshTagsImpl(state) {
1291
1308
  for (let i = 0; i < keys.length; i += 1) {
1292
1309
  const v = values[i];
1293
1310
  if (!v) continue;
1294
- const ts = Number.parseInt(v, 10);
1295
- if (!Number.isFinite(ts)) continue;
1311
+ const st = decodeTagMarker(v);
1312
+ if (!st) continue;
1296
1313
  const key = keys[i];
1297
1314
  if (key === void 0) continue;
1298
1315
  const tag = stripExpPrefix(state, key);
1299
- if (tag) fresh.set(tag, ts);
1316
+ if (tag) fresh.set(tag, st);
1300
1317
  }
1301
1318
  }
1302
- for (const [tag, ts] of state.localTagTimestamps) {
1303
- if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
1304
- fresh.set(tag, ts);
1319
+ for (const [tag, st] of state.localTagTimestamps) {
1320
+ if (st.s >= scanStartMs && (fresh.get(tag)?.s ?? 0) < st.s) {
1321
+ fresh.set(tag, st);
1305
1322
  }
1306
1323
  }
1307
1324
  state.localTagTimestamps = fresh;
@@ -1319,7 +1336,7 @@ function escapeMatch(s) {
1319
1336
  var MAX_LOCAL_TAG_ENTRIES = 1e4;
1320
1337
  function pruneOldestByTimestamp(map, maxEntries) {
1321
1338
  if (map.size <= maxEntries) return;
1322
- const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
1339
+ const byAge = [...map.entries()].sort((a, b) => a[1].s - b[1].s);
1323
1340
  const drop = map.size - maxEntries;
1324
1341
  for (let i = 0; i < drop; i += 1) {
1325
1342
  const entry = byAge[i];
@@ -1336,9 +1353,9 @@ async function getExpirationImpl(state, tags) {
1336
1353
  let max = 0;
1337
1354
  for (const t of tags) {
1338
1355
  const local = state.localTagTimestamps.get(t);
1339
- if (local !== void 0 && local > max) max = local;
1356
+ if (local && Number.isFinite(local.e) && local.e > max) max = local.e;
1340
1357
  const mem = state.memTagExp.get(t);
1341
- if (mem !== void 0 && mem > max) max = mem;
1358
+ if (mem && Number.isFinite(mem.e) && mem.e > max) max = mem.e;
1342
1359
  }
1343
1360
  if (max > 0) {
1344
1361
  state.emit({ type: "tag.expiration.read", meta: { source: "local", count: tags.length } });
@@ -1359,8 +1376,8 @@ async function getExpirationImpl(state, tags) {
1359
1376
  let m = 0;
1360
1377
  for (const v of values) {
1361
1378
  if (!v) continue;
1362
- const n = Number.parseInt(v, 10);
1363
- if (Number.isFinite(n) && n > m) m = n;
1379
+ const st = decodeTagMarker(v);
1380
+ if (st && Number.isFinite(st.e) && st.e > m) m = st.e;
1364
1381
  }
1365
1382
  return m;
1366
1383
  }
@@ -1371,11 +1388,14 @@ async function getExpirationImpl(state, tags) {
1371
1388
  }
1372
1389
  async function updateTagsImpl(state, tags, durations) {
1373
1390
  if (tags.length === 0) return;
1374
- const isHardExpire = durations?.expire === 0;
1391
+ const isHardExpire = durations === void 0 || durations.expire === 0;
1375
1392
  const now = Date.now();
1393
+ const deadline = isHardExpire ? now : durations.expire !== void 0 && Number.isFinite(durations.expire) ? now + durations.expire * 1e3 : Infinity;
1394
+ const tagState = { s: now, e: deadline };
1395
+ const marker = encodeTagMarker(tagState);
1376
1396
  for (const t of tags) {
1377
- state.localTagTimestamps.set(t, now);
1378
- state.memTagExp.set(t, now);
1397
+ mergeTagState(state.localTagTimestamps, t, tagState);
1398
+ mergeTagState(state.memTagExp, t, tagState);
1379
1399
  }
1380
1400
  pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
1381
1401
  if (!gateRedis(state)) {
@@ -1413,8 +1433,8 @@ async function updateTagsImpl(state, tags, durations) {
1413
1433
  client,
1414
1434
  "revalidateHard",
1415
1435
  [tagKey(state, t), tagExpKey(state, t)],
1416
- [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1417
- ) : client.set(tagExpKey(state, t), String(now), {
1436
+ [marker, String(TAG_EXP_MARKER_TTL_SEC)]
1437
+ ) : client.set(tagExpKey(state, t), marker, {
1418
1438
  EX: TAG_EXP_MARKER_TTL_SEC
1419
1439
  })
1420
1440
  )
@@ -1430,7 +1450,9 @@ async function updateTagsImpl(state, tags, durations) {
1430
1450
  const client = await state.conn.getOrConnect();
1431
1451
  await client?.publish?.(
1432
1452
  invalChannel(state),
1433
- JSON.stringify({ t: tags, ts: now })
1453
+ JSON.stringify(
1454
+ deadline === Infinity ? { t: tags, ts: now } : { t: tags, ts: now, e: deadline }
1455
+ )
1434
1456
  );
1435
1457
  } catch {
1436
1458
  }
@@ -1472,11 +1494,12 @@ function isEnvelope(value) {
1472
1494
  function isLegacyBufferShape(value) {
1473
1495
  return typeof value === "object" && value !== null && value.type === "Buffer" && Array.isArray(value.data);
1474
1496
  }
1475
- function replacer(_key, value) {
1476
- if (Buffer.isBuffer(value)) {
1497
+ function replacer(key, value) {
1498
+ const raw = this[key];
1499
+ if (Buffer.isBuffer(raw)) {
1477
1500
  const env = {
1478
1501
  __type: "Buffer",
1479
- data: value.toString("base64")
1502
+ data: raw.toString("base64")
1480
1503
  };
1481
1504
  return env;
1482
1505
  }