@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/CHANGELOG.md +32 -0
- package/README.md +44 -15
- package/dist/cache-components/index.cjs +59 -37
- package/dist/cache-components/index.cjs.map +1 -1
- package/dist/cache-components/index.d.cts +2 -2
- package/dist/cache-components/index.d.ts +2 -2
- package/dist/cache-components/index.js +59 -37
- package/dist/cache-components/index.js.map +1 -1
- package/dist/incremental/index.cjs.map +1 -1
- package/dist/incremental/index.d.cts +2 -2
- package/dist/incremental/index.d.ts +2 -2
- package/dist/incremental/index.js.map +1 -1
- package/dist/index.cjs +59 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -37
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.cts +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/otel/index.d.cts +1 -1
- package/dist/otel/index.d.ts +1 -1
- package/dist/seed/index.d.cts +1 -1
- package/dist/seed/index.d.ts +1 -1
- package/dist/shared/client/adapter-ioredis.d.cts +1 -1
- package/dist/shared/client/adapter-ioredis.d.ts +1 -1
- package/dist/shared/client/adapter-redis.d.cts +1 -1
- package/dist/shared/client/adapter-redis.d.ts +1 -1
- package/dist/{types-C1vJ_sED.d.cts → types-YbDAllyp.d.cts} +7 -1
- package/dist/{types-C1vJ_sED.d.ts → types-YbDAllyp.d.ts} +7 -1
- package/package.json +7 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CacheComponentsEntry, c as CacheHandlerOptions } from '../types-
|
|
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-
|
|
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
|
|
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
|
-
|
|
1063
|
+
const now = Date.now();
|
|
1047
1064
|
for (const t of freshnessTags) {
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
);
|
|
1052
|
-
if (
|
|
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:
|
|
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
|
-
|
|
1216
|
-
|
|
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
|
|
1271
|
-
if (!
|
|
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,
|
|
1292
|
+
if (tag) fresh.set(tag, st);
|
|
1276
1293
|
}
|
|
1277
1294
|
}
|
|
1278
|
-
for (const [tag,
|
|
1279
|
-
if (
|
|
1280
|
-
fresh.set(tag,
|
|
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
|
|
1332
|
+
if (local && Number.isFinite(local.e) && local.e > max) max = local.e;
|
|
1316
1333
|
const mem = state.memTagExp.get(t);
|
|
1317
|
-
if (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
|
|
1339
|
-
if (Number.isFinite(
|
|
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
|
|
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
|
|
1354
|
-
state.memTagExp
|
|
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
|
-
[
|
|
1393
|
-
) : client.set(tagExpKey(state, t),
|
|
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(
|
|
1429
|
+
JSON.stringify(
|
|
1430
|
+
deadline === Infinity ? { t: tags, ts: now } : { t: tags, ts: now, e: deadline }
|
|
1431
|
+
)
|
|
1410
1432
|
);
|
|
1411
1433
|
} catch {
|
|
1412
1434
|
}
|