@leejpsd/nextjs-cache-handler 0.3.4 → 0.4.1

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +10 -3
  3. package/bin/cli.cjs +10 -0
  4. package/dist/cache-components/index.cjs +150 -4
  5. package/dist/cache-components/index.cjs.map +1 -1
  6. package/dist/cache-components/index.d.cts +1 -1
  7. package/dist/cache-components/index.d.ts +1 -1
  8. package/dist/cache-components/index.js +150 -4
  9. package/dist/cache-components/index.js.map +1 -1
  10. package/dist/cli/index.cjs +938 -0
  11. package/dist/cli/index.cjs.map +1 -0
  12. package/dist/cli/index.d.cts +28 -0
  13. package/dist/cli/index.d.ts +28 -0
  14. package/dist/cli/index.js +928 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/incremental/index.cjs +81 -6
  17. package/dist/incremental/index.cjs.map +1 -1
  18. package/dist/incremental/index.d.cts +1 -1
  19. package/dist/incremental/index.d.ts +1 -1
  20. package/dist/incremental/index.js +81 -6
  21. package/dist/incremental/index.js.map +1 -1
  22. package/dist/index.cjs +157 -8
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +157 -8
  27. package/dist/index.js.map +1 -1
  28. package/dist/ops/index.d.cts +1 -1
  29. package/dist/ops/index.d.ts +1 -1
  30. package/dist/otel/index.d.cts +1 -1
  31. package/dist/otel/index.d.ts +1 -1
  32. package/dist/seed/index.cjs +561 -0
  33. package/dist/seed/index.cjs.map +1 -0
  34. package/dist/seed/index.d.cts +53 -0
  35. package/dist/seed/index.d.ts +53 -0
  36. package/dist/seed/index.js +550 -0
  37. package/dist/seed/index.js.map +1 -0
  38. package/dist/shared/client/adapter-ioredis.cjs +40 -1
  39. package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
  40. package/dist/shared/client/adapter-ioredis.d.cts +1 -1
  41. package/dist/shared/client/adapter-ioredis.d.ts +1 -1
  42. package/dist/shared/client/adapter-ioredis.js +40 -1
  43. package/dist/shared/client/adapter-ioredis.js.map +1 -1
  44. package/dist/shared/client/adapter-redis.cjs +34 -1
  45. package/dist/shared/client/adapter-redis.cjs.map +1 -1
  46. package/dist/shared/client/adapter-redis.d.cts +1 -1
  47. package/dist/shared/client/adapter-redis.d.ts +1 -1
  48. package/dist/shared/client/adapter-redis.js +34 -1
  49. package/dist/shared/client/adapter-redis.js.map +1 -1
  50. package/dist/{types-Bl-rkLKX.d.cts → types-C1vJ_sED.d.cts} +25 -1
  51. package/dist/{types-Bl-rkLKX.d.ts → types-C1vJ_sED.d.ts} +25 -1
  52. package/package.json +22 -2
  53. package/setup-instructions/setup.md +31 -9
  54. package/skills/nextjs-redis-cache/SKILL.md +15 -0
@@ -1,4 +1,4 @@
1
- import { C as CacheComponentsEntry, b as CacheHandlerOptions } from '../types-Bl-rkLKX.cjs';
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, b as CacheHandlerOptions } from '../types-Bl-rkLKX.js';
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
- return client;
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) => opts?.EX !== void 0 ? client.set(k, v, "EX", opts.EX) : client.set(k, v),
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 = state.localTagTimestamps.get(t);
974
- if (ts !== void 0 && ts > mostRecent) mostRecent = ts;
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" } });