@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +117 -2
  2. package/README.md +131 -30
  3. package/dist/cache-components/index.cjs +297 -52
  4. package/dist/cache-components/index.cjs.map +1 -1
  5. package/dist/cache-components/index.d.cts +1 -1
  6. package/dist/cache-components/index.d.ts +1 -1
  7. package/dist/cache-components/index.js +289 -52
  8. package/dist/cache-components/index.js.map +1 -1
  9. package/dist/incremental/index.cjs +231 -41
  10. package/dist/incremental/index.cjs.map +1 -1
  11. package/dist/incremental/index.d.cts +42 -3
  12. package/dist/incremental/index.d.ts +42 -3
  13. package/dist/incremental/index.js +223 -41
  14. package/dist/incremental/index.js.map +1 -1
  15. package/dist/index.cjs +375 -76
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +367 -76
  20. package/dist/index.js.map +1 -1
  21. package/dist/ops/index.d.cts +1 -1
  22. package/dist/ops/index.d.ts +1 -1
  23. package/dist/otel/index.cjs +72 -0
  24. package/dist/otel/index.cjs.map +1 -0
  25. package/dist/otel/index.d.cts +72 -0
  26. package/dist/otel/index.d.ts +72 -0
  27. package/dist/otel/index.js +64 -0
  28. package/dist/otel/index.js.map +1 -0
  29. package/dist/shared/client/adapter-ioredis.cjs +49 -10
  30. package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
  31. package/dist/shared/client/adapter-ioredis.d.cts +5 -2
  32. package/dist/shared/client/adapter-ioredis.d.ts +5 -2
  33. package/dist/shared/client/adapter-ioredis.js +43 -11
  34. package/dist/shared/client/adapter-ioredis.js.map +1 -1
  35. package/dist/shared/client/adapter-redis.cjs +22 -7
  36. package/dist/shared/client/adapter-redis.cjs.map +1 -1
  37. package/dist/shared/client/adapter-redis.d.cts +1 -1
  38. package/dist/shared/client/adapter-redis.d.ts +1 -1
  39. package/dist/shared/client/adapter-redis.js +16 -7
  40. package/dist/shared/client/adapter-redis.js.map +1 -1
  41. package/dist/{types-CgriT7My.d.cts → types-Bl-rkLKX.d.cts} +59 -1
  42. package/dist/{types-CgriT7My.d.ts → types-Bl-rkLKX.d.ts} +59 -1
  43. 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
  [![npm](https://img.shields.io/npm/v/@leejpsd/nextjs-cache-handler.svg)](https://www.npmjs.com/package/@leejpsd/nextjs-cache-handler)
4
4
  [![license](https://img.shields.io/npm/l/@leejpsd/nextjs-cache-handler.svg)](./LICENSE)
5
5
 
6
- > **Stable `v0.1.0`** — install with
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: 0
9
- > errors, 0 leaks, 2ms Redis ping, namespace isolation working).
10
-
11
- The Redis cache handler for **Next.js 16** that ships **both** `cacheHandler`
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-05-10.** The OSS Next.js cache
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
- | Next 16 feature | this | @fortedigital 3.2.0 | nextjs-turbo-redis-cache 1.13 |
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
- | OpenTelemetry hook | ✅ `onMetric` | ❌ | ❌ |
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` has been stalled for 3+ months on the same issue: the
78
- upstream review insisted on `PHASE_PRODUCTION_BUILD` handling, which this
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; // default: "next-cache:" / "next-incremental:"
187
+ keyPrefix?: string; // default: "next-cache:" / "next-incremental:"
175
188
  buildNamespace?: string | (() => string); // default: env DEPLOYMENT_VERSION || GIT_HASH || "unversioned"
176
- abortTimeoutMs?: number; // default: 1500
189
+ abortTimeoutMs?: number; // default: 1500
177
190
  fallback?: "auto" | "always" | "never"; // default: "auto"
178
191
  staleWhileRevalidate?: boolean; // default: true (cache-components only)
179
- isBuildPhase?: () => boolean; // override PHASE_PRODUCTION_BUILD detection
180
- hashTag?: boolean; // default: false (set true on Redis Cluster)
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) (auto-generated).
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
- | **Vercel KV** | not yet supported — `@vercel/kv` adapter ships in v0.3 | — |
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**: `>=16.1.5 <17`
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** *(target: 2026-05)* — Both handlers, SWR, Lua atomicity,
278
- build-phase skip, `redis@5` + `ioredis` adapters, AbortSignal,
279
- in-memory fallback with TTL, soft-tag freshness check
280
- - **v0.2.0** — Single-flight refresh lock (Lua SETNX), OpenTelemetry
281
- forwarder example
282
- - **v0.3.0** — Vercel KV / Upstash Redis adapter, `'use cache: remote'`
283
- multi-tier setup
284
- - **v0.4.0** — Cache stampede protection, request-scoped memoization
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