@leejpsd/nextjs-cache-handler 0.1.1 → 0.3.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 +117 -2
- package/README.md +131 -30
- package/dist/cache-components/index.cjs +297 -52
- 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 +289 -52
- package/dist/cache-components/index.js.map +1 -1
- package/dist/incremental/index.cjs +231 -41
- package/dist/incremental/index.cjs.map +1 -1
- package/dist/incremental/index.d.cts +42 -3
- package/dist/incremental/index.d.ts +42 -3
- package/dist/incremental/index.js +223 -41
- package/dist/incremental/index.js.map +1 -1
- package/dist/index.cjs +375 -76
- 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 +367 -76
- 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.cjs +72 -0
- package/dist/otel/index.cjs.map +1 -0
- package/dist/otel/index.d.cts +72 -0
- package/dist/otel/index.d.ts +72 -0
- package/dist/otel/index.js +64 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/shared/client/adapter-ioredis.cjs +49 -10
- package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
- package/dist/shared/client/adapter-ioredis.d.cts +5 -2
- package/dist/shared/client/adapter-ioredis.d.ts +5 -2
- package/dist/shared/client/adapter-ioredis.js +43 -11
- package/dist/shared/client/adapter-ioredis.js.map +1 -1
- package/dist/shared/client/adapter-redis.cjs +22 -7
- 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 +16 -7
- package/dist/shared/client/adapter-redis.js.map +1 -1
- package/dist/{types-CgriT7My.d.cts → types-Bl-rkLKX.d.cts} +59 -1
- package/dist/{types-CgriT7My.d.ts → types-Bl-rkLKX.d.ts} +59 -1
- package/package.json +23 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,128 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`dc1ac0a`](https://github.com/leejpsd/nextjs-cache-handler/commit/dc1ac0a0b29e73485f4af587f8ec0f4b68d1d54f) Thanks [@leejpsd](https://github.com/leejpsd)! - Reliability fixes (reconnect backoff instead of a permanent connect-failure
|
|
8
|
+
latch, bounded memory fallback, namespace-scoped tag propagation without
|
|
9
|
+
truncation, strict `fallback: "never"` honored by the ISR handler, working
|
|
10
|
+
ESM peer loading) plus new features: Next.js 15 ISR support, request-scoped
|
|
11
|
+
GET deduplication, transparent gzip/brotli value compression, Redis Sentinel
|
|
12
|
+
support, a built-in OpenTelemetry adapter at `/otel`, and `memoryMaxEntries`.
|
|
13
|
+
|
|
3
14
|
All notable changes to this project are documented here. The format is
|
|
4
15
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
5
16
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
17
|
|
|
7
18
|
## [Unreleased]
|
|
8
19
|
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Next.js 15 support** for the singular `cacheHandler` (ISR): both ctx
|
|
23
|
+
generations accepted (`ctx.revalidate`/`kindHint` and
|
|
24
|
+
`ctx.cacheControl`/`kind`); `peerDependencies.next` widened to
|
|
25
|
+
`>=15.0.0 <17`. Verified end to end against Next 15.5 (build, ISR
|
|
26
|
+
HIT/MISS, tag/path revalidation, fetch-cache TTL, cross-process reads).
|
|
27
|
+
- **Request-scoped GET deduplication** (singular handler):
|
|
28
|
+
`resetRequestCache()` is now a real implementation — duplicate reads of a
|
|
29
|
+
cache key within one request share a single Redis GET. New metric event
|
|
30
|
+
`cache.get.deduped`.
|
|
31
|
+
- **Transparent value compression** (`compression: "gzip" | "brotli"`,
|
|
32
|
+
default off) via node:zlib — marker-prefixed storage with auto-detecting
|
|
33
|
+
reads, safe to enable on a live cache with a mixed fleet. Payloads < 1 KiB
|
|
34
|
+
stay plain.
|
|
35
|
+
- **Redis Sentinel support** (`{ type: "sentinel", sentinels, name }`) via
|
|
36
|
+
ioredis master discovery — verified locally including a live failover.
|
|
37
|
+
- **Built-in OpenTelemetry adapter** at
|
|
38
|
+
`@leejpsd/nextjs-cache-handler/otel` (`createOtelMetricEmitter`);
|
|
39
|
+
`@opentelemetry/api` is an optional peer resolved at runtime.
|
|
40
|
+
- **`memoryMaxEntries` option** (default 1000) bounding the in-memory
|
|
41
|
+
fallback stores with LRU eviction.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **Connect failures no longer latch the process into memory-only mode.**
|
|
46
|
+
Reconnects retry with exponential backoff (1s→30s cap) and dropped
|
|
47
|
+
connections are replaced (dead clients disposed).
|
|
48
|
+
- **`refreshTags` scans only the current build namespace** and processes
|
|
49
|
+
markers chunk-by-chunk — the previous 500-key cap silently left tags
|
|
50
|
+
unpropagated; the local mirror is now rebuilt per complete scan (bounded
|
|
51
|
+
by live markers) and `memTagExp` is capped.
|
|
52
|
+
- **`fallback: "never"` is honored by the singular handler** (it previously
|
|
53
|
+
read/wrote the memory fallback unconditionally, silently degrading strict
|
|
54
|
+
mode to per-process caching).
|
|
55
|
+
- **ESM build can load optional peers**: bare `require()` calls became
|
|
56
|
+
esbuild's throwing `__require` fallback in ESM; now routed through
|
|
57
|
+
`createRequire` with a CWD fallback for symlinked installs (npm file
|
|
58
|
+
installs, pnpm).
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- Multi-tag `updateTags` fans out concurrently (one round trip via
|
|
63
|
+
auto-pipelining) instead of one await per tag; ioredis clients enable
|
|
64
|
+
`enableAutoPipelining`.
|
|
65
|
+
- Removed the declared-but-never-emitted `cache.stale.refresh.skipped`
|
|
66
|
+
metric event type.
|
|
67
|
+
|
|
68
|
+
## [0.2.0] - 2026-05-10
|
|
69
|
+
|
|
70
|
+
First minor bump. Three differentiators land in this release.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **Single-flight refresh lock** (`singleFlight: true`,
|
|
75
|
+
`singleFlightLockTtlSec: 10`). Opt-in stampede protection at the SWR
|
|
76
|
+
boundary. Uses the bundled `refresh-tag-lock.lua` script over a Redis
|
|
77
|
+
SETNX-style lock; only the leader's stale read triggers the
|
|
78
|
+
background refresh, all others fall through to the follower path.
|
|
79
|
+
Two new `MetricEvent` types (`cache.stale.refresh.leader`,
|
|
80
|
+
`cache.stale.refresh.follower`) appear in `onMetric` so operators can
|
|
81
|
+
verify leadership balance. Lock-acquisition failures are best-effort
|
|
82
|
+
and always degrade to the safe follower path; the stale entry is
|
|
83
|
+
served regardless.
|
|
84
|
+
- **OpenTelemetry reference adapter** at
|
|
85
|
+
[`examples/opentelemetry/`](./examples/opentelemetry/). The library
|
|
86
|
+
itself stays dependency-free; the example shows the smallest viable
|
|
87
|
+
wiring of `onMetric` to OTel counters and histograms with bounded
|
|
88
|
+
cardinality (no cache keys or tag names emitted as attributes).
|
|
89
|
+
- **Integration tests against real Redis 7** (21 scenarios) covering
|
|
90
|
+
both `redis@5` and `ioredis` adapters. Each adapter runs the same
|
|
91
|
+
test grid so a regression in either client library is pinpointed
|
|
92
|
+
immediately. New scripts: `npm run test:integration`,
|
|
93
|
+
`npm run test:integration:up` / `:down` for the docker-compose
|
|
94
|
+
fixture.
|
|
95
|
+
- **CI integration job** runs the integration suite against an
|
|
96
|
+
ephemeral Redis 7 service container on every PR.
|
|
97
|
+
- **GitHub Actions OIDC publish path** in `.github/workflows/release.yml`
|
|
98
|
+
with `id-token: write` and `NPM_CONFIG_PROVENANCE=true`. Currently
|
|
99
|
+
scoped to `workflow_dispatch` trigger so the first GHA-driven publish
|
|
100
|
+
can be observed; flip to `push: main` once the changesets cadence
|
|
101
|
+
stabilizes. Tarballs published from the workflow will land on npm
|
|
102
|
+
with verified provenance.
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
|
|
106
|
+
- `MetricEventType` union grew three new entries:
|
|
107
|
+
`cache.stale.refresh.leader`, `cache.stale.refresh.follower`,
|
|
108
|
+
`cache.stale.refresh.skipped`. The legacy `cache.stale` event still
|
|
109
|
+
fires when `singleFlight` is off (default).
|
|
110
|
+
- `HandlerState` now carries an `instanceId` (read from `HOSTNAME` /
|
|
111
|
+
`ECS_TASK_ID` / `pid-<pid>`) and embeds it in every refresh-lock
|
|
112
|
+
acquisition for operator-side observability.
|
|
113
|
+
|
|
114
|
+
### Compatibility
|
|
115
|
+
|
|
116
|
+
- No breaking changes. All existing 0.1.x configurations continue to
|
|
117
|
+
work; `singleFlight` defaults to `false`.
|
|
118
|
+
|
|
119
|
+
### Verified
|
|
120
|
+
|
|
121
|
+
- 72 unit tests (8 files) + 21 integration tests (1 file × 21 scenarios)
|
|
122
|
+
all green.
|
|
123
|
+
- arethetypeswrong: 28/28 cells green.
|
|
124
|
+
- publint: clean.
|
|
125
|
+
|
|
9
126
|
## [0.1.1] - 2026-05-10
|
|
10
127
|
|
|
11
128
|
Documentation-only patch released alongside the first round of external
|
|
@@ -59,8 +176,6 @@ runtime cache key shapes.
|
|
|
59
176
|
- Provenance attestation is disabled for the local-publish flow; v0.2
|
|
60
177
|
will switch to GitHub Actions OIDC publish
|
|
61
178
|
|
|
62
|
-
|
|
63
|
-
|
|
64
179
|
### Added — `cacheHandlers` (plural) for `'use cache'`
|
|
65
180
|
|
|
66
181
|
- `createCacheComponentsHandler(opts)` factory implementing the full
|
package/README.md
CHANGED
|
@@ -3,12 +3,20 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@leejpsd/nextjs-cache-handler)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> **`v0.3.0`** — install with
|
|
7
7
|
> `npm install @leejpsd/nextjs-cache-handler`. Production-validated against
|
|
8
|
-
> AWS ECS Fargate with multi-instance Redis (24h live-traffic soak
|
|
9
|
-
>
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
> AWS ECS Fargate with multi-instance Redis (v0.1: 24h live-traffic soak;
|
|
9
|
+
> v0.3: fresh multi-instance verification incl. a live Redis-reboot drill
|
|
10
|
+
> with zero 5xx — see
|
|
11
|
+
> [`docs/staging-verification-2026-08-01.md`](./docs/staging-verification-2026-08-01.md)).
|
|
12
|
+
>
|
|
13
|
+
> v0.3 adds: **Next.js 15 support** (ISR handler), **reconnect with
|
|
14
|
+
> exponential backoff** (no more permanent memory-only latch), **request-scoped
|
|
15
|
+
> GET deduplication**, transparent **gzip/brotli compression**, **Redis
|
|
16
|
+
> Sentinel** support, a built-in **OpenTelemetry emitter** at `/otel`, and an
|
|
17
|
+
> LRU-bounded memory fallback.
|
|
18
|
+
|
|
19
|
+
The Redis cache handler for **Next.js 15/16** that ships **both** `cacheHandler`
|
|
12
20
|
(ISR / Pages Router) **and** `cacheHandlers` (`'use cache'` directive,
|
|
13
21
|
`cacheComponents: true`) — the area where
|
|
14
22
|
[`@fortedigital/nextjs-cache-handler`](https://github.com/fortedigital/nextjs-cache-handler)
|
|
@@ -49,18 +57,19 @@ As of 2026-05, the leading OSS Redis handler `@fortedigital/nextjs-cache-handler
|
|
|
49
57
|
declares `peerDependencies.next: ">=16.1.5"` but the README marks the new
|
|
50
58
|
plural interface as ❌ **"Not yet supported - Help needed"**:
|
|
51
59
|
|
|
52
|
-
> 📅 **Compatibility matrix verified 2026-
|
|
60
|
+
> 📅 **Compatibility matrix re-verified 2026-07-31** (from each project's
|
|
61
|
+
> published README/registry metadata). The OSS Next.js cache
|
|
53
62
|
> handler ecosystem moves quickly — please verify
|
|
54
63
|
> [`@fortedigital`](https://github.com/fortedigital/nextjs-cache-handler#compatibility)
|
|
55
64
|
> and
|
|
56
65
|
> [`nextjs-turbo-redis-cache`](https://github.com/trieb-work/nextjs-turbo-redis-cache#features)
|
|
57
66
|
> directly before relying on this comparison.
|
|
58
67
|
|
|
59
|
-
|
|
|
68
|
+
| Feature | this (0.3.0) | @fortedigital 3.2.1 | nextjs-turbo-redis-cache 1.15 |
|
|
60
69
|
|---|---|---|---|
|
|
61
70
|
| `cacheHandlers` config (plural) | ✅ | ❌ Help needed | ✅ since 1.11 |
|
|
62
71
|
| `'use cache'` directive | ✅ | ❌ Help needed | ✅ since 1.11 |
|
|
63
|
-
| `'use cache: remote'` | ✅ | ❌ Help needed | partial |
|
|
72
|
+
| `'use cache: remote'` | ✅ default handler (dedicated multi-tier: roadmap) | ❌ Help needed | partial |
|
|
64
73
|
| `'use cache: private'` | n/a (uncustomizable) | n/a | n/a |
|
|
65
74
|
| `cacheComponents: true` | ✅ | ❌ Help needed | ✅ |
|
|
66
75
|
| Build-phase skip (`PHASE_PRODUCTION_BUILD`) | ✅ | ✅ (singular only) | ✅ |
|
|
@@ -70,13 +79,17 @@ plural interface as ❌ **"Not yet supported - Help needed"**:
|
|
|
70
79
|
| Redis Cluster | ✅ (cluster adapter, see Production checklist) | ✅ | ✅ |
|
|
71
80
|
| ioredis support | ✅ | ✅ | ✅ |
|
|
72
81
|
| In-memory fallback (TTL-aware) | ✅ | partial | ✅ L1 + Redis L2 |
|
|
73
|
-
|
|
|
82
|
+
| Next 15 support (ISR handler) | ✅ `>=15.0.0` | ✅ (legacy 2.x line) | ✅ `>=15.0.3` |
|
|
83
|
+
| Request-scoped GET dedup | ✅ | ❌ | ✅ |
|
|
84
|
+
| Built-in value compression | ✅ gzip/brotli option | example only | example only |
|
|
85
|
+
| Redis Sentinel | ✅ (local failover drill) | ❌ | ❌ |
|
|
86
|
+
| OpenTelemetry | ✅ built-in `/otel` emitter + `onMetric` hook | ❌ | ❌ |
|
|
87
|
+
| Reconnect strategy | ✅ exponential backoff | client-level | error-threshold restart |
|
|
74
88
|
| Live-traffic dogfood (24h+) | ✅ AWS ECS Fargate | not published | not published |
|
|
75
89
|
|
|
76
90
|
PR [#207](https://github.com/fortedigital/nextjs-cache-handler/pull/207) on
|
|
77
|
-
`@fortedigital`
|
|
78
|
-
|
|
79
|
-
package has from the start.
|
|
91
|
+
`@fortedigital` (their `cacheHandlers` attempt) was held up in review over
|
|
92
|
+
`PHASE_PRODUCTION_BUILD` handling — which this package has from the start.
|
|
80
93
|
|
|
81
94
|
---
|
|
82
95
|
|
|
@@ -171,13 +184,17 @@ export default async function Page() {
|
|
|
171
184
|
```ts
|
|
172
185
|
interface CacheHandlerOptions {
|
|
173
186
|
client: RedisClientFactory | RedisClientConfig;
|
|
174
|
-
keyPrefix?: string;
|
|
187
|
+
keyPrefix?: string; // default: "next-cache:" / "next-incremental:"
|
|
175
188
|
buildNamespace?: string | (() => string); // default: env DEPLOYMENT_VERSION || GIT_HASH || "unversioned"
|
|
176
|
-
abortTimeoutMs?: number;
|
|
189
|
+
abortTimeoutMs?: number; // default: 1500
|
|
177
190
|
fallback?: "auto" | "always" | "never"; // default: "auto"
|
|
178
191
|
staleWhileRevalidate?: boolean; // default: true (cache-components only)
|
|
179
|
-
|
|
180
|
-
|
|
192
|
+
singleFlight?: boolean; // default: false — see "Single-flight refresh lock" below
|
|
193
|
+
singleFlightLockTtlSec?: number; // default: 10
|
|
194
|
+
isBuildPhase?: () => boolean; // override PHASE_PRODUCTION_BUILD detection
|
|
195
|
+
hashTag?: boolean; // default: false (set true on Redis Cluster)
|
|
196
|
+
memoryMaxEntries?: number; // default: 1000 — LRU cap for the in-memory fallback
|
|
197
|
+
compression?: "gzip" | "brotli"; // default: off — transparent value compression (node:zlib)
|
|
181
198
|
onMetric?: (event: MetricEvent) => void;
|
|
182
199
|
logger?: Logger;
|
|
183
200
|
}
|
|
@@ -185,10 +202,12 @@ interface CacheHandlerOptions {
|
|
|
185
202
|
type RedisClientConfig =
|
|
186
203
|
| { type: "redis"; url: string; password?: string; tls?: boolean; connectTimeout?: number }
|
|
187
204
|
| { type: "ioredis"; url: string; password?: string; tls?: boolean; connectTimeout?: number }
|
|
188
|
-
| { type: "cluster"; nodes: { host: string; port: number }[]; password?: string; tls?: boolean }
|
|
205
|
+
| { type: "cluster"; nodes: { host: string; port: number }[]; password?: string; tls?: boolean }
|
|
206
|
+
| { type: "sentinel"; sentinels: { host: string; port: number }[]; name: string;
|
|
207
|
+
password?: string; sentinelPassword?: string; tls?: boolean; connectTimeout?: number };
|
|
189
208
|
```
|
|
190
209
|
|
|
191
|
-
Full reference: [`docs/api.md`](./docs/api.md)
|
|
210
|
+
Full reference: [`docs/api.md`](./docs/api.md).
|
|
192
211
|
|
|
193
212
|
---
|
|
194
213
|
|
|
@@ -227,12 +246,82 @@ Full reference: [`docs/api.md`](./docs/api.md) (auto-generated).
|
|
|
227
246
|
| **Self-hosted Redis 7+** | `{ type: "redis", url }` or `{ type: "ioredis", url }` | ✅ AWS ElastiCache 24h soak |
|
|
228
247
|
| **Redis Cluster** | `{ type: "cluster", nodes }` + `hashTag: true` | unit-tested, not yet load-tested at scale |
|
|
229
248
|
| **Upstash Redis** | `{ type: "redis", url: "rediss://..." }` (TLS auto-detected) | not yet validated, expected to work via the standard Redis protocol |
|
|
230
|
-
| **AWS ElastiCache (replication group)** | `{ type: "redis", url: "rediss://..." }` | ✅ reference deployment |
|
|
231
|
-
| **
|
|
249
|
+
| **AWS ElastiCache (replication group)** | `{ type: "redis", url: "rediss://..." }` | ✅ reference deployment (re-verified 2026-08-01, Seoul) |
|
|
250
|
+
| **Redis Sentinel** | `{ type: "sentinel", sentinels, name }` | ✅ local master/replica failover drill |
|
|
251
|
+
| **Vercel KV** | not yet supported — dedicated adapter on the roadmap | — |
|
|
232
252
|
| **DragonflyDB / KeyDB** | Redis-protocol compatible — `{ type: "redis", url }` should work | not validated |
|
|
233
253
|
|
|
234
254
|
---
|
|
235
255
|
|
|
256
|
+
## Single-flight refresh lock (v0.2)
|
|
257
|
+
|
|
258
|
+
The `cacheHandlers` (plural) interface returns stale entries inside the
|
|
259
|
+
SWR window so users get an instant response while the background
|
|
260
|
+
refresh completes. With many instances, the moment an entry crosses the
|
|
261
|
+
`revalidate` boundary, every instance independently triggers its own
|
|
262
|
+
refresh — N parallel re-renders for the same key, each hitting your
|
|
263
|
+
origin once.
|
|
264
|
+
|
|
265
|
+
`singleFlight: true` adds an opt-in Redis lock (`refresh-tag-lock.lua`,
|
|
266
|
+
default TTL 10s) at the SWR boundary. The first instance to acquire it
|
|
267
|
+
becomes the **leader** and runs the refresh; the rest become
|
|
268
|
+
**followers**, keep serving the same stale entry, and wait for the
|
|
269
|
+
leader's write to land. The lock is observability-only at the handler
|
|
270
|
+
layer — Next.js still drives the actual refresh; we just suppress the
|
|
271
|
+
stampede.
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
createCacheComponentsHandler({
|
|
275
|
+
client: { type: "redis", url: process.env.REDIS_URL },
|
|
276
|
+
singleFlight: true, // default false
|
|
277
|
+
singleFlightLockTtlSec: 10, // default 10
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Two new `MetricEvent` types appear on `onMetric`:
|
|
282
|
+
|
|
283
|
+
| event type | meaning |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `cache.stale.refresh.leader` | this instance just acquired the lock and is the designated refresher |
|
|
286
|
+
| `cache.stale.refresh.follower` | another instance holds the lock; we serve stale and skip the refresh |
|
|
287
|
+
|
|
288
|
+
If lock acquisition fails (Redis hiccup, TTL race), the handler
|
|
289
|
+
defaults to the follower path — the stale entry is always served, never
|
|
290
|
+
dropped. This is intentional: the lock is an optimization, not a
|
|
291
|
+
correctness-critical primitive.
|
|
292
|
+
|
|
293
|
+
When **not** to enable single-flight: small fleets (1–2 instances) where
|
|
294
|
+
Next's per-process serialization already covers the stampede risk.
|
|
295
|
+
Adding a Redis round-trip per stale read isn't free.
|
|
296
|
+
|
|
297
|
+
See [`docs/architecture.md`](./docs/architecture.md#single-flight-refresh-lock-v02-)
|
|
298
|
+
for the full state machine and a reference to the Lua script body.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## OpenTelemetry instrumentation (v0.2)
|
|
303
|
+
|
|
304
|
+
The handler doesn't bundle `@opentelemetry/api` (zero runtime
|
|
305
|
+
dependencies stays a goal). Instead, the `onMetric(event)` hook gives
|
|
306
|
+
strictly-typed events you can pipe into whatever observability stack
|
|
307
|
+
you already run.
|
|
308
|
+
|
|
309
|
+
[`examples/opentelemetry/`](./examples/opentelemetry/) is a copy-paste
|
|
310
|
+
reference wrapper that:
|
|
311
|
+
|
|
312
|
+
- exposes a `nextjs_cache.events_total` counter dimensioned on
|
|
313
|
+
`type` / `freshness` / `backend` / `reason` / `op`
|
|
314
|
+
- exposes a `nextjs_cache.op_latency_ms` histogram for events that
|
|
315
|
+
carry an `ms` field
|
|
316
|
+
- keeps cardinality bounded — cache keys and tag names are never
|
|
317
|
+
emitted as attributes
|
|
318
|
+
|
|
319
|
+
See [`examples/opentelemetry/README.md`](./examples/opentelemetry/README.md)
|
|
320
|
+
for setup and three suggested dashboards (hit rate, single-flight
|
|
321
|
+
leadership distribution, op latency tails).
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
236
325
|
## How it differs from `@fortedigital/nextjs-cache-handler`
|
|
237
326
|
|
|
238
327
|
Three deliberate departures, all rooted in lessons from production
|
|
@@ -261,10 +350,16 @@ eventual implementation should converge on the same behavior.
|
|
|
261
350
|
|
|
262
351
|
## Compatibility
|
|
263
352
|
|
|
264
|
-
- **Next.js**: `>=
|
|
353
|
+
- **Next.js**: `>=15.0.0 <17`
|
|
354
|
+
- `cacheHandler` (singular, ISR): Next **15 and 16** — the handler accepts
|
|
355
|
+
both ctx shapes (Next 15's `ctx.revalidate` / `kindHint`, Next 16's
|
|
356
|
+
`ctx.cacheControl` / `kind`)
|
|
357
|
+
- `cacheHandlers` (plural, `'use cache'`): Next **>=16.1.5** only — the
|
|
358
|
+
interface does not exist before 16
|
|
265
359
|
- **Node.js**: `>=20`
|
|
266
360
|
- **redis**: `>=5.0.0` (peer, optional)
|
|
267
|
-
- **ioredis**: `>=5.0.0` (peer, optional)
|
|
361
|
+
- **ioredis**: `>=5.0.0` (peer, optional) — also powers `type: "cluster"` and
|
|
362
|
+
`type: "sentinel"` (Sentinel master discovery with automatic failover)
|
|
268
363
|
|
|
269
364
|
ESM and CJS dual-published, full TypeScript types, validated via
|
|
270
365
|
[`arethetypeswrong`](https://arethetypeswrong.github.io) and
|
|
@@ -274,14 +369,20 @@ ESM and CJS dual-published, full TypeScript types, validated via
|
|
|
274
369
|
|
|
275
370
|
## Roadmap
|
|
276
371
|
|
|
277
|
-
- **v0.1.0** *(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
- **v0.2.0** — Single-flight refresh lock
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
- **v0.
|
|
372
|
+
- **v0.1.0** *(2026-05)* — Both handlers, SWR, Lua atomicity, build-phase
|
|
373
|
+
skip, `redis@5` + `ioredis` adapters, AbortSignal, in-memory fallback
|
|
374
|
+
with TTL, soft-tag freshness check ✅
|
|
375
|
+
- **v0.2.0** *(2026-05)* — Single-flight refresh lock with leader/follower
|
|
376
|
+
metrics, OpenTelemetry reference adapter, integration tests against
|
|
377
|
+
real Redis 7 (21 scenarios over `redis@5` + `ioredis`), GitHub Actions
|
|
378
|
+
OIDC publish path with provenance attestation ✅
|
|
379
|
+
- **v0.3.0** *(2026-08)* — Next.js 15 ISR support, reconnect backoff (no
|
|
380
|
+
permanent connect-failure latch), request-scoped GET deduplication,
|
|
381
|
+
gzip/brotli compression, Redis Sentinel, built-in OpenTelemetry emitter
|
|
382
|
+
(`/otel`), LRU-bounded memory fallback, ESM peer-loading fixes ✅
|
|
383
|
+
- **v0.4.0** — Build-phase cache seeding (prepopulate prebuilt pages),
|
|
384
|
+
pub/sub-based tag propagation, Redis Cluster e2e validation, Vercel KV /
|
|
385
|
+
Upstash adapter, `'use cache: remote'` multi-tier setup
|
|
285
386
|
|
|
286
387
|
---
|
|
287
388
|
|