@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.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { CacheComponentsHandler, createCacheComponentsHandler, partitionEntry, shouldServeStale } from './cache-components/index.cjs';
2
2
  export { IncrementalCtx, IncrementalRedisCacheHandler, createIncrementalCacheHandler } from './incremental/index.cjs';
3
3
  export { getMetricSnapshot } from './ops/index.cjs';
4
- export { C as CacheComponentsEntry, c as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, d as MetricEvent, e as MetricEventType, a as RedisClientConfig, b as RedisClientFactory, R as RedisClientLike } from './types-C1vJ_sED.cjs';
4
+ export { C as CacheComponentsEntry, c as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, d as MetricEvent, e as MetricEventType, a as RedisClientConfig, b as RedisClientFactory, R as RedisClientLike } from './types-YbDAllyp.cjs';
5
5
 
6
6
  /**
7
7
  * Timeout-bounded operation wrapper. Surrounds every Redis call so a hung
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { CacheComponentsHandler, createCacheComponentsHandler, partitionEntry, shouldServeStale } from './cache-components/index.js';
2
2
  export { IncrementalCtx, IncrementalRedisCacheHandler, createIncrementalCacheHandler } from './incremental/index.js';
3
3
  export { getMetricSnapshot } from './ops/index.js';
4
- export { C as CacheComponentsEntry, c as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, d as MetricEvent, e as MetricEventType, a as RedisClientConfig, b as RedisClientFactory, R as RedisClientLike } from './types-C1vJ_sED.js';
4
+ export { C as CacheComponentsEntry, c as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, d as MetricEvent, e as MetricEventType, a as RedisClientConfig, b as RedisClientFactory, R as RedisClientLike } from './types-YbDAllyp.js';
5
5
 
6
6
  /**
7
7
  * Timeout-bounded operation wrapper. Surrounds every Redis call so a hung
package/dist/index.js CHANGED
@@ -518,7 +518,7 @@ return 1
518
518
  `;
519
519
 
520
520
  // src/shared/lua/revalidate-hard.lua
521
- 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";
521
+ 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";
522
522
 
523
523
  // src/shared/lua/refresh-tag-lock.lua
524
524
  var refresh_tag_lock_default = `-- refresh-tag-lock.lua
@@ -873,6 +873,23 @@ var DEFAULT_LOCK_TTL_SEC = 10;
873
873
  var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
874
874
  var FALLBACK_TTL_SEC = 3600;
875
875
  var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
876
+ function encodeTagMarker(state) {
877
+ return `${state.s}|${state.e === Infinity ? "inf" : state.e}`;
878
+ }
879
+ function decodeTagMarker(raw) {
880
+ const s = Number.parseInt(raw, 10);
881
+ if (!Number.isFinite(s)) return null;
882
+ const sep = raw.indexOf("|");
883
+ if (sep === -1) return { s, e: s };
884
+ const tail = raw.slice(sep + 1);
885
+ if (tail === "inf") return { s, e: Infinity };
886
+ const e = Number.parseInt(tail, 10);
887
+ return { s, e: Number.isFinite(e) ? e : s };
888
+ }
889
+ function mergeTagState(map, tag, next) {
890
+ const prev = map.get(tag);
891
+ if (!prev || next.s > prev.s) map.set(tag, next);
892
+ }
876
893
  function gateRedis(state) {
877
894
  const fb = state.opts.fallback;
878
895
  const ibp = state.opts.rest.isBuildPhase;
@@ -1057,21 +1074,17 @@ async function getImpl(state, cacheKey, softTags) {
1057
1074
  const freshnessTags = softTags.length > 0 ? [...softTags, ...envelope.tags] : envelope.tags;
1058
1075
  let tagStale = false;
1059
1076
  if (freshnessTags.length > 0) {
1060
- let mostRecent = 0;
1077
+ const now = Date.now();
1061
1078
  for (const t of freshnessTags) {
1062
- const ts = Math.max(
1063
- state.localTagTimestamps.get(t) ?? 0,
1064
- state.memTagExp.get(t) ?? 0
1065
- );
1066
- if (ts > mostRecent) mostRecent = ts;
1067
- }
1068
- if (mostRecent > envelope.timestamp) {
1069
- if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
1070
- tagStale = true;
1071
- } else {
1079
+ const local = state.localTagTimestamps.get(t);
1080
+ const mem = state.memTagExp.get(t);
1081
+ const st = local && mem ? local.s >= mem.s ? local : mem : local ?? mem;
1082
+ if (!st || st.s <= envelope.timestamp) continue;
1083
+ if (now >= st.e || partition.freshness === "expired") {
1072
1084
  state.emit({ type: "cache.miss", meta: { reason: "tag-invalidated" } });
1073
1085
  return void 0;
1074
1086
  }
1087
+ tagStale = true;
1075
1088
  }
1076
1089
  }
1077
1090
  const freshness = tagStale ? "stale" : partition.freshness;
@@ -1098,15 +1111,13 @@ async function getImpl(state, cacheKey, softTags) {
1098
1111
  meta: { freshness }
1099
1112
  });
1100
1113
  }
1101
- const revalidateMs = Math.max(0, envelope.revalidate) * 1e3;
1102
- const outTimestamp = tagStale ? Math.min(envelope.timestamp, Date.now() - revalidateMs - 1) : envelope.timestamp;
1103
1114
  return {
1104
1115
  value: bufferToStream(Buffer.from(envelope.value, "base64")),
1105
1116
  tags: envelope.tags,
1106
1117
  stale: envelope.stale,
1107
- timestamp: outTimestamp,
1118
+ timestamp: envelope.timestamp,
1108
1119
  expire: envelope.expire,
1109
- revalidate: envelope.revalidate
1120
+ revalidate: tagStale ? -1 : envelope.revalidate
1110
1121
  };
1111
1122
  }
1112
1123
  async function setImpl(state, cacheKey, pendingEntry) {
@@ -1120,6 +1131,10 @@ async function setImpl(state, cacheKey, pendingEntry) {
1120
1131
  state.emit({ type: "cache.set.failed", meta: { reason: "pending-rejected" } });
1121
1132
  return;
1122
1133
  }
1134
+ if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) {
1135
+ state.emit({ type: "cache.set.skipped", meta: { reason: "dynamic-entry" } });
1136
+ return;
1137
+ }
1123
1138
  let buf;
1124
1139
  try {
1125
1140
  buf = await readStreamFully(entry.value);
@@ -1225,11 +1240,13 @@ async function ensureTagSubscription(state) {
1225
1240
  try {
1226
1241
  const parsed = JSON.parse(message);
1227
1242
  if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
1243
+ const incoming = {
1244
+ s: parsed.ts,
1245
+ e: typeof parsed.e === "number" ? parsed.e : Infinity
1246
+ };
1228
1247
  for (const tag of parsed.t) {
1229
- const prev = state.localTagTimestamps.get(tag) ?? 0;
1230
- if (parsed.ts > prev) state.localTagTimestamps.set(tag, parsed.ts);
1231
- const prevMem = state.memTagExp.get(tag) ?? 0;
1232
- if (parsed.ts > prevMem) state.memTagExp.set(tag, parsed.ts);
1248
+ mergeTagState(state.localTagTimestamps, tag, incoming);
1249
+ mergeTagState(state.memTagExp, tag, incoming);
1233
1250
  }
1234
1251
  pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
1235
1252
  pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
@@ -1281,17 +1298,17 @@ async function refreshTagsImpl(state) {
1281
1298
  for (let i = 0; i < keys.length; i += 1) {
1282
1299
  const v = values[i];
1283
1300
  if (!v) continue;
1284
- const ts = Number.parseInt(v, 10);
1285
- if (!Number.isFinite(ts)) continue;
1301
+ const st = decodeTagMarker(v);
1302
+ if (!st) continue;
1286
1303
  const key = keys[i];
1287
1304
  if (key === void 0) continue;
1288
1305
  const tag = stripExpPrefix(state, key);
1289
- if (tag) fresh.set(tag, ts);
1306
+ if (tag) fresh.set(tag, st);
1290
1307
  }
1291
1308
  }
1292
- for (const [tag, ts] of state.localTagTimestamps) {
1293
- if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
1294
- fresh.set(tag, ts);
1309
+ for (const [tag, st] of state.localTagTimestamps) {
1310
+ if (st.s >= scanStartMs && (fresh.get(tag)?.s ?? 0) < st.s) {
1311
+ fresh.set(tag, st);
1295
1312
  }
1296
1313
  }
1297
1314
  state.localTagTimestamps = fresh;
@@ -1309,7 +1326,7 @@ function escapeMatch(s) {
1309
1326
  var MAX_LOCAL_TAG_ENTRIES = 1e4;
1310
1327
  function pruneOldestByTimestamp(map, maxEntries) {
1311
1328
  if (map.size <= maxEntries) return;
1312
- const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
1329
+ const byAge = [...map.entries()].sort((a, b) => a[1].s - b[1].s);
1313
1330
  const drop = map.size - maxEntries;
1314
1331
  for (let i = 0; i < drop; i += 1) {
1315
1332
  const entry = byAge[i];
@@ -1326,9 +1343,9 @@ async function getExpirationImpl(state, tags) {
1326
1343
  let max = 0;
1327
1344
  for (const t of tags) {
1328
1345
  const local = state.localTagTimestamps.get(t);
1329
- if (local !== void 0 && local > max) max = local;
1346
+ if (local && Number.isFinite(local.e) && local.e > max) max = local.e;
1330
1347
  const mem = state.memTagExp.get(t);
1331
- if (mem !== void 0 && mem > max) max = mem;
1348
+ if (mem && Number.isFinite(mem.e) && mem.e > max) max = mem.e;
1332
1349
  }
1333
1350
  if (max > 0) {
1334
1351
  state.emit({ type: "tag.expiration.read", meta: { source: "local", count: tags.length } });
@@ -1349,8 +1366,8 @@ async function getExpirationImpl(state, tags) {
1349
1366
  let m = 0;
1350
1367
  for (const v of values) {
1351
1368
  if (!v) continue;
1352
- const n = Number.parseInt(v, 10);
1353
- if (Number.isFinite(n) && n > m) m = n;
1369
+ const st = decodeTagMarker(v);
1370
+ if (st && Number.isFinite(st.e) && st.e > m) m = st.e;
1354
1371
  }
1355
1372
  return m;
1356
1373
  }
@@ -1361,11 +1378,14 @@ async function getExpirationImpl(state, tags) {
1361
1378
  }
1362
1379
  async function updateTagsImpl(state, tags, durations) {
1363
1380
  if (tags.length === 0) return;
1364
- const isHardExpire = durations?.expire === 0;
1381
+ const isHardExpire = durations === void 0 || durations.expire === 0;
1365
1382
  const now = Date.now();
1383
+ const deadline = isHardExpire ? now : durations.expire !== void 0 && Number.isFinite(durations.expire) ? now + durations.expire * 1e3 : Infinity;
1384
+ const tagState = { s: now, e: deadline };
1385
+ const marker = encodeTagMarker(tagState);
1366
1386
  for (const t of tags) {
1367
- state.localTagTimestamps.set(t, now);
1368
- state.memTagExp.set(t, now);
1387
+ mergeTagState(state.localTagTimestamps, t, tagState);
1388
+ mergeTagState(state.memTagExp, t, tagState);
1369
1389
  }
1370
1390
  pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
1371
1391
  if (!gateRedis(state)) {
@@ -1403,8 +1423,8 @@ async function updateTagsImpl(state, tags, durations) {
1403
1423
  client,
1404
1424
  "revalidateHard",
1405
1425
  [tagKey(state, t), tagExpKey(state, t)],
1406
- [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1407
- ) : client.set(tagExpKey(state, t), String(now), {
1426
+ [marker, String(TAG_EXP_MARKER_TTL_SEC)]
1427
+ ) : client.set(tagExpKey(state, t), marker, {
1408
1428
  EX: TAG_EXP_MARKER_TTL_SEC
1409
1429
  })
1410
1430
  )
@@ -1420,7 +1440,9 @@ async function updateTagsImpl(state, tags, durations) {
1420
1440
  const client = await state.conn.getOrConnect();
1421
1441
  await client?.publish?.(
1422
1442
  invalChannel(state),
1423
- JSON.stringify({ t: tags, ts: now })
1443
+ JSON.stringify(
1444
+ deadline === Infinity ? { t: tags, ts: now } : { t: tags, ts: now, e: deadline }
1445
+ )
1424
1446
  );
1425
1447
  } catch {
1426
1448
  }