@leejpsd/nextjs-cache-handler 0.4.1 → 1.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.
@@ -1,4 +1,4 @@
1
- import { C as CacheComponentsEntry, c as CacheHandlerOptions } from '../types-C1vJ_sED.cjs';
1
+ import { C as CacheComponentsEntry, c as CacheHandlerOptions } from '../types-YbDAllyp.cjs';
2
2
 
3
3
  /**
4
4
  * Spec-compliant `cacheHandlers` (plural) implementation. See docs/next16-spec.md.
@@ -22,7 +22,7 @@ interface CacheComponentsHandler {
22
22
  refreshTags(): Promise<void>;
23
23
  getExpiration(tags: string[]): Promise<number>;
24
24
  updateTags(tags: string[], durations?: {
25
- expire?: number;
25
+ expire?: number | undefined;
26
26
  }): Promise<void>;
27
27
  }
28
28
  declare function createCacheComponentsHandler(opts: CacheHandlerOptions): CacheComponentsHandler;
@@ -1,4 +1,4 @@
1
- import { C as CacheComponentsEntry, c as CacheHandlerOptions } from '../types-C1vJ_sED.js';
1
+ import { C as CacheComponentsEntry, c as CacheHandlerOptions } from '../types-YbDAllyp.js';
2
2
 
3
3
  /**
4
4
  * Spec-compliant `cacheHandlers` (plural) implementation. See docs/next16-spec.md.
@@ -22,7 +22,7 @@ interface CacheComponentsHandler {
22
22
  refreshTags(): Promise<void>;
23
23
  getExpiration(tags: string[]): Promise<number>;
24
24
  updateTags(tags: string[], durations?: {
25
- expire?: number;
25
+ expire?: number | undefined;
26
26
  }): Promise<void>;
27
27
  }
28
28
  declare function createCacheComponentsHandler(opts: CacheHandlerOptions): CacheComponentsHandler;
@@ -515,7 +515,7 @@ return 1
515
515
  `;
516
516
 
517
517
  // src/shared/lua/revalidate-hard.lua
518
- 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";
518
+ 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";
519
519
 
520
520
  // src/shared/lua/refresh-tag-lock.lua
521
521
  var refresh_tag_lock_default = `-- refresh-tag-lock.lua
@@ -859,6 +859,23 @@ var DEFAULT_LOCK_TTL_SEC = 10;
859
859
  var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
860
860
  var FALLBACK_TTL_SEC = 3600;
861
861
  var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
862
+ function encodeTagMarker(state) {
863
+ return `${state.s}|${state.e === Infinity ? "inf" : state.e}`;
864
+ }
865
+ function decodeTagMarker(raw) {
866
+ const s = Number.parseInt(raw, 10);
867
+ if (!Number.isFinite(s)) return null;
868
+ const sep = raw.indexOf("|");
869
+ if (sep === -1) return { s, e: s };
870
+ const tail = raw.slice(sep + 1);
871
+ if (tail === "inf") return { s, e: Infinity };
872
+ const e = Number.parseInt(tail, 10);
873
+ return { s, e: Number.isFinite(e) ? e : s };
874
+ }
875
+ function mergeTagState(map, tag, next) {
876
+ const prev = map.get(tag);
877
+ if (!prev || next.s > prev.s) map.set(tag, next);
878
+ }
862
879
  function gateRedis(state) {
863
880
  const fb = state.opts.fallback;
864
881
  const ibp = state.opts.rest.isBuildPhase;
@@ -1043,21 +1060,17 @@ async function getImpl(state, cacheKey, softTags) {
1043
1060
  const freshnessTags = softTags.length > 0 ? [...softTags, ...envelope.tags] : envelope.tags;
1044
1061
  let tagStale = false;
1045
1062
  if (freshnessTags.length > 0) {
1046
- let mostRecent = 0;
1063
+ const now = Date.now();
1047
1064
  for (const t of freshnessTags) {
1048
- const ts = Math.max(
1049
- state.localTagTimestamps.get(t) ?? 0,
1050
- state.memTagExp.get(t) ?? 0
1051
- );
1052
- if (ts > mostRecent) mostRecent = ts;
1053
- }
1054
- if (mostRecent > envelope.timestamp) {
1055
- if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
1056
- tagStale = true;
1057
- } else {
1065
+ const local = state.localTagTimestamps.get(t);
1066
+ const mem = state.memTagExp.get(t);
1067
+ const st = local && mem ? local.s >= mem.s ? local : mem : local ?? mem;
1068
+ if (!st || st.s <= envelope.timestamp) continue;
1069
+ if (now >= st.e || partition.freshness === "expired") {
1058
1070
  state.emit({ type: "cache.miss", meta: { reason: "tag-invalidated" } });
1059
1071
  return void 0;
1060
1072
  }
1073
+ tagStale = true;
1061
1074
  }
1062
1075
  }
1063
1076
  const freshness = tagStale ? "stale" : partition.freshness;
@@ -1084,15 +1097,13 @@ async function getImpl(state, cacheKey, softTags) {
1084
1097
  meta: { freshness }
1085
1098
  });
1086
1099
  }
1087
- const revalidateMs = Math.max(0, envelope.revalidate) * 1e3;
1088
- const outTimestamp = tagStale ? Math.min(envelope.timestamp, Date.now() - revalidateMs - 1) : envelope.timestamp;
1089
1100
  return {
1090
1101
  value: bufferToStream(Buffer.from(envelope.value, "base64")),
1091
1102
  tags: envelope.tags,
1092
1103
  stale: envelope.stale,
1093
- timestamp: outTimestamp,
1104
+ timestamp: envelope.timestamp,
1094
1105
  expire: envelope.expire,
1095
- revalidate: envelope.revalidate
1106
+ revalidate: tagStale ? -1 : envelope.revalidate
1096
1107
  };
1097
1108
  }
1098
1109
  async function setImpl(state, cacheKey, pendingEntry) {
@@ -1106,6 +1117,10 @@ async function setImpl(state, cacheKey, pendingEntry) {
1106
1117
  state.emit({ type: "cache.set.failed", meta: { reason: "pending-rejected" } });
1107
1118
  return;
1108
1119
  }
1120
+ if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) {
1121
+ state.emit({ type: "cache.set.skipped", meta: { reason: "dynamic-entry" } });
1122
+ return;
1123
+ }
1109
1124
  let buf;
1110
1125
  try {
1111
1126
  buf = await readStreamFully(entry.value);
@@ -1211,11 +1226,13 @@ async function ensureTagSubscription(state) {
1211
1226
  try {
1212
1227
  const parsed = JSON.parse(message);
1213
1228
  if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
1229
+ const incoming = {
1230
+ s: parsed.ts,
1231
+ e: typeof parsed.e === "number" ? parsed.e : Infinity
1232
+ };
1214
1233
  for (const tag of parsed.t) {
1215
- const prev = state.localTagTimestamps.get(tag) ?? 0;
1216
- if (parsed.ts > prev) state.localTagTimestamps.set(tag, parsed.ts);
1217
- const prevMem = state.memTagExp.get(tag) ?? 0;
1218
- if (parsed.ts > prevMem) state.memTagExp.set(tag, parsed.ts);
1234
+ mergeTagState(state.localTagTimestamps, tag, incoming);
1235
+ mergeTagState(state.memTagExp, tag, incoming);
1219
1236
  }
1220
1237
  pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
1221
1238
  pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
@@ -1267,17 +1284,17 @@ async function refreshTagsImpl(state) {
1267
1284
  for (let i = 0; i < keys.length; i += 1) {
1268
1285
  const v = values[i];
1269
1286
  if (!v) continue;
1270
- const ts = Number.parseInt(v, 10);
1271
- if (!Number.isFinite(ts)) continue;
1287
+ const st = decodeTagMarker(v);
1288
+ if (!st) continue;
1272
1289
  const key = keys[i];
1273
1290
  if (key === void 0) continue;
1274
1291
  const tag = stripExpPrefix(state, key);
1275
- if (tag) fresh.set(tag, ts);
1292
+ if (tag) fresh.set(tag, st);
1276
1293
  }
1277
1294
  }
1278
- for (const [tag, ts] of state.localTagTimestamps) {
1279
- if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
1280
- fresh.set(tag, ts);
1295
+ for (const [tag, st] of state.localTagTimestamps) {
1296
+ if (st.s >= scanStartMs && (fresh.get(tag)?.s ?? 0) < st.s) {
1297
+ fresh.set(tag, st);
1281
1298
  }
1282
1299
  }
1283
1300
  state.localTagTimestamps = fresh;
@@ -1295,7 +1312,7 @@ function escapeMatch(s) {
1295
1312
  var MAX_LOCAL_TAG_ENTRIES = 1e4;
1296
1313
  function pruneOldestByTimestamp(map, maxEntries) {
1297
1314
  if (map.size <= maxEntries) return;
1298
- const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
1315
+ const byAge = [...map.entries()].sort((a, b) => a[1].s - b[1].s);
1299
1316
  const drop = map.size - maxEntries;
1300
1317
  for (let i = 0; i < drop; i += 1) {
1301
1318
  const entry = byAge[i];
@@ -1312,9 +1329,9 @@ async function getExpirationImpl(state, tags) {
1312
1329
  let max = 0;
1313
1330
  for (const t of tags) {
1314
1331
  const local = state.localTagTimestamps.get(t);
1315
- if (local !== void 0 && local > max) max = local;
1332
+ if (local && Number.isFinite(local.e) && local.e > max) max = local.e;
1316
1333
  const mem = state.memTagExp.get(t);
1317
- if (mem !== void 0 && mem > max) max = mem;
1334
+ if (mem && Number.isFinite(mem.e) && mem.e > max) max = mem.e;
1318
1335
  }
1319
1336
  if (max > 0) {
1320
1337
  state.emit({ type: "tag.expiration.read", meta: { source: "local", count: tags.length } });
@@ -1335,8 +1352,8 @@ async function getExpirationImpl(state, tags) {
1335
1352
  let m = 0;
1336
1353
  for (const v of values) {
1337
1354
  if (!v) continue;
1338
- const n = Number.parseInt(v, 10);
1339
- if (Number.isFinite(n) && n > m) m = n;
1355
+ const st = decodeTagMarker(v);
1356
+ if (st && Number.isFinite(st.e) && st.e > m) m = st.e;
1340
1357
  }
1341
1358
  return m;
1342
1359
  }
@@ -1347,11 +1364,14 @@ async function getExpirationImpl(state, tags) {
1347
1364
  }
1348
1365
  async function updateTagsImpl(state, tags, durations) {
1349
1366
  if (tags.length === 0) return;
1350
- const isHardExpire = durations?.expire === 0;
1367
+ const isHardExpire = durations === void 0 || durations.expire === 0;
1351
1368
  const now = Date.now();
1369
+ const deadline = isHardExpire ? now : durations.expire !== void 0 && Number.isFinite(durations.expire) ? now + durations.expire * 1e3 : Infinity;
1370
+ const tagState = { s: now, e: deadline };
1371
+ const marker = encodeTagMarker(tagState);
1352
1372
  for (const t of tags) {
1353
- state.localTagTimestamps.set(t, now);
1354
- state.memTagExp.set(t, now);
1373
+ mergeTagState(state.localTagTimestamps, t, tagState);
1374
+ mergeTagState(state.memTagExp, t, tagState);
1355
1375
  }
1356
1376
  pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
1357
1377
  if (!gateRedis(state)) {
@@ -1389,8 +1409,8 @@ async function updateTagsImpl(state, tags, durations) {
1389
1409
  client,
1390
1410
  "revalidateHard",
1391
1411
  [tagKey(state, t), tagExpKey(state, t)],
1392
- [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1393
- ) : client.set(tagExpKey(state, t), String(now), {
1412
+ [marker, String(TAG_EXP_MARKER_TTL_SEC)]
1413
+ ) : client.set(tagExpKey(state, t), marker, {
1394
1414
  EX: TAG_EXP_MARKER_TTL_SEC
1395
1415
  })
1396
1416
  )
@@ -1406,7 +1426,9 @@ async function updateTagsImpl(state, tags, durations) {
1406
1426
  const client = await state.conn.getOrConnect();
1407
1427
  await client?.publish?.(
1408
1428
  invalChannel(state),
1409
- JSON.stringify({ t: tags, ts: now })
1429
+ JSON.stringify(
1430
+ deadline === Infinity ? { t: tags, ts: now } : { t: tags, ts: now, e: deadline }
1431
+ )
1410
1432
  );
1411
1433
  } catch {
1412
1434
  }