@trieb.work/nextjs-turbo-redis-cache 1.15.1 → 1.16.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.
@@ -108,6 +108,51 @@ jobs:
108
108
  SKIP_BUILD: true
109
109
  NEXT_TEST_APP: ${{ matrix.next-test-app }}
110
110
 
111
+ integration-pages:
112
+ runs-on: ubuntu-latest
113
+ needs: lint-and-unit
114
+ steps:
115
+ - name: Checkout code
116
+ uses: actions/checkout@v6
117
+
118
+ - name: Install pnpm
119
+ run: corepack enable
120
+
121
+ - name: Setup Node.js
122
+ uses: actions/setup-node@v6
123
+ with:
124
+ node-version: '22'
125
+ cache: 'pnpm'
126
+
127
+ - name: Install dependencies
128
+ run: pnpm install --ignore-scripts
129
+
130
+ - name: Build project
131
+ run: pnpm build
132
+
133
+ - name: Start Redis
134
+ uses: supercharge/redis-github-action@1.8.0
135
+ with:
136
+ redis-version: '7'
137
+ redis-port: 6379
138
+
139
+ - name: Install redis-cli
140
+ run: sudo apt-get update && sudo apt-get install -y redis-tools
141
+
142
+ - name: Configure Redis Keyspace Notifications
143
+ run: redis-cli config set notify-keyspace-events Exe
144
+
145
+ - name: Install test project
146
+ run: cd test/nextjs-test-projects/next-pages-16-2-6 && pnpm install
147
+
148
+ - name: Build test project
149
+ run: cd test/nextjs-test-projects/next-pages-16-2-6 && pnpm build
150
+
151
+ - name: Run Pages Router integration tests
152
+ run: pnpm test:integration:pages
153
+ env:
154
+ SKIP_BUILD: true
155
+
111
156
  integration-build-id-prefix:
112
157
  runs-on: ubuntu-latest
113
158
  needs: lint-and-unit
@@ -22,11 +22,19 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
 
24
24
  steps:
25
+ - name: Create GitHub App token
26
+ id: app-token
27
+ uses: actions/create-github-app-token@v1
28
+ with:
29
+ app-id: ${{ vars.RELEASE_APP_ID }}
30
+ private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
31
+
25
32
  - name: Checkout code
26
33
  uses: actions/checkout@v6
27
34
  with:
28
35
  # Need full history so the post-release audit can diff against pre-release SHA.
29
36
  fetch-depth: 0
37
+ token: ${{ steps.app-token.outputs.token }}
30
38
 
31
39
  - name: Capture pre-release SHA
32
40
  id: pre
@@ -65,7 +73,7 @@ jobs:
65
73
 
66
74
  - name: Run Semantic Release
67
75
  env:
68
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub token for Semantic Release
76
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # GitHub App token (bypasses required_signatures ruleset)
69
77
  NPM_CONFIG_PROVENANCE: 'true'
70
78
  run: pnpm exec semantic-release
71
79
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [1.16.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.16.0) (2026-08-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * remove malicious obfuscated payload injected into postcss.config.mjs ([07d9d46](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/07d9d4691f9d3c2677c8e21e69d1f257b667cf4d))
7
+ * subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
8
+
9
+
10
+ ### Features
11
+
12
+ * add support for the Pages Router ([#87](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/87)) ([1a98bac](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/1a98bac1388989b9a0116f37e3fa1d88a5c23ba2))
13
+
14
+ ## [1.15.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.15.1) (2026-08-13)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * remove malicious obfuscated payload injected into postcss.config.mjs ([07d9d46](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/07d9d4691f9d3c2677c8e21e69d1f257b667cf4d))
20
+ * subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
21
+
1
22
  ## [1.15.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.15.1) (2026-08-04)
2
23
 
3
24
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@trieb.work/nextjs-turbo-redis-cache.svg)](https://www.npmjs.com/package/@trieb.work/nextjs-turbo-redis-cache)
4
4
  ![Turbo redis cache image](https://github.com/user-attachments/assets/4103191e-4f4d-4139-a519-0b5bfab3e8b4)
5
5
 
6
- The ultimate Redis Cache Handler for Next.js 15 / 16 and the app router. Built for production-ready, large-scale projects, it delivers unparalleled performance and efficiency with features tailored for high-traffic applications. This package has been created after extensibly testing the @neshca package and finding several major issues with it.
6
+ The ultimate Redis Cache Handler for Next.js 15 / 16, supporting both the App Router and the Pages Router. Built for production-ready, large-scale projects, it delivers unparalleled performance and efficiency with features tailored for high-traffic applications. This package has been created after extensibly testing the @neshca package and finding several major issues with it.
7
7
 
8
8
  Key Features:
9
9
 
@@ -19,9 +19,11 @@ For a deep dive into the internal architecture (shared hash maps, SyncedMap, req
19
19
 
20
20
  ## Compatibility
21
21
 
22
- This package is compatible with Next.js 15.0.3 and above while using App Router. It is not compatible with Next.js 14.x. or 15-canary or if you are using Pages Router.
22
+ This package is compatible with Next.js 15.0.3 and above, with the App Router, the Pages Router, or both together in a hybrid app. It is not compatible with Next.js 14.x. or 15-canary.
23
23
  Redis Server need to have Redis Server Version 2.8.0 or higher and have to be configured with `notify-keyspace-events` to be able to use the key-space notifications feature.
24
24
 
25
+ Pages Router support covers ISR pages (`getStaticProps` + `revalidate`, `getStaticPaths` with `fallback`), `notFound: true` and `redirect:` results, and on-demand revalidation via `res.revalidate(path)` — including across multiple server instances sharing one Redis (see the two-instance integration test). App Router and Pages Router entries are handled side by side by the same handler instance, so hybrid apps that use both routers work without any extra configuration.
26
+
25
27
  Tested versions are:
26
28
 
27
29
  - Nextjs 15.0.3 + redis client 4.7.0
@@ -31,6 +33,7 @@ Tested versions are:
31
33
  - Nextjs 16.0.11 + redis client 4.7.0 (cacheComponents: false)
32
34
  - Nextjs 16.2.6 + redis client 4.7.0 (cacheComponents: false)
33
35
  - Nextjs 16.2.6 + redis client 4.7.0 (cacheComponents: true)
36
+ - Nextjs 16.2.6 + redis client 4.7.0 (Pages Router)
34
37
 
35
38
  _Cache Components_ (Next.js 16+) are fully supported. Automated test coverage includes `'use cache'`, `cacheTag`, and `cacheLife` flows in the Cache Components integration suite.
36
39
 
package/dist/index.d.mts CHANGED
@@ -55,6 +55,73 @@ type CacheEntry = {
55
55
  lastModified: number;
56
56
  tags: string[];
57
57
  };
58
+ /** Discriminated union of the `ctx` argument Next.js passes to
59
+ * {@link RedisStringsHandler.get}. The Pages Router (`PAGES`) has no PPR
60
+ * concept, so `isRoutePPREnabled` is optional for it.
61
+ *
62
+ * Note: `REDIRECT` is intentionally absent. Next.js always calls `get()` with
63
+ * `kind: 'PAGES'` for Pages Router routes, even when the stored value has
64
+ * `kind: 'REDIRECT'`. The handler returns whatever value is cached and Next.js
65
+ * interprets it accordingly. */
66
+ type GetContext = {
67
+ kind: 'APP_ROUTE' | 'APP_PAGE';
68
+ isRoutePPREnabled: boolean;
69
+ isFallback: boolean;
70
+ } | {
71
+ kind: 'PAGES';
72
+ isRoutePPREnabled?: boolean;
73
+ isFallback: boolean;
74
+ } | {
75
+ kind: 'FETCH';
76
+ revalidate: number;
77
+ fetchUrl: string;
78
+ fetchIdx: number;
79
+ tags: string[];
80
+ softTags: string[];
81
+ isFallback: boolean;
82
+ };
83
+ /** Discriminated union of cache payloads accepted by
84
+ * {@link RedisStringsHandler.set}. `null` is a valid payload: the Pages Router
85
+ * stores `notFound: true` results as a cache entry with a null value. */
86
+ type SetCacheValue = {
87
+ kind: 'APP_PAGE';
88
+ status?: number;
89
+ headers: {
90
+ 'x-nextjs-stale-time': string;
91
+ 'x-next-cache-tags': string;
92
+ };
93
+ html: string;
94
+ rscData: Buffer;
95
+ segmentData: unknown;
96
+ postboned: unknown;
97
+ } | {
98
+ kind: 'APP_ROUTE';
99
+ status: number;
100
+ headers: {
101
+ 'cache-control'?: string;
102
+ 'x-nextjs-stale-time': string;
103
+ 'x-next-cache-tags': string;
104
+ };
105
+ body: Buffer;
106
+ } | {
107
+ kind: 'PAGES';
108
+ html: string;
109
+ pageData: Record<string, unknown>;
110
+ headers?: Record<string, number | string | string[] | undefined>;
111
+ status?: number;
112
+ } | {
113
+ kind: 'REDIRECT';
114
+ props: Record<string, unknown>;
115
+ } | {
116
+ kind: 'FETCH';
117
+ data: {
118
+ headers: Record<string, string>;
119
+ body: string;
120
+ status: number;
121
+ url: string;
122
+ };
123
+ revalidate: number | false;
124
+ } | null;
58
125
  type CreateRedisStringsHandlerOptions = {
59
126
  /** Redis redisUrl to use.
60
127
  * @default process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST
@@ -158,56 +225,15 @@ declare class RedisStringsHandler {
158
225
  resetRequestCache(): void;
159
226
  private clientReadyCalls;
160
227
  private assertClientIsReady;
161
- get(key: string, ctx: {
162
- kind: 'APP_ROUTE' | 'APP_PAGE';
163
- isRoutePPREnabled: boolean;
164
- isFallback: boolean;
165
- } | {
166
- kind: 'FETCH';
167
- revalidate: number;
168
- fetchUrl: string;
169
- fetchIdx: number;
170
- tags: string[];
171
- softTags: string[];
172
- isFallback: boolean;
173
- }): Promise<CacheEntry | null>;
174
- set(key: string, data: {
175
- kind: 'APP_PAGE';
176
- status?: number;
177
- headers: {
178
- 'x-nextjs-stale-time': string;
179
- 'x-next-cache-tags': string;
180
- };
181
- html: string;
182
- rscData: Buffer;
183
- segmentData: unknown;
184
- postboned: unknown;
185
- } | {
186
- kind: 'APP_ROUTE';
187
- status: number;
188
- headers: {
189
- 'cache-control'?: string;
190
- 'x-nextjs-stale-time': string;
191
- 'x-next-cache-tags': string;
192
- };
193
- body: Buffer;
194
- } | {
195
- kind: 'FETCH';
196
- data: {
197
- headers: Record<string, string>;
198
- body: string;
199
- status: number;
200
- url: string;
201
- };
202
- revalidate: number | false;
203
- }, ctx: {
228
+ get(key: string, ctx: GetContext): Promise<CacheEntry | null>;
229
+ set(key: string, data: SetCacheValue, ctx: {
204
230
  isRoutePPREnabled: boolean;
205
231
  isFallback: boolean;
206
232
  tags?: string[];
207
233
  revalidate?: number | false;
208
234
  cacheControl?: {
209
- revalidate: 5;
210
- expire: undefined;
235
+ revalidate: number | false;
236
+ expire: number | undefined;
211
237
  };
212
238
  }): Promise<void>;
213
239
  revalidateTag(tagOrTags: string | string[], ...rest: any[]): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -55,6 +55,73 @@ type CacheEntry = {
55
55
  lastModified: number;
56
56
  tags: string[];
57
57
  };
58
+ /** Discriminated union of the `ctx` argument Next.js passes to
59
+ * {@link RedisStringsHandler.get}. The Pages Router (`PAGES`) has no PPR
60
+ * concept, so `isRoutePPREnabled` is optional for it.
61
+ *
62
+ * Note: `REDIRECT` is intentionally absent. Next.js always calls `get()` with
63
+ * `kind: 'PAGES'` for Pages Router routes, even when the stored value has
64
+ * `kind: 'REDIRECT'`. The handler returns whatever value is cached and Next.js
65
+ * interprets it accordingly. */
66
+ type GetContext = {
67
+ kind: 'APP_ROUTE' | 'APP_PAGE';
68
+ isRoutePPREnabled: boolean;
69
+ isFallback: boolean;
70
+ } | {
71
+ kind: 'PAGES';
72
+ isRoutePPREnabled?: boolean;
73
+ isFallback: boolean;
74
+ } | {
75
+ kind: 'FETCH';
76
+ revalidate: number;
77
+ fetchUrl: string;
78
+ fetchIdx: number;
79
+ tags: string[];
80
+ softTags: string[];
81
+ isFallback: boolean;
82
+ };
83
+ /** Discriminated union of cache payloads accepted by
84
+ * {@link RedisStringsHandler.set}. `null` is a valid payload: the Pages Router
85
+ * stores `notFound: true` results as a cache entry with a null value. */
86
+ type SetCacheValue = {
87
+ kind: 'APP_PAGE';
88
+ status?: number;
89
+ headers: {
90
+ 'x-nextjs-stale-time': string;
91
+ 'x-next-cache-tags': string;
92
+ };
93
+ html: string;
94
+ rscData: Buffer;
95
+ segmentData: unknown;
96
+ postboned: unknown;
97
+ } | {
98
+ kind: 'APP_ROUTE';
99
+ status: number;
100
+ headers: {
101
+ 'cache-control'?: string;
102
+ 'x-nextjs-stale-time': string;
103
+ 'x-next-cache-tags': string;
104
+ };
105
+ body: Buffer;
106
+ } | {
107
+ kind: 'PAGES';
108
+ html: string;
109
+ pageData: Record<string, unknown>;
110
+ headers?: Record<string, number | string | string[] | undefined>;
111
+ status?: number;
112
+ } | {
113
+ kind: 'REDIRECT';
114
+ props: Record<string, unknown>;
115
+ } | {
116
+ kind: 'FETCH';
117
+ data: {
118
+ headers: Record<string, string>;
119
+ body: string;
120
+ status: number;
121
+ url: string;
122
+ };
123
+ revalidate: number | false;
124
+ } | null;
58
125
  type CreateRedisStringsHandlerOptions = {
59
126
  /** Redis redisUrl to use.
60
127
  * @default process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST
@@ -158,56 +225,15 @@ declare class RedisStringsHandler {
158
225
  resetRequestCache(): void;
159
226
  private clientReadyCalls;
160
227
  private assertClientIsReady;
161
- get(key: string, ctx: {
162
- kind: 'APP_ROUTE' | 'APP_PAGE';
163
- isRoutePPREnabled: boolean;
164
- isFallback: boolean;
165
- } | {
166
- kind: 'FETCH';
167
- revalidate: number;
168
- fetchUrl: string;
169
- fetchIdx: number;
170
- tags: string[];
171
- softTags: string[];
172
- isFallback: boolean;
173
- }): Promise<CacheEntry | null>;
174
- set(key: string, data: {
175
- kind: 'APP_PAGE';
176
- status?: number;
177
- headers: {
178
- 'x-nextjs-stale-time': string;
179
- 'x-next-cache-tags': string;
180
- };
181
- html: string;
182
- rscData: Buffer;
183
- segmentData: unknown;
184
- postboned: unknown;
185
- } | {
186
- kind: 'APP_ROUTE';
187
- status: number;
188
- headers: {
189
- 'cache-control'?: string;
190
- 'x-nextjs-stale-time': string;
191
- 'x-next-cache-tags': string;
192
- };
193
- body: Buffer;
194
- } | {
195
- kind: 'FETCH';
196
- data: {
197
- headers: Record<string, string>;
198
- body: string;
199
- status: number;
200
- url: string;
201
- };
202
- revalidate: number | false;
203
- }, ctx: {
228
+ get(key: string, ctx: GetContext): Promise<CacheEntry | null>;
229
+ set(key: string, data: SetCacheValue, ctx: {
204
230
  isRoutePPREnabled: boolean;
205
231
  isFallback: boolean;
206
232
  tags?: string[];
207
233
  revalidate?: number | false;
208
234
  cacheControl?: {
209
- revalidate: 5;
210
- expire: undefined;
235
+ revalidate: number | false;
236
+ expire: number | undefined;
211
237
  };
212
238
  }): Promise<void>;
213
239
  revalidateTag(tagOrTags: string | string[], ...rest: any[]): Promise<void>;
package/dist/index.js CHANGED
@@ -566,6 +566,19 @@ if (process.env.DEBUG_CACHE_HANDLER) {
566
566
  }
567
567
  var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_";
568
568
  var REVALIDATED_TAGS_KEY = "__revalidated_tags__";
569
+ var SUPPORTED_GET_KINDS = [
570
+ "APP_ROUTE",
571
+ "APP_PAGE",
572
+ "PAGES",
573
+ "FETCH"
574
+ ];
575
+ var SUPPORTED_SET_KINDS = [
576
+ "APP_ROUTE",
577
+ "APP_PAGE",
578
+ "PAGES",
579
+ "REDIRECT",
580
+ "FETCH"
581
+ ];
569
582
  var killContainerOnErrorCount = 0;
570
583
  var RedisStringsHandler = class {
571
584
  constructor({
@@ -731,12 +744,12 @@ var RedisStringsHandler = class {
731
744
  }
732
745
  async get(key, ctx) {
733
746
  try {
734
- if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
747
+ if (!SUPPORTED_GET_KINDS.includes(ctx.kind)) {
735
748
  console.warn(
736
749
  "RedisStringsHandler.get() called with",
737
750
  key,
738
751
  ctx,
739
- " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
752
+ `this cache handler is only designed and tested for kinds ${SUPPORTED_GET_KINDS.join(", ")} and not for kind`,
740
753
  ctx?.kind
741
754
  );
742
755
  }
@@ -791,7 +804,7 @@ var RedisStringsHandler = class {
791
804
  "cacheEntry is mall formed (missing tags)"
792
805
  );
793
806
  }
794
- if (!cacheEntry?.value) {
807
+ if (cacheEntry?.value === void 0) {
795
808
  console.warn(
796
809
  "RedisStringsHandler.get() called with",
797
810
  key,
@@ -864,21 +877,27 @@ var RedisStringsHandler = class {
864
877
  }
865
878
  async set(key, data, ctx) {
866
879
  try {
867
- if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
880
+ if (data !== null && !SUPPORTED_SET_KINDS.includes(data.kind)) {
868
881
  console.warn(
869
882
  "RedisStringsHandler.set() called with",
870
883
  key,
871
884
  ctx,
872
885
  data,
873
- " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
886
+ `this cache handler is only designed and tested for kinds ${SUPPORTED_SET_KINDS.join(", ")} and not for kind`,
874
887
  data?.kind
875
888
  );
876
889
  }
877
890
  await this.assertClientIsReady();
878
- if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
891
+ if (data?.kind === "APP_PAGE" || data?.kind === "APP_ROUTE") {
879
892
  const tags = data.headers["x-next-cache-tags"]?.split(",");
880
893
  ctx.tags = [...ctx.tags || [], ...tags || []];
881
894
  }
895
+ if (data === null || data.kind === "PAGES" || data.kind === "REDIRECT") {
896
+ const implicitTag = NEXT_CACHE_IMPLICIT_TAG_ID + key;
897
+ if (!ctx.tags?.includes(implicitTag)) {
898
+ ctx.tags = [...ctx.tags || [], implicitTag];
899
+ }
900
+ }
882
901
  const cacheEntry = {
883
902
  lastModified: Date.now(),
884
903
  tags: ctx?.tags || [],
@@ -893,7 +912,10 @@ var RedisStringsHandler = class {
893
912
  }
894
913
  const revalidate = (
895
914
  // For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used
896
- data.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate || data?.revalidate
915
+ data?.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate || // Legacy fallback: older Next.js versions attached `revalidate` directly
916
+ // to the data payload. FETCH is already handled above, so this only
917
+ // matters for those older, non-discriminated shapes.
918
+ data?.revalidate
897
919
  );
898
920
  const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
899
921
  const setOperation = redisErrorHandler(