@leejpsd/nextjs-cache-handler 0.3.4 → 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/CHANGELOG.md +32 -0
- package/README.md +5 -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/package.json +22 -2
- package/setup-instructions/setup.md +31 -9
- package/skills/nextjs-redis-cache/SKILL.md +15 -0
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,
|
|
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';
|
|
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,
|
|
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';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Timeout-bounded operation wrapper. Surrounds every Redis call so a hung
|
package/dist/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -821,6 +893,7 @@ function init(opts) {
|
|
|
821
893
|
hashTag: opts.hashTag ?? false,
|
|
822
894
|
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
823
895
|
singleFlight: opts.singleFlight ?? false,
|
|
896
|
+
tagPubSub: opts.tagPubSub ?? false,
|
|
824
897
|
singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
|
|
825
898
|
rest: opts
|
|
826
899
|
},
|
|
@@ -831,6 +904,7 @@ function init(opts) {
|
|
|
831
904
|
memTags: new MemorySetStore(opts.memoryMaxEntries),
|
|
832
905
|
memTagExp: /* @__PURE__ */ new Map(),
|
|
833
906
|
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
907
|
+
pubsub: { active: false, disabled: false, attempting: false, lastAttempt: 0 },
|
|
834
908
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
|
|
835
909
|
instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
|
|
836
910
|
};
|
|
@@ -908,6 +982,7 @@ function resolveTtlSeconds(entry) {
|
|
|
908
982
|
async function getImpl(state, cacheKey, softTags) {
|
|
909
983
|
const start = Date.now();
|
|
910
984
|
const useRedis = gateRedis(state);
|
|
985
|
+
if (useRedis) void ensureTagSubscription(state);
|
|
911
986
|
if (!useRedis && state.opts.fallback === "never") {
|
|
912
987
|
state.emit({ type: "cache.miss", meta: { reason: "no-redis" } });
|
|
913
988
|
return void 0;
|
|
@@ -984,8 +1059,11 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
984
1059
|
if (freshnessTags.length > 0) {
|
|
985
1060
|
let mostRecent = 0;
|
|
986
1061
|
for (const t of freshnessTags) {
|
|
987
|
-
const ts =
|
|
988
|
-
|
|
1062
|
+
const ts = Math.max(
|
|
1063
|
+
state.localTagTimestamps.get(t) ?? 0,
|
|
1064
|
+
state.memTagExp.get(t) ?? 0
|
|
1065
|
+
);
|
|
1066
|
+
if (ts > mostRecent) mostRecent = ts;
|
|
989
1067
|
}
|
|
990
1068
|
if (mostRecent > envelope.timestamp) {
|
|
991
1069
|
if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
|
|
@@ -1118,10 +1196,68 @@ async function luaSetWithTags(client, state, eKey, payload, ttl, tags) {
|
|
|
1118
1196
|
[payload, String(ttl), String(ttl), String(tags.length)]
|
|
1119
1197
|
);
|
|
1120
1198
|
}
|
|
1199
|
+
function invalChannel(state) {
|
|
1200
|
+
const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
|
|
1201
|
+
return `${state.opts.keyPrefix}inval:${ns}`;
|
|
1202
|
+
}
|
|
1203
|
+
var SUBSCRIBE_RETRY_MS = 5e3;
|
|
1204
|
+
async function ensureTagSubscription(state) {
|
|
1205
|
+
if (!state.opts.tagPubSub || state.pubsub.active || state.pubsub.disabled || state.pubsub.attempting) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const now = Date.now();
|
|
1209
|
+
if (now - state.pubsub.lastAttempt < SUBSCRIBE_RETRY_MS) return;
|
|
1210
|
+
state.pubsub.lastAttempt = now;
|
|
1211
|
+
state.pubsub.attempting = true;
|
|
1212
|
+
try {
|
|
1213
|
+
const client = await state.conn.getOrConnect();
|
|
1214
|
+
if (!client) return;
|
|
1215
|
+
if (typeof client.subscribe !== "function") {
|
|
1216
|
+
state.pubsub.disabled = true;
|
|
1217
|
+
state.logger.warn(
|
|
1218
|
+
"tagPubSub unavailable on this client (no subscribe support, e.g. Cluster) \u2014 falling back to scan-based propagation"
|
|
1219
|
+
);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const stop = await client.subscribe(
|
|
1223
|
+
invalChannel(state),
|
|
1224
|
+
(message) => {
|
|
1225
|
+
try {
|
|
1226
|
+
const parsed = JSON.parse(message);
|
|
1227
|
+
if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
|
|
1228
|
+
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);
|
|
1233
|
+
}
|
|
1234
|
+
pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
|
|
1235
|
+
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
},
|
|
1239
|
+
() => {
|
|
1240
|
+
state.pubsub.active = false;
|
|
1241
|
+
}
|
|
1242
|
+
);
|
|
1243
|
+
state.pubsub.active = true;
|
|
1244
|
+
state.pubsub.stop = async () => {
|
|
1245
|
+
state.pubsub.active = false;
|
|
1246
|
+
await stop();
|
|
1247
|
+
};
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
state.logger.warn("tagPubSub subscribe failed \u2014 will retry", {
|
|
1250
|
+
message: err.message
|
|
1251
|
+
});
|
|
1252
|
+
} finally {
|
|
1253
|
+
state.pubsub.attempting = false;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1121
1256
|
async function refreshTagsImpl(state) {
|
|
1122
1257
|
if (!gateRedis(state)) {
|
|
1123
1258
|
return;
|
|
1124
1259
|
}
|
|
1260
|
+
void ensureTagSubscription(state);
|
|
1125
1261
|
try {
|
|
1126
1262
|
await withAbortSignal(
|
|
1127
1263
|
"cacheHandlers.refreshTags",
|
|
@@ -1279,6 +1415,16 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1279
1415
|
type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
1280
1416
|
meta: { count: tags.length, backend: "redis" }
|
|
1281
1417
|
});
|
|
1418
|
+
if (state.opts.tagPubSub) {
|
|
1419
|
+
try {
|
|
1420
|
+
const client = await state.conn.getOrConnect();
|
|
1421
|
+
await client?.publish?.(
|
|
1422
|
+
invalChannel(state),
|
|
1423
|
+
JSON.stringify({ t: tags, ts: now })
|
|
1424
|
+
);
|
|
1425
|
+
} catch {
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1282
1428
|
} catch (err) {
|
|
1283
1429
|
if (err instanceof CacheTimeoutError) {
|
|
1284
1430
|
state.emit({ type: "redis.timeout", meta: { op: "updateTags" } });
|
|
@@ -1470,7 +1616,9 @@ async function readTagStates(state, tags) {
|
|
|
1470
1616
|
async () => {
|
|
1471
1617
|
const client = await state.conn.getOrConnect();
|
|
1472
1618
|
if (!client) return tags.map((t) => state.memTagStates.get(t) ?? null);
|
|
1473
|
-
const values = await
|
|
1619
|
+
const values = await Promise.all(
|
|
1620
|
+
tags.map((t) => client.get(tagMetaKey(state, t)))
|
|
1621
|
+
);
|
|
1474
1622
|
return values.map((v) => {
|
|
1475
1623
|
if (!v) return null;
|
|
1476
1624
|
try {
|