@trieb.work/nextjs-turbo-redis-cache 1.10.0-beta.14 → 1.11.0-beta.1

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 (55) hide show
  1. package/.github/workflows/ci.yml +28 -12
  2. package/CHANGELOG.md +6 -94
  3. package/README.md +94 -0
  4. package/dist/index.d.mts +22 -1
  5. package/dist/index.d.ts +22 -1
  6. package/dist/index.js +333 -15
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +330 -14
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +3 -2
  11. package/playwright.config.ts +8 -1
  12. package/src/CacheComponentsHandler.ts +471 -0
  13. package/src/RedisStringsHandler.ts +11 -11
  14. package/src/index.test.ts +1 -1
  15. package/src/index.ts +5 -0
  16. package/test/cache-components/cache-components.integration.spec.ts +188 -0
  17. package/test/integration/next-app-15-4-7/next.config.js +3 -0
  18. package/test/integration/next-app-15-4-7/pnpm-lock.yaml +1 -1
  19. package/test/integration/next-app-16-0-3/next.config.ts +3 -0
  20. package/test/integration/next-app-16-1-1-cache-components/README.md +36 -0
  21. package/test/integration/next-app-16-1-1-cache-components/cache-handler.js +3 -0
  22. package/test/integration/next-app-16-1-1-cache-components/eslint.config.mjs +18 -0
  23. package/test/integration/next-app-16-1-1-cache-components/next.config.ts +13 -0
  24. package/test/integration/next-app-16-1-1-cache-components/package.json +28 -0
  25. package/test/integration/next-app-16-1-1-cache-components/pnpm-lock.yaml +4128 -0
  26. package/test/integration/next-app-16-1-1-cache-components/postcss.config.mjs +7 -0
  27. package/test/integration/next-app-16-1-1-cache-components/public/file.svg +1 -0
  28. package/test/integration/next-app-16-1-1-cache-components/public/globe.svg +1 -0
  29. package/test/integration/next-app-16-1-1-cache-components/public/next.svg +1 -0
  30. package/test/integration/next-app-16-1-1-cache-components/public/public/file.svg +1 -0
  31. package/test/integration/next-app-16-1-1-cache-components/public/public/globe.svg +1 -0
  32. package/test/integration/next-app-16-1-1-cache-components/public/public/next.svg +1 -0
  33. package/test/integration/next-app-16-1-1-cache-components/public/public/vercel.svg +1 -0
  34. package/test/integration/next-app-16-1-1-cache-components/public/public/window.svg +1 -0
  35. package/test/integration/next-app-16-1-1-cache-components/public/vercel.svg +1 -0
  36. package/test/integration/next-app-16-1-1-cache-components/public/window.svg +1 -0
  37. package/test/integration/next-app-16-1-1-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  38. package/test/integration/next-app-16-1-1-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  39. package/test/integration/next-app-16-1-1-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  40. package/test/integration/next-app-16-1-1-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  41. package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  42. package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/page.tsx +90 -0
  43. package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  44. package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  45. package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  46. package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  47. package/test/integration/next-app-16-1-1-cache-components/src/app/favicon.ico +0 -0
  48. package/test/integration/next-app-16-1-1-cache-components/src/app/globals.css +26 -0
  49. package/test/integration/next-app-16-1-1-cache-components/src/app/layout.tsx +57 -0
  50. package/test/integration/next-app-16-1-1-cache-components/src/app/page.tsx +755 -0
  51. package/test/integration/next-app-16-1-1-cache-components/src/app/revalidation-interface.tsx +267 -0
  52. package/test/integration/next-app-16-1-1-cache-components/src/app/update-tag-test/page.tsx +22 -0
  53. package/test/integration/next-app-16-1-1-cache-components/tsconfig.json +34 -0
  54. package/tests/cache-lab.spec.ts +157 -0
  55. package/vitest.cache-components.config.ts +16 -0
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { spawn, ChildProcess } from 'child_process';
3
+ import { createClient, RedisClientType } from 'redis';
4
+ import path from 'path';
5
+
6
+ const PORT = Number(process.env.CACHE_COMPONENTS_PORT || '3065');
7
+ const BASE_URL = `http://localhost:${PORT}`;
8
+
9
+ describe('Next.js 16 Cache Components Integration', () => {
10
+ let nextProcess: ChildProcess;
11
+ let redisClient: RedisClientType;
12
+ let keyPrefix: string;
13
+
14
+ beforeAll(async () => {
15
+ // Connect to Redis
16
+ redisClient = createClient({
17
+ url: process.env.REDIS_URL || 'redis://localhost:6379',
18
+ database: 1,
19
+ });
20
+ await redisClient.connect();
21
+
22
+ // Generate unique key prefix for this test run
23
+ keyPrefix = `cache-components-test-${Math.random().toString(36).substring(7)}`;
24
+ process.env.VERCEL_URL = keyPrefix;
25
+
26
+ // Build and start Next.js app
27
+ const appDir = path.join(
28
+ __dirname,
29
+ '..',
30
+ 'integration',
31
+ 'next-app-16-1-1-cache-components',
32
+ );
33
+
34
+ console.log('Installing Next.js app dependencies...');
35
+ await new Promise<void>((resolve, reject) => {
36
+ const installProcess = spawn('pnpm', ['install'], {
37
+ cwd: appDir,
38
+ stdio: 'inherit',
39
+ });
40
+
41
+ installProcess.on('close', (code) => {
42
+ if (code === 0) resolve();
43
+ else reject(new Error(`Install failed with code ${code}`));
44
+ });
45
+ });
46
+
47
+ console.log('Building Next.js app...');
48
+ await new Promise<void>((resolve, reject) => {
49
+ const buildProcess = spawn('pnpm', ['build'], {
50
+ cwd: appDir,
51
+ stdio: 'inherit',
52
+ });
53
+
54
+ buildProcess.on('close', (code) => {
55
+ if (code === 0) resolve();
56
+ else reject(new Error(`Build failed with code ${code}`));
57
+ });
58
+ });
59
+
60
+ console.log('Starting Next.js app...');
61
+ nextProcess = spawn('pnpm', ['start', '-p', PORT.toString()], {
62
+ cwd: appDir,
63
+ env: { ...process.env, VERCEL_URL: keyPrefix },
64
+ });
65
+
66
+ // Wait for server to be ready
67
+ await new Promise((resolve) => setTimeout(resolve, 3000));
68
+ }, 120000);
69
+
70
+ afterAll(async () => {
71
+ // Clean up Redis keys
72
+ const keys = await redisClient.keys(`${keyPrefix}*`);
73
+ if (keys.length > 0) {
74
+ await redisClient.del(keys);
75
+ }
76
+ await redisClient.quit();
77
+
78
+ // Kill Next.js process
79
+ if (nextProcess) {
80
+ nextProcess.kill();
81
+ }
82
+ });
83
+
84
+ describe('Basic use cache functionality', () => {
85
+ it('should cache data and return same counter value on subsequent requests', async () => {
86
+ // First request
87
+ const res1 = await fetch(`${BASE_URL}/api/cached-static-fetch`);
88
+ const data1 = await res1.json();
89
+
90
+ expect(data1.counter).toBe(1);
91
+
92
+ // Second request should return cached data
93
+ const res2 = await fetch(`${BASE_URL}/api/cached-static-fetch`);
94
+ const data2 = await res2.json();
95
+
96
+ expect(data2.counter).toBe(1); // Same counter value
97
+ expect(data2.timestamp).toBe(data1.timestamp); // Same timestamp
98
+ });
99
+
100
+ it('should store cache entry in Redis', async () => {
101
+ await fetch(`${BASE_URL}/api/cached-static-fetch`);
102
+
103
+ // Check Redis for cache keys
104
+ const keys = await redisClient.keys(`${keyPrefix}*`);
105
+ expect(keys.length).toBeGreaterThan(0);
106
+ });
107
+ });
108
+
109
+ describe('cacheTag functionality', () => {
110
+ it('should cache data with tags', async () => {
111
+ const res1 = await fetch(`${BASE_URL}/api/cached-with-tag`);
112
+ const data1 = await res1.json();
113
+
114
+ expect(data1.counter).toBeDefined();
115
+
116
+ // Second request should return cached data
117
+ const res2 = await fetch(`${BASE_URL}/api/cached-with-tag`);
118
+ const data2 = await res2.json();
119
+
120
+ expect(data2.counter).toBe(data1.counter);
121
+ });
122
+
123
+ it('should invalidate cache when tag is revalidated (Stale while revalidate)', async () => {
124
+ // Get initial cached data
125
+ const res1 = await fetch(`${BASE_URL}/api/cached-with-tag`);
126
+ const data1 = await res1.json();
127
+
128
+ // Revalidate the tag
129
+ await fetch(`${BASE_URL}/api/revalidate-tag`, {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify({ tag: 'test-tag' }),
133
+ });
134
+
135
+ // The cache should be invalidated - verify by making multiple requests
136
+ // until we get fresh data (with retries for async revalidation)
137
+ let freshDataReceived = false;
138
+ // Next.js tag revalidation can be async and may take longer under some runtimes.
139
+ // Use a more tolerant window to avoid flaky failures.
140
+ for (let i = 0; i < 60; i++) {
141
+ await new Promise((resolve) => setTimeout(resolve, 500));
142
+ const res = await fetch(`${BASE_URL}/api/cached-with-tag`);
143
+ const data = await res.json();
144
+
145
+ if (
146
+ data.counter !== data1.counter ||
147
+ data.timestamp !== data1.timestamp
148
+ ) {
149
+ freshDataReceived = true;
150
+ break;
151
+ }
152
+ }
153
+
154
+ expect(freshDataReceived).toBe(true);
155
+ }, 20_000);
156
+ });
157
+
158
+ describe('Redis cache handler integration', () => {
159
+ it('should call cache handler get and set methods', async () => {
160
+ // Make request to trigger cache (don't clear first)
161
+ await fetch(`${BASE_URL}/api/cached-static-fetch`);
162
+
163
+ // Verify Redis has the cached data
164
+ const redisKeys = await redisClient.keys(`${keyPrefix}*`);
165
+ expect(redisKeys.length).toBeGreaterThan(0);
166
+
167
+ // Filter out hash keys (sharedTagsMap) and only check string keys (cache entries)
168
+ // Try to get each key and verify at least one is a string value
169
+ let foundStringKey = false;
170
+ for (const key of redisKeys) {
171
+ try {
172
+ const type = await redisClient.type(key);
173
+ if (type === 'string') {
174
+ const cachedValue = await redisClient.get(key);
175
+ if (cachedValue) {
176
+ foundStringKey = true;
177
+ expect(cachedValue).toBeTruthy();
178
+ break;
179
+ }
180
+ }
181
+ } catch (e) {
182
+ // Skip non-string keys
183
+ }
184
+ }
185
+ expect(foundStringKey).toBe(true);
186
+ });
187
+ });
188
+ });
@@ -1,6 +1,9 @@
1
1
  /** @type {import('next').NextConfig} */
2
2
  const nextConfig = {
3
3
  cacheHandler: require.resolve('@trieb.work/nextjs-turbo-redis-cache'),
4
+ turbopack: {
5
+ root: __dirname,
6
+ },
4
7
  };
5
8
 
6
9
  module.exports = nextConfig;
@@ -471,7 +471,7 @@ packages:
471
471
  '@trieb.work/nextjs-turbo-redis-cache@file:../../..':
472
472
  resolution: {directory: ../../.., type: directory}
473
473
  peerDependencies:
474
- next: '>=15.0.3 <= 15.4.7'
474
+ next: '>=15.0.3 <16.2.0'
475
475
  redis: 4.7.0
476
476
 
477
477
  '@tybys/wasm-util@0.9.0':
@@ -2,6 +2,9 @@ import type { NextConfig } from 'next';
2
2
 
3
3
  const nextConfig: NextConfig = {
4
4
  cacheHandler: require.resolve('@trieb.work/nextjs-turbo-redis-cache'),
5
+ turbopack: {
6
+ root: __dirname,
7
+ },
5
8
  };
6
9
 
7
10
  export default nextConfig;
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,3 @@
1
+ const { redisCacheHandler } = require('@trieb.work/nextjs-turbo-redis-cache');
2
+
3
+ module.exports = redisCacheHandler;
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,13 @@
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {
4
+ cacheComponents: true,
5
+ cacheHandlers: {
6
+ default: require.resolve('./cache-handler.js'),
7
+ },
8
+ turbopack: {
9
+ root: __dirname,
10
+ },
11
+ };
12
+
13
+ export default nextConfig;
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "next-app-16-1-1-cache-components",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@trieb.work/nextjs-turbo-redis-cache": "file:../../../",
13
+ "next": "16.1.1",
14
+ "react": "19.2.0",
15
+ "react-dom": "19.2.0",
16
+ "redis": "4.7.0"
17
+ },
18
+ "devDependencies": {
19
+ "@tailwindcss/postcss": "^4",
20
+ "@types/node": "^20",
21
+ "@types/react": "^19",
22
+ "@types/react-dom": "^19",
23
+ "eslint": "^9",
24
+ "eslint-config-next": "16.1.1",
25
+ "tailwindcss": "^4",
26
+ "typescript": "^5"
27
+ }
28
+ }