@leejpsd/nextjs-cache-handler 0.1.0 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { d as MetricEventType } from '../types-CgriT7My.cjs';
1
+ import { d as MetricEventType } from '../types-Dq1q6qXd.cjs';
2
2
 
3
3
  /**
4
4
  * Process-local metric counters with an optional emit hook.
@@ -1,4 +1,4 @@
1
- import { d as MetricEventType } from '../types-CgriT7My.js';
1
+ import { d as MetricEventType } from '../types-Dq1q6qXd.js';
2
2
 
3
3
  /**
4
4
  * Process-local metric counters with an optional emit hook.
@@ -1,5 +1,5 @@
1
1
  import { Cluster, Redis } from 'ioredis';
2
- import { R as RedisClientLike, a as RedisClientConfig } from '../../types-CgriT7My.cjs';
2
+ import { R as RedisClientLike, a as RedisClientConfig } from '../../types-Dq1q6qXd.cjs';
3
3
 
4
4
  /**
5
5
  * Adapter for `ioredis` (and `ioredis.Cluster`).
@@ -1,5 +1,5 @@
1
1
  import { Cluster, Redis } from 'ioredis';
2
- import { R as RedisClientLike, a as RedisClientConfig } from '../../types-CgriT7My.js';
2
+ import { R as RedisClientLike, a as RedisClientConfig } from '../../types-Dq1q6qXd.js';
3
3
 
4
4
  /**
5
5
  * Adapter for `ioredis` (and `ioredis.Cluster`).
@@ -1,5 +1,5 @@
1
1
  import { RedisClientType } from 'redis';
2
- import { R as RedisClientLike, a as RedisClientConfig } from '../../types-CgriT7My.cjs';
2
+ import { R as RedisClientLike, a as RedisClientConfig } from '../../types-Dq1q6qXd.cjs';
3
3
 
4
4
  /**
5
5
  * Adapter for `redis@5` (the official `node-redis` client).
@@ -1,5 +1,5 @@
1
1
  import { RedisClientType } from 'redis';
2
- import { R as RedisClientLike, a as RedisClientConfig } from '../../types-CgriT7My.js';
2
+ import { R as RedisClientLike, a as RedisClientConfig } from '../../types-Dq1q6qXd.js';
3
3
 
4
4
  /**
5
5
  * Adapter for `redis@5` (the official `node-redis` client).
@@ -71,7 +71,7 @@ type FallbackStrategy = "auto" | "always" | "never";
71
71
  * Optional — when not provided, only the process-local counters in
72
72
  * `src/shared/metrics.ts` are updated.
73
73
  */
74
- type MetricEventType = "cache.hit" | "cache.miss" | "cache.stale" | "cache.set" | "cache.set.failed" | "tag.invalidate.soft" | "tag.invalidate.hard" | "tag.expiration.read" | "redis.connect.failed" | "redis.timeout" | "fallback.activated" | "build_phase.skip";
74
+ type MetricEventType = "cache.hit" | "cache.miss" | "cache.stale" | "cache.stale.refresh.leader" | "cache.stale.refresh.follower" | "cache.stale.refresh.skipped" | "cache.set" | "cache.set.failed" | "tag.invalidate.soft" | "tag.invalidate.hard" | "tag.expiration.read" | "redis.connect.failed" | "redis.timeout" | "fallback.activated" | "build_phase.skip";
75
75
  interface MetricEvent {
76
76
  type: MetricEventType;
77
77
  /** Operation latency in milliseconds, when measurable. */
@@ -141,6 +141,28 @@ interface CacheHandlerOptions {
141
141
  * Default: `true`. Set to `false` to revert to "expire on revalidate" behavior.
142
142
  */
143
143
  staleWhileRevalidate?: boolean;
144
+ /**
145
+ * Single-flight refresh lock for the SWR boundary.
146
+ *
147
+ * When enabled, only one handler instance per cache key attempts a
148
+ * background refresh during the stale window. Other instances continue
149
+ * serving the same stale entry until the leader publishes a fresh one or
150
+ * the lock TTL expires. Prevents N parallel refreshes (cache stampede)
151
+ * when many instances cross the `revalidate` boundary at the same time.
152
+ *
153
+ * Default: `false`. The marginal benefit at small scale is small because
154
+ * Next.js already serializes per-cacheKey refresh per process; turn this
155
+ * on when you have many instances and observe duplicate origin work
156
+ * around the revalidate boundary.
157
+ */
158
+ singleFlight?: boolean;
159
+ /**
160
+ * Lock TTL in seconds for `singleFlight` mode. Default: 10.
161
+ *
162
+ * Long enough that a normal refresh can complete (most are sub-second),
163
+ * short enough that a crashed leader doesn't block followers for long.
164
+ */
165
+ singleFlightLockTtlSec?: number;
144
166
  /**
145
167
  * Override the build-phase detector. Default checks
146
168
  * `process.env.NEXT_PHASE === "phase-production-build"`.
@@ -71,7 +71,7 @@ type FallbackStrategy = "auto" | "always" | "never";
71
71
  * Optional — when not provided, only the process-local counters in
72
72
  * `src/shared/metrics.ts` are updated.
73
73
  */
74
- type MetricEventType = "cache.hit" | "cache.miss" | "cache.stale" | "cache.set" | "cache.set.failed" | "tag.invalidate.soft" | "tag.invalidate.hard" | "tag.expiration.read" | "redis.connect.failed" | "redis.timeout" | "fallback.activated" | "build_phase.skip";
74
+ type MetricEventType = "cache.hit" | "cache.miss" | "cache.stale" | "cache.stale.refresh.leader" | "cache.stale.refresh.follower" | "cache.stale.refresh.skipped" | "cache.set" | "cache.set.failed" | "tag.invalidate.soft" | "tag.invalidate.hard" | "tag.expiration.read" | "redis.connect.failed" | "redis.timeout" | "fallback.activated" | "build_phase.skip";
75
75
  interface MetricEvent {
76
76
  type: MetricEventType;
77
77
  /** Operation latency in milliseconds, when measurable. */
@@ -141,6 +141,28 @@ interface CacheHandlerOptions {
141
141
  * Default: `true`. Set to `false` to revert to "expire on revalidate" behavior.
142
142
  */
143
143
  staleWhileRevalidate?: boolean;
144
+ /**
145
+ * Single-flight refresh lock for the SWR boundary.
146
+ *
147
+ * When enabled, only one handler instance per cache key attempts a
148
+ * background refresh during the stale window. Other instances continue
149
+ * serving the same stale entry until the leader publishes a fresh one or
150
+ * the lock TTL expires. Prevents N parallel refreshes (cache stampede)
151
+ * when many instances cross the `revalidate` boundary at the same time.
152
+ *
153
+ * Default: `false`. The marginal benefit at small scale is small because
154
+ * Next.js already serializes per-cacheKey refresh per process; turn this
155
+ * on when you have many instances and observe duplicate origin work
156
+ * around the revalidate boundary.
157
+ */
158
+ singleFlight?: boolean;
159
+ /**
160
+ * Lock TTL in seconds for `singleFlight` mode. Default: 10.
161
+ *
162
+ * Long enough that a normal refresh can complete (most are sub-second),
163
+ * short enough that a crashed leader doesn't block followers for long.
164
+ */
165
+ singleFlightLockTtlSec?: number;
144
166
  /**
145
167
  * Override the build-phase detector. Default checks
146
168
  * `process.env.NEXT_PHASE === "phase-production-build"`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leejpsd/nextjs-cache-handler",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The only Redis cache handler for Next.js 16 that ships both cacheHandler (ISR) and cacheHandlers ('use cache') — production-validated for multi-instance deployments where @fortedigital lists 'Help needed'.",
5
5
  "keywords": [
6
6
  "next",
@@ -127,7 +127,9 @@
127
127
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
128
128
  "test": "vitest run",
129
129
  "test:unit": "vitest run tests/unit",
130
- "test:integration": "vitest run tests/integration",
130
+ "test:integration": "vitest run --config vitest.integration.config.ts",
131
+ "test:integration:up": "docker compose -f docker-compose.test.yml up -d --wait",
132
+ "test:integration:down": "docker compose -f docker-compose.test.yml down",
131
133
  "test:watch": "vitest",
132
134
  "test:coverage": "vitest run --coverage",
133
135
  "format": "prettier --write \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",