@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
package/dist/index.cjs
CHANGED
|
@@ -53,7 +53,40 @@ __export(adapter_redis_exports, {
|
|
|
53
53
|
createRedisV5Client: () => createRedisV5Client
|
|
54
54
|
});
|
|
55
55
|
function adaptRedisV5(client) {
|
|
56
|
-
|
|
56
|
+
const like = client;
|
|
57
|
+
like.dispose = () => {
|
|
58
|
+
try {
|
|
59
|
+
client.destroy();
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
like.subscribe = async (channel, onMessage, onDown) => {
|
|
64
|
+
const sub = client.duplicate();
|
|
65
|
+
const teardown = () => {
|
|
66
|
+
try {
|
|
67
|
+
sub.destroy();
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
let down = false;
|
|
72
|
+
const markDown = () => {
|
|
73
|
+
if (down) return;
|
|
74
|
+
down = true;
|
|
75
|
+
teardown();
|
|
76
|
+
onDown?.();
|
|
77
|
+
};
|
|
78
|
+
sub.on("error", markDown);
|
|
79
|
+
sub.on("end", markDown);
|
|
80
|
+
try {
|
|
81
|
+
await sub.connect();
|
|
82
|
+
await sub.subscribe(channel, (message) => onMessage(message));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
teardown();
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
return async () => teardown();
|
|
88
|
+
};
|
|
89
|
+
return like;
|
|
57
90
|
}
|
|
58
91
|
function createRedisV5Client(config) {
|
|
59
92
|
const { createClient } = nodeRequire("redis");
|
|
@@ -96,7 +129,12 @@ function adapt(client, isClusterClient = false) {
|
|
|
96
129
|
}
|
|
97
130
|
},
|
|
98
131
|
get: (k) => client.get(k),
|
|
99
|
-
set: (k, v, opts) =>
|
|
132
|
+
set: (k, v, opts) => {
|
|
133
|
+
const args = [];
|
|
134
|
+
if (opts?.EX !== void 0) args.push("EX", opts.EX);
|
|
135
|
+
if (opts?.NX) args.push("NX");
|
|
136
|
+
return client.set(k, v, ...args);
|
|
137
|
+
},
|
|
100
138
|
del: async (keys) => {
|
|
101
139
|
const arr = Array.isArray(keys) ? keys : [keys];
|
|
102
140
|
if (arr.length === 0) return 0;
|
|
@@ -167,6 +205,40 @@ function adapt(client, isClusterClient = false) {
|
|
|
167
205
|
client.on(event, listener);
|
|
168
206
|
return client;
|
|
169
207
|
},
|
|
208
|
+
publish: (channel, message) => client.publish(channel, message),
|
|
209
|
+
...isClusterClient ? {} : {
|
|
210
|
+
subscribe: async (channel, onMessage, onDown) => {
|
|
211
|
+
const sub = client.duplicate();
|
|
212
|
+
let down = false;
|
|
213
|
+
const markDown = () => {
|
|
214
|
+
if (down) return;
|
|
215
|
+
down = true;
|
|
216
|
+
try {
|
|
217
|
+
sub.disconnect();
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
onDown?.();
|
|
221
|
+
};
|
|
222
|
+
sub.on("error", markDown);
|
|
223
|
+
sub.on("end", markDown);
|
|
224
|
+
try {
|
|
225
|
+
if (sub.status !== "ready" && sub.status !== "connecting") {
|
|
226
|
+
await sub.connect();
|
|
227
|
+
}
|
|
228
|
+
await sub.subscribe(channel);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
markDown();
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
sub.on("message", (ch, message) => {
|
|
234
|
+
if (ch === channel) onMessage(message);
|
|
235
|
+
});
|
|
236
|
+
return async () => {
|
|
237
|
+
down = true;
|
|
238
|
+
sub.disconnect();
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
},
|
|
170
242
|
dispose: () => {
|
|
171
243
|
client.disconnect();
|
|
172
244
|
}
|
|
@@ -831,6 +903,7 @@ function init(opts) {
|
|
|
831
903
|
hashTag: opts.hashTag ?? false,
|
|
832
904
|
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
833
905
|
singleFlight: opts.singleFlight ?? false,
|
|
906
|
+
tagPubSub: opts.tagPubSub ?? false,
|
|
834
907
|
singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
|
|
835
908
|
rest: opts
|
|
836
909
|
},
|
|
@@ -841,6 +914,7 @@ function init(opts) {
|
|
|
841
914
|
memTags: new MemorySetStore(opts.memoryMaxEntries),
|
|
842
915
|
memTagExp: /* @__PURE__ */ new Map(),
|
|
843
916
|
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
917
|
+
pubsub: { active: false, disabled: false, attempting: false, lastAttempt: 0 },
|
|
844
918
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
|
|
845
919
|
instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
|
|
846
920
|
};
|
|
@@ -918,6 +992,7 @@ function resolveTtlSeconds(entry) {
|
|
|
918
992
|
async function getImpl(state, cacheKey, softTags) {
|
|
919
993
|
const start = Date.now();
|
|
920
994
|
const useRedis = gateRedis(state);
|
|
995
|
+
if (useRedis) void ensureTagSubscription(state);
|
|
921
996
|
if (!useRedis && state.opts.fallback === "never") {
|
|
922
997
|
state.emit({ type: "cache.miss", meta: { reason: "no-redis" } });
|
|
923
998
|
return void 0;
|
|
@@ -994,8 +1069,11 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
994
1069
|
if (freshnessTags.length > 0) {
|
|
995
1070
|
let mostRecent = 0;
|
|
996
1071
|
for (const t of freshnessTags) {
|
|
997
|
-
const ts =
|
|
998
|
-
|
|
1072
|
+
const ts = Math.max(
|
|
1073
|
+
state.localTagTimestamps.get(t) ?? 0,
|
|
1074
|
+
state.memTagExp.get(t) ?? 0
|
|
1075
|
+
);
|
|
1076
|
+
if (ts > mostRecent) mostRecent = ts;
|
|
999
1077
|
}
|
|
1000
1078
|
if (mostRecent > envelope.timestamp) {
|
|
1001
1079
|
if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
|
|
@@ -1128,10 +1206,68 @@ async function luaSetWithTags(client, state, eKey, payload, ttl, tags) {
|
|
|
1128
1206
|
[payload, String(ttl), String(ttl), String(tags.length)]
|
|
1129
1207
|
);
|
|
1130
1208
|
}
|
|
1209
|
+
function invalChannel(state) {
|
|
1210
|
+
const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
|
|
1211
|
+
return `${state.opts.keyPrefix}inval:${ns}`;
|
|
1212
|
+
}
|
|
1213
|
+
var SUBSCRIBE_RETRY_MS = 5e3;
|
|
1214
|
+
async function ensureTagSubscription(state) {
|
|
1215
|
+
if (!state.opts.tagPubSub || state.pubsub.active || state.pubsub.disabled || state.pubsub.attempting) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const now = Date.now();
|
|
1219
|
+
if (now - state.pubsub.lastAttempt < SUBSCRIBE_RETRY_MS) return;
|
|
1220
|
+
state.pubsub.lastAttempt = now;
|
|
1221
|
+
state.pubsub.attempting = true;
|
|
1222
|
+
try {
|
|
1223
|
+
const client = await state.conn.getOrConnect();
|
|
1224
|
+
if (!client) return;
|
|
1225
|
+
if (typeof client.subscribe !== "function") {
|
|
1226
|
+
state.pubsub.disabled = true;
|
|
1227
|
+
state.logger.warn(
|
|
1228
|
+
"tagPubSub unavailable on this client (no subscribe support, e.g. Cluster) \u2014 falling back to scan-based propagation"
|
|
1229
|
+
);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const stop = await client.subscribe(
|
|
1233
|
+
invalChannel(state),
|
|
1234
|
+
(message) => {
|
|
1235
|
+
try {
|
|
1236
|
+
const parsed = JSON.parse(message);
|
|
1237
|
+
if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
|
|
1238
|
+
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);
|
|
1243
|
+
}
|
|
1244
|
+
pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
|
|
1245
|
+
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
() => {
|
|
1250
|
+
state.pubsub.active = false;
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1253
|
+
state.pubsub.active = true;
|
|
1254
|
+
state.pubsub.stop = async () => {
|
|
1255
|
+
state.pubsub.active = false;
|
|
1256
|
+
await stop();
|
|
1257
|
+
};
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
state.logger.warn("tagPubSub subscribe failed \u2014 will retry", {
|
|
1260
|
+
message: err.message
|
|
1261
|
+
});
|
|
1262
|
+
} finally {
|
|
1263
|
+
state.pubsub.attempting = false;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1131
1266
|
async function refreshTagsImpl(state) {
|
|
1132
1267
|
if (!gateRedis(state)) {
|
|
1133
1268
|
return;
|
|
1134
1269
|
}
|
|
1270
|
+
void ensureTagSubscription(state);
|
|
1135
1271
|
try {
|
|
1136
1272
|
await withAbortSignal(
|
|
1137
1273
|
"cacheHandlers.refreshTags",
|
|
@@ -1289,6 +1425,16 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1289
1425
|
type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
1290
1426
|
meta: { count: tags.length, backend: "redis" }
|
|
1291
1427
|
});
|
|
1428
|
+
if (state.opts.tagPubSub) {
|
|
1429
|
+
try {
|
|
1430
|
+
const client = await state.conn.getOrConnect();
|
|
1431
|
+
await client?.publish?.(
|
|
1432
|
+
invalChannel(state),
|
|
1433
|
+
JSON.stringify({ t: tags, ts: now })
|
|
1434
|
+
);
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1292
1438
|
} catch (err) {
|
|
1293
1439
|
if (err instanceof CacheTimeoutError) {
|
|
1294
1440
|
state.emit({ type: "redis.timeout", meta: { op: "updateTags" } });
|
|
@@ -1480,7 +1626,9 @@ async function readTagStates(state, tags) {
|
|
|
1480
1626
|
async () => {
|
|
1481
1627
|
const client = await state.conn.getOrConnect();
|
|
1482
1628
|
if (!client) return tags.map((t) => state.memTagStates.get(t) ?? null);
|
|
1483
|
-
const values = await
|
|
1629
|
+
const values = await Promise.all(
|
|
1630
|
+
tags.map((t) => client.get(tagMetaKey(state, t)))
|
|
1631
|
+
);
|
|
1484
1632
|
return values.map((v) => {
|
|
1485
1633
|
if (!v) return null;
|
|
1486
1634
|
try {
|