@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/AGENTS.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @leejpsd/nextjs-cache-handler — notes for AI agents
|
|
2
|
+
|
|
3
|
+
Redis cache handler for **self-hosted** Next.js 15/16. Ships BOTH interfaces:
|
|
4
|
+
`cacheHandler` (ISR, Next 15+16) and `cacheHandlers` (`'use cache'`,
|
|
5
|
+
Next >= 16.1.5). Not for Vercel-hosted apps.
|
|
6
|
+
|
|
7
|
+
**Full agent guidance:** `skills/nextjs-redis-cache/SKILL.md` in this package
|
|
8
|
+
(also installable via `npx skills add leejpsd/nextjs-cache-handler`).
|
|
9
|
+
**Agent-executable setup:**
|
|
10
|
+
https://raw.githubusercontent.com/leejpsd/nextjs-cache-handler/main/setup-instructions/setup.md
|
|
11
|
+
|
|
12
|
+
## Rules that prevent the most common mistakes
|
|
13
|
+
|
|
14
|
+
1. `revalidateTag(tag, "max")` is SOFT = stale-while-revalidate: instant
|
|
15
|
+
stale serve + background re-render, cross-instance convergence in ~1-2s.
|
|
16
|
+
Brief stale content is BY DESIGN — not a bug. `updateTag(tag)` is HARD =
|
|
17
|
+
delete + blocking regeneration. Requires version >= 0.3.3.
|
|
18
|
+
2. `cacheMaxMemorySize: 0` in next.config is mandatory for multi-instance
|
|
19
|
+
correctness. Do not remove it.
|
|
20
|
+
3. `DEPLOYMENT_VERSION` env must exist at RUNTIME (Docker runner stage).
|
|
21
|
+
Missing → cross-deploy cache bleed / static chunk 404s.
|
|
22
|
+
4. Redis Cluster needs `hashTag: true`; TLS via `rediss://` URLs.
|
|
23
|
+
5. Build-time `ECONNREFUSED` toward Redis is expected (build-phase gate) —
|
|
24
|
+
only investigate if the build fails.
|
|
25
|
+
6. Redis outages degrade to a bounded in-memory fallback with automatic
|
|
26
|
+
reconnect (backoff 1s→30s). Do not wrap cache calls in extra try/catch.
|
|
27
|
+
7. Wiring shape: two CJS wrapper files + `require.resolve` in next.config —
|
|
28
|
+
see README "Quick start" or the setup URL above.
|
|
29
|
+
|
|
30
|
+
Entry points: `/incremental` (ISR), `/cache-components` ('use cache'),
|
|
31
|
+
`/client/redis`, `/client/ioredis` (incl. cluster/sentinel), `/ops`
|
|
32
|
+
(metric snapshot), `/otel` (OpenTelemetry emitter). API details: README.md
|
|
33
|
+
and https://github.com/leejpsd/nextjs-cache-handler/blob/main/docs/api.md
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#7](https://github.com/leejpsd/nextjs-cache-handler/pull/7) [`bf047c9`](https://github.com/leejpsd/nextjs-cache-handler/commit/bf047c937985dafabed7c60ab1972a749a8f3b9f) Thanks [@leejpsd](https://github.com/leejpsd)! - Build-output cache seeding: `seedBuildOutput()` (new `/seed` entry point) and
|
|
8
|
+
`npx nextjs-cache-handler seed` walk `.next/` after a build and insert
|
|
9
|
+
prerendered App Router routes (including PPR segment data), Pages Router
|
|
10
|
+
routes, and fetch-cache entries into Redis in the handler's own record
|
|
11
|
+
format — with NX semantics so entries already written by live instances are
|
|
12
|
+
never overwritten. A fresh deployment's first requests are cache HITs
|
|
13
|
+
instead of a regeneration stampede (verified on a real Next 16 app: cold
|
|
14
|
+
server + seeded Redis → first request `x-nextjs-cache: HIT`). The
|
|
15
|
+
`RedisClientLike.set` contract gains an optional `NX` flag.
|
|
16
|
+
|
|
17
|
+
- [#7](https://github.com/leejpsd/nextjs-cache-handler/pull/7) [`1e57e03`](https://github.com/leejpsd/nextjs-cache-handler/commit/1e57e03020926933f524796645b279d004dddf2a) Thanks [@leejpsd](https://github.com/leejpsd)! - New `nextjs-cache-handler` CLI (zero-dependency): `init` detects the Next.js
|
|
18
|
+
version and Redis client, generates the handler wrapper shims, shows the
|
|
19
|
+
next.config keys to add (never edits it), appends env templates, and injects
|
|
20
|
+
the agent rules block into CLAUDE.md/AGENTS.md idempotently (`--yes` to
|
|
21
|
+
apply, `--skills` to install the agent skill locally). `doctor` verifies
|
|
22
|
+
Redis connectivity, inspects cache key namespaces, and runs a write/read
|
|
23
|
+
round-trip — the first command an agent should reach for when debugging.
|
|
24
|
+
|
|
25
|
+
- [#7](https://github.com/leejpsd/nextjs-cache-handler/pull/7) [`6cd5901`](https://github.com/leejpsd/nextjs-cache-handler/commit/6cd59010d30b55e8a5f9b99db8ec53b0e64e03ce) Thanks [@leejpsd](https://github.com/leejpsd)! - Opt-in push-based tag propagation (`tagPubSub: true`, plural handler):
|
|
26
|
+
`updateTags()` publishes invalidations on a namespaced channel and every
|
|
27
|
+
instance maintains a subscription on a dedicated duplicate connection,
|
|
28
|
+
updating its local tag mirror in ~3 ms (measured cross-instance over real
|
|
29
|
+
Redis with both redis@5 and ioredis) instead of waiting for the next
|
|
30
|
+
`refreshTags()` scan (~seconds). The scan keeps running as the consistency
|
|
31
|
+
safety net, so a dropped subscription degrades to the previous behavior —
|
|
32
|
+
never to staleness. Cluster clients fall back to polling with a one-time
|
|
33
|
+
warning. `RedisClientLike` gains optional `publish`/`subscribe`.
|
|
34
|
+
|
|
35
|
+
## 0.3.4
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- [`79e112b`](https://github.com/leejpsd/nextjs-cache-handler/commit/79e112bc03249e6e042da717c4558de733cc62f0) Thanks [@leejpsd](https://github.com/leejpsd)! - Ship agent-native assets in the package: `AGENTS.md` (agent guidance
|
|
40
|
+
discovered via node_modules scans), an agent skill
|
|
41
|
+
(`skills/nextjs-redis-cache/SKILL.md` — decision tables, invalidation
|
|
42
|
+
semantics, troubleshooting playbook), agent-executable setup instructions
|
|
43
|
+
(`setup-instructions/setup.md`), a project rules block (`rules/`), and
|
|
44
|
+
`llms.txt`. README gains a "For AI agents" section. No runtime code changes.
|
|
45
|
+
|
|
3
46
|
## 0.3.3
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,6 +16,23 @@
|
|
|
16
16
|
> Sentinel** support, a built-in **OpenTelemetry emitter** at `/otel`, and an
|
|
17
17
|
> LRU-bounded memory fallback.
|
|
18
18
|
|
|
19
|
+
## 🤖 For AI agents
|
|
20
|
+
|
|
21
|
+
Working with Claude Code / Codex / Cursor? Give your agent this URL and it
|
|
22
|
+
will install and wire everything (version detection, wrapper files,
|
|
23
|
+
next.config patch, verification):
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
https://raw.githubusercontent.com/leejpsd/nextjs-cache-handler/main/setup-instructions/setup.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
An agent skill with decision tables, invalidation semantics, and a
|
|
30
|
+
troubleshooting playbook ships in the package (`AGENTS.md`,
|
|
31
|
+
`skills/nextjs-redis-cache/SKILL.md`) and via
|
|
32
|
+
`npx skills add leejpsd/nextjs-cache-handler`.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
19
36
|
The Redis cache handler for **Next.js 15/16** that ships **both** `cacheHandler`
|
|
20
37
|
(ISR / Pages Router) **and** `cacheHandlers` (`'use cache'` directive,
|
|
21
38
|
`cacheComponents: true`) — the area where
|
|
@@ -228,8 +245,10 @@ Full reference: [`docs/api.md`](./docs/api.md).
|
|
|
228
245
|
on the same hash slot. Without `hashTag`, cluster deployments will hit
|
|
229
246
|
`CROSSSLOT Keys in request don't hash to the same slot`. The flag wraps
|
|
230
247
|
the namespace in `{}` so every key for a given deploy hashes together.
|
|
231
|
-
Cluster support is
|
|
232
|
-
|
|
248
|
+
Cluster support is validated by a dedicated e2e suite against a real
|
|
249
|
+
3-master cluster (`npm run test:cluster`, also in CI) — covering the
|
|
250
|
+
multi-key Lua scripts, per-master SCAN propagation, and both handlers.
|
|
251
|
+
Not yet load-tested at production scale.
|
|
233
252
|
- [ ] **Redis `maxmemory-policy: allkeys-lru` or `noeviction`** — if you need
|
|
234
253
|
bounded memory, choose `allkeys-lru`. Otherwise `noeviction` keeps
|
|
235
254
|
tag indices intact.
|
|
@@ -244,7 +263,7 @@ Full reference: [`docs/api.md`](./docs/api.md).
|
|
|
244
263
|
| Service | How to use | Tested? |
|
|
245
264
|
|---|---|---|
|
|
246
265
|
| **Self-hosted Redis 7+** | `{ type: "redis", url }` or `{ type: "ioredis", url }` | ✅ AWS ElastiCache 24h soak |
|
|
247
|
-
| **Redis Cluster** | `{ type: "cluster", nodes }` + `hashTag: true` |
|
|
266
|
+
| **Redis Cluster** | `{ type: "cluster", nodes }` + `hashTag: true` | ✅ e2e-tested against a real 3-master cluster (CI); not yet load-tested at scale |
|
|
248
267
|
| **Upstash Redis** | `{ type: "redis", url: "rediss://..." }` (TLS auto-detected) | not yet validated, expected to work via the standard Redis protocol |
|
|
249
268
|
| **AWS ElastiCache (replication group)** | `{ type: "redis", url: "rediss://..." }` | ✅ reference deployment (re-verified 2026-08-01, Seoul) |
|
|
250
269
|
| **Redis Sentinel** | `{ type: "sentinel", sentinels, name }` | ✅ local master/replica failover drill |
|
package/bin/cli.cjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin launcher for the built CLI (keeps the tsup build shebang-free).
|
|
3
|
+
const { main } = require("../dist/cli/index.cjs");
|
|
4
|
+
main().then(
|
|
5
|
+
(code) => process.exit(code),
|
|
6
|
+
(err) => {
|
|
7
|
+
console.error("[error]", err && err.message ? err.message : err);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
);
|
|
@@ -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
|
}
|
|
@@ -817,6 +889,7 @@ function init(opts) {
|
|
|
817
889
|
hashTag: opts.hashTag ?? false,
|
|
818
890
|
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
819
891
|
singleFlight: opts.singleFlight ?? false,
|
|
892
|
+
tagPubSub: opts.tagPubSub ?? false,
|
|
820
893
|
singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
|
|
821
894
|
rest: opts
|
|
822
895
|
},
|
|
@@ -827,6 +900,7 @@ function init(opts) {
|
|
|
827
900
|
memTags: new MemorySetStore(opts.memoryMaxEntries),
|
|
828
901
|
memTagExp: /* @__PURE__ */ new Map(),
|
|
829
902
|
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
903
|
+
pubsub: { active: false, disabled: false, attempting: false, lastAttempt: 0 },
|
|
830
904
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
|
|
831
905
|
instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
|
|
832
906
|
};
|
|
@@ -904,6 +978,7 @@ function resolveTtlSeconds(entry) {
|
|
|
904
978
|
async function getImpl(state, cacheKey, softTags) {
|
|
905
979
|
const start = Date.now();
|
|
906
980
|
const useRedis = gateRedis(state);
|
|
981
|
+
if (useRedis) void ensureTagSubscription(state);
|
|
907
982
|
if (!useRedis && state.opts.fallback === "never") {
|
|
908
983
|
state.emit({ type: "cache.miss", meta: { reason: "no-redis" } });
|
|
909
984
|
return void 0;
|
|
@@ -980,8 +1055,11 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
980
1055
|
if (freshnessTags.length > 0) {
|
|
981
1056
|
let mostRecent = 0;
|
|
982
1057
|
for (const t of freshnessTags) {
|
|
983
|
-
const ts =
|
|
984
|
-
|
|
1058
|
+
const ts = Math.max(
|
|
1059
|
+
state.localTagTimestamps.get(t) ?? 0,
|
|
1060
|
+
state.memTagExp.get(t) ?? 0
|
|
1061
|
+
);
|
|
1062
|
+
if (ts > mostRecent) mostRecent = ts;
|
|
985
1063
|
}
|
|
986
1064
|
if (mostRecent > envelope.timestamp) {
|
|
987
1065
|
if (state.opts.staleWhileRevalidate && partition.freshness !== "expired") {
|
|
@@ -1114,10 +1192,68 @@ async function luaSetWithTags(client, state, eKey, payload, ttl, tags) {
|
|
|
1114
1192
|
[payload, String(ttl), String(ttl), String(tags.length)]
|
|
1115
1193
|
);
|
|
1116
1194
|
}
|
|
1195
|
+
function invalChannel(state) {
|
|
1196
|
+
const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
|
|
1197
|
+
return `${state.opts.keyPrefix}inval:${ns}`;
|
|
1198
|
+
}
|
|
1199
|
+
var SUBSCRIBE_RETRY_MS = 5e3;
|
|
1200
|
+
async function ensureTagSubscription(state) {
|
|
1201
|
+
if (!state.opts.tagPubSub || state.pubsub.active || state.pubsub.disabled || state.pubsub.attempting) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const now = Date.now();
|
|
1205
|
+
if (now - state.pubsub.lastAttempt < SUBSCRIBE_RETRY_MS) return;
|
|
1206
|
+
state.pubsub.lastAttempt = now;
|
|
1207
|
+
state.pubsub.attempting = true;
|
|
1208
|
+
try {
|
|
1209
|
+
const client = await state.conn.getOrConnect();
|
|
1210
|
+
if (!client) return;
|
|
1211
|
+
if (typeof client.subscribe !== "function") {
|
|
1212
|
+
state.pubsub.disabled = true;
|
|
1213
|
+
state.logger.warn(
|
|
1214
|
+
"tagPubSub unavailable on this client (no subscribe support, e.g. Cluster) \u2014 falling back to scan-based propagation"
|
|
1215
|
+
);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const stop = await client.subscribe(
|
|
1219
|
+
invalChannel(state),
|
|
1220
|
+
(message) => {
|
|
1221
|
+
try {
|
|
1222
|
+
const parsed = JSON.parse(message);
|
|
1223
|
+
if (!Array.isArray(parsed.t) || typeof parsed.ts !== "number") return;
|
|
1224
|
+
for (const tag of parsed.t) {
|
|
1225
|
+
const prev = state.localTagTimestamps.get(tag) ?? 0;
|
|
1226
|
+
if (parsed.ts > prev) state.localTagTimestamps.set(tag, parsed.ts);
|
|
1227
|
+
const prevMem = state.memTagExp.get(tag) ?? 0;
|
|
1228
|
+
if (parsed.ts > prevMem) state.memTagExp.set(tag, parsed.ts);
|
|
1229
|
+
}
|
|
1230
|
+
pruneOldestByTimestamp(state.localTagTimestamps, MAX_LOCAL_TAG_ENTRIES);
|
|
1231
|
+
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
() => {
|
|
1236
|
+
state.pubsub.active = false;
|
|
1237
|
+
}
|
|
1238
|
+
);
|
|
1239
|
+
state.pubsub.active = true;
|
|
1240
|
+
state.pubsub.stop = async () => {
|
|
1241
|
+
state.pubsub.active = false;
|
|
1242
|
+
await stop();
|
|
1243
|
+
};
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
state.logger.warn("tagPubSub subscribe failed \u2014 will retry", {
|
|
1246
|
+
message: err.message
|
|
1247
|
+
});
|
|
1248
|
+
} finally {
|
|
1249
|
+
state.pubsub.attempting = false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1117
1252
|
async function refreshTagsImpl(state) {
|
|
1118
1253
|
if (!gateRedis(state)) {
|
|
1119
1254
|
return;
|
|
1120
1255
|
}
|
|
1256
|
+
void ensureTagSubscription(state);
|
|
1121
1257
|
try {
|
|
1122
1258
|
await withAbortSignal(
|
|
1123
1259
|
"cacheHandlers.refreshTags",
|
|
@@ -1275,6 +1411,16 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1275
1411
|
type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
1276
1412
|
meta: { count: tags.length, backend: "redis" }
|
|
1277
1413
|
});
|
|
1414
|
+
if (state.opts.tagPubSub) {
|
|
1415
|
+
try {
|
|
1416
|
+
const client = await state.conn.getOrConnect();
|
|
1417
|
+
await client?.publish?.(
|
|
1418
|
+
invalChannel(state),
|
|
1419
|
+
JSON.stringify({ t: tags, ts: now })
|
|
1420
|
+
);
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1278
1424
|
} catch (err) {
|
|
1279
1425
|
if (err instanceof CacheTimeoutError) {
|
|
1280
1426
|
state.emit({ type: "redis.timeout", meta: { op: "updateTags" } });
|