@leejpsd/nextjs-cache-handler 0.3.3 → 0.4.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/AGENTS.md +33 -0
- package/CHANGELOG.md +43 -0
- package/README.md +22 -3
- package/bin/cli.cjs +10 -0
- package/dist/cache-components/index.cjs +150 -4
- package/dist/cache-components/index.cjs.map +1 -1
- package/dist/cache-components/index.d.cts +1 -1
- package/dist/cache-components/index.d.ts +1 -1
- package/dist/cache-components/index.js +150 -4
- package/dist/cache-components/index.js.map +1 -1
- package/dist/cli/index.cjs +937 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +28 -0
- package/dist/cli/index.d.ts +28 -0
- package/dist/cli/index.js +927 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/incremental/index.cjs +77 -3
- package/dist/incremental/index.cjs.map +1 -1
- package/dist/incremental/index.d.cts +1 -1
- package/dist/incremental/index.d.ts +1 -1
- package/dist/incremental/index.js +77 -3
- package/dist/incremental/index.js.map +1 -1
- package/dist/index.cjs +153 -5
- 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 +153 -5
- 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.cjs +560 -0
- package/dist/seed/index.cjs.map +1 -0
- package/dist/seed/index.d.cts +53 -0
- package/dist/seed/index.d.ts +53 -0
- package/dist/seed/index.js +549 -0
- package/dist/seed/index.js.map +1 -0
- package/dist/shared/client/adapter-ioredis.cjs +40 -1
- package/dist/shared/client/adapter-ioredis.cjs.map +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-ioredis.js +40 -1
- package/dist/shared/client/adapter-ioredis.js.map +1 -1
- package/dist/shared/client/adapter-redis.cjs +34 -1
- package/dist/shared/client/adapter-redis.cjs.map +1 -1
- package/dist/shared/client/adapter-redis.d.cts +1 -1
- package/dist/shared/client/adapter-redis.d.ts +1 -1
- package/dist/shared/client/adapter-redis.js +34 -1
- package/dist/shared/client/adapter-redis.js.map +1 -1
- package/dist/{types-Bl-rkLKX.d.cts → types-C1vJ_sED.d.cts} +25 -1
- package/dist/{types-Bl-rkLKX.d.ts → types-C1vJ_sED.d.ts} +25 -1
- package/llms.txt +27 -0
- package/package.json +27 -2
- package/rules/nextjs-cache-rules.md +23 -0
- package/setup-instructions/setup.md +179 -0
- package/skills/nextjs-redis-cache/SKILL.md +182 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CacheComponentsEntry,
|
|
1
|
+
import { C as CacheComponentsEntry, c as CacheHandlerOptions } from '../types-C1vJ_sED.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Spec-compliant `cacheHandlers` (plural) implementation. See docs/next16-spec.md.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CacheComponentsEntry,
|
|
1
|
+
import { C as CacheComponentsEntry, c as CacheHandlerOptions } from '../types-C1vJ_sED.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Spec-compliant `cacheHandlers` (plural) implementation. See docs/next16-spec.md.
|
|
@@ -43,7 +43,40 @@ __export(adapter_redis_exports, {
|
|
|
43
43
|
createRedisV5Client: () => createRedisV5Client
|
|
44
44
|
});
|
|
45
45
|
function adaptRedisV5(client) {
|
|
46
|
-
|
|
46
|
+
const like = client;
|
|
47
|
+
like.dispose = () => {
|
|
48
|
+
try {
|
|
49
|
+
client.destroy();
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
like.subscribe = async (channel, onMessage, onDown) => {
|
|
54
|
+
const sub = client.duplicate();
|
|
55
|
+
const teardown = () => {
|
|
56
|
+
try {
|
|
57
|
+
sub.destroy();
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
let down = false;
|
|
62
|
+
const markDown = () => {
|
|
63
|
+
if (down) return;
|
|
64
|
+
down = true;
|
|
65
|
+
teardown();
|
|
66
|
+
onDown?.();
|
|
67
|
+
};
|
|
68
|
+
sub.on("error", markDown);
|
|
69
|
+
sub.on("end", markDown);
|
|
70
|
+
try {
|
|
71
|
+
await sub.connect();
|
|
72
|
+
await sub.subscribe(channel, (message) => onMessage(message));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
teardown();
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
return async () => teardown();
|
|
78
|
+
};
|
|
79
|
+
return like;
|
|
47
80
|
}
|
|
48
81
|
function createRedisV5Client(config) {
|
|
49
82
|
const { createClient } = nodeRequire("redis");
|
|
@@ -86,7 +119,12 @@ function adapt(client, isClusterClient = false) {
|
|
|
86
119
|
}
|
|
87
120
|
},
|
|
88
121
|
get: (k) => client.get(k),
|
|
89
|
-
set: (k, v, opts) =>
|
|
122
|
+
set: (k, v, opts) => {
|
|
123
|
+
const args = [];
|
|
124
|
+
if (opts?.EX !== void 0) args.push("EX", opts.EX);
|
|
125
|
+
if (opts?.NX) args.push("NX");
|
|
126
|
+
return client.set(k, v, ...args);
|
|
127
|
+
},
|
|
90
128
|
del: async (keys) => {
|
|
91
129
|
const arr = Array.isArray(keys) ? keys : [keys];
|
|
92
130
|
if (arr.length === 0) return 0;
|
|
@@ -157,6 +195,40 @@ function adapt(client, isClusterClient = false) {
|
|
|
157
195
|
client.on(event, listener);
|
|
158
196
|
return client;
|
|
159
197
|
},
|
|
198
|
+
publish: (channel, message) => client.publish(channel, message),
|
|
199
|
+
...isClusterClient ? {} : {
|
|
200
|
+
subscribe: async (channel, onMessage, onDown) => {
|
|
201
|
+
const sub = client.duplicate();
|
|
202
|
+
let down = false;
|
|
203
|
+
const markDown = () => {
|
|
204
|
+
if (down) return;
|
|
205
|
+
down = true;
|
|
206
|
+
try {
|
|
207
|
+
sub.disconnect();
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
onDown?.();
|
|
211
|
+
};
|
|
212
|
+
sub.on("error", markDown);
|
|
213
|
+
sub.on("end", markDown);
|
|
214
|
+
try {
|
|
215
|
+
if (sub.status !== "ready" && sub.status !== "connecting") {
|
|
216
|
+
await sub.connect();
|
|
217
|
+
}
|
|
218
|
+
await sub.subscribe(channel);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
markDown();
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
sub.on("message", (ch, message) => {
|
|
224
|
+
if (ch === channel) onMessage(message);
|
|
225
|
+
});
|
|
226
|
+
return async () => {
|
|
227
|
+
down = true;
|
|
228
|
+
sub.disconnect();
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
},
|
|
160
232
|
dispose: () => {
|
|
161
233
|
client.disconnect();
|
|
162
234
|
}
|
|
@@ -807,6 +879,7 @@ function init(opts) {
|
|
|
807
879
|
hashTag: opts.hashTag ?? false,
|
|
808
880
|
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
809
881
|
singleFlight: opts.singleFlight ?? false,
|
|
882
|
+
tagPubSub: opts.tagPubSub ?? false,
|
|
810
883
|
singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
|
|
811
884
|
rest: opts
|
|
812
885
|
},
|
|
@@ -817,6 +890,7 @@ function init(opts) {
|
|
|
817
890
|
memTags: new MemorySetStore(opts.memoryMaxEntries),
|
|
818
891
|
memTagExp: /* @__PURE__ */ new Map(),
|
|
819
892
|
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
893
|
+
pubsub: { active: false, disabled: false, attempting: false, lastAttempt: 0 },
|
|
820
894
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
|
|
821
895
|
instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
|
|
822
896
|
};
|
|
@@ -894,6 +968,7 @@ function resolveTtlSeconds(entry) {
|
|
|
894
968
|
async function getImpl(state, cacheKey, softTags) {
|
|
895
969
|
const start = Date.now();
|
|
896
970
|
const useRedis = gateRedis(state);
|
|
971
|
+
if (useRedis) void ensureTagSubscription(state);
|
|
897
972
|
if (!useRedis && state.opts.fallback === "never") {
|
|
898
973
|
state.emit({ type: "cache.miss", meta: { reason: "no-redis" } });
|
|
899
974
|
return void 0;
|
|
@@ -970,8 +1045,11 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
970
1045
|
if (freshnessTags.length > 0) {
|
|
971
1046
|
let mostRecent = 0;
|
|
972
1047
|
for (const t of freshnessTags) {
|
|
973
|
-
const ts =
|
|
974
|
-
|
|
1048
|
+
const ts = Math.max(
|
|
1049
|
+
state.localTagTimestamps.get(t) ?? 0,
|
|
1050
|
+
state.memTagExp.get(t) ?? 0
|
|
1051
|
+
);
|
|
1052
|
+
if (ts > mostRecent) mostRecent = ts;
|
|
975
1053
|
}
|
|
976
1054
|
if (mostRecent > envelope.timestamp) {
|
|
977
1055
|
if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
|
|
@@ -1104,10 +1182,68 @@ async function luaSetWithTags(client, state, eKey, payload, ttl, tags) {
|
|
|
1104
1182
|
[payload, String(ttl), String(ttl), String(tags.length)]
|
|
1105
1183
|
);
|
|
1106
1184
|
}
|
|
1185
|
+
function invalChannel(state) {
|
|
1186
|
+
const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
|
|
1187
|
+
return `${state.opts.keyPrefix}inval:${ns}`;
|
|
1188
|
+
}
|
|
1189
|
+
var SUBSCRIBE_RETRY_MS = 5e3;
|
|
1190
|
+
async function ensureTagSubscription(state) {
|
|
1191
|
+
if (!state.opts.tagPubSub || state.pubsub.active || state.pubsub.disabled || state.pubsub.attempting) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const now = Date.now();
|
|
1195
|
+
if (now - state.pubsub.lastAttempt < SUBSCRIBE_RETRY_MS) return;
|
|
1196
|
+
state.pubsub.lastAttempt = now;
|
|
1197
|
+
state.pubsub.attempting = true;
|
|
1198
|
+
try {
|
|
1199
|
+
const client = await state.conn.getOrConnect();
|
|
1200
|
+
if (!client) return;
|
|
1201
|
+
if (typeof client.subscribe !== "function") {
|
|
1202
|
+
state.pubsub.disabled = true;
|
|
1203
|
+
state.logger.warn(
|
|
1204
|
+
"tagPubSub unavailable on this client (no subscribe support, e.g. Cluster) \u2014 falling back to scan-based propagation"
|
|
1205
|
+
);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const stop = await client.subscribe(
|
|
1209
|
+
invalChannel(state),
|
|
1210
|
+
(message) => {
|
|
1211
|
+
try {
|
|
1212
|
+
const parsed = JSON.parse(message);
|
|
1213
|
+
if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
|
|
1214
|
+
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);
|
|
1219
|
+
}
|
|
1220
|
+
pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
|
|
1221
|
+
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
() => {
|
|
1226
|
+
state.pubsub.active = false;
|
|
1227
|
+
}
|
|
1228
|
+
);
|
|
1229
|
+
state.pubsub.active = true;
|
|
1230
|
+
state.pubsub.stop = async () => {
|
|
1231
|
+
state.pubsub.active = false;
|
|
1232
|
+
await stop();
|
|
1233
|
+
};
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
state.logger.warn("tagPubSub subscribe failed \u2014 will retry", {
|
|
1236
|
+
message: err.message
|
|
1237
|
+
});
|
|
1238
|
+
} finally {
|
|
1239
|
+
state.pubsub.attempting = false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1107
1242
|
async function refreshTagsImpl(state) {
|
|
1108
1243
|
if (!gateRedis(state)) {
|
|
1109
1244
|
return;
|
|
1110
1245
|
}
|
|
1246
|
+
void ensureTagSubscription(state);
|
|
1111
1247
|
try {
|
|
1112
1248
|
await withAbortSignal(
|
|
1113
1249
|
"cacheHandlers.refreshTags",
|
|
@@ -1265,6 +1401,16 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1265
1401
|
type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
1266
1402
|
meta: { count: tags.length, backend: "redis" }
|
|
1267
1403
|
});
|
|
1404
|
+
if (state.opts.tagPubSub) {
|
|
1405
|
+
try {
|
|
1406
|
+
const client = await state.conn.getOrConnect();
|
|
1407
|
+
await client?.publish?.(
|
|
1408
|
+
invalChannel(state),
|
|
1409
|
+
JSON.stringify({ t: tags, ts: now })
|
|
1410
|
+
);
|
|
1411
|
+
} catch {
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1268
1414
|
} catch (err) {
|
|
1269
1415
|
if (err instanceof CacheTimeoutError) {
|
|
1270
1416
|
state.emit({ type: "redis.timeout", meta: { op: "updateTags" } });
|