@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.
- package/.github/workflows/ci.yml +28 -12
- package/CHANGELOG.md +6 -94
- package/README.md +94 -0
- package/dist/index.d.mts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +333 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +330 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/playwright.config.ts +8 -1
- package/src/CacheComponentsHandler.ts +471 -0
- package/src/RedisStringsHandler.ts +11 -11
- package/src/index.test.ts +1 -1
- package/src/index.ts +5 -0
- package/test/cache-components/cache-components.integration.spec.ts +188 -0
- package/test/integration/next-app-15-4-7/next.config.js +3 -0
- package/test/integration/next-app-15-4-7/pnpm-lock.yaml +1 -1
- package/test/integration/next-app-16-0-3/next.config.ts +3 -0
- package/test/integration/next-app-16-1-1-cache-components/README.md +36 -0
- package/test/integration/next-app-16-1-1-cache-components/cache-handler.js +3 -0
- package/test/integration/next-app-16-1-1-cache-components/eslint.config.mjs +18 -0
- package/test/integration/next-app-16-1-1-cache-components/next.config.ts +13 -0
- package/test/integration/next-app-16-1-1-cache-components/package.json +28 -0
- package/test/integration/next-app-16-1-1-cache-components/pnpm-lock.yaml +4128 -0
- package/test/integration/next-app-16-1-1-cache-components/postcss.config.mjs +7 -0
- package/test/integration/next-app-16-1-1-cache-components/public/file.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/globe.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/next.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/public/file.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/public/globe.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/public/next.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/public/vercel.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/public/window.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/vercel.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/public/window.svg +1 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/page.tsx +90 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/favicon.ico +0 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/globals.css +26 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/layout.tsx +57 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/page.tsx +755 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/revalidation-interface.tsx +267 -0
- package/test/integration/next-app-16-1-1-cache-components/src/app/update-tag-test/page.tsx +22 -0
- package/test/integration/next-app-16-1-1-cache-components/tsconfig.json +34 -0
- package/tests/cache-lab.spec.ts +157 -0
- 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
|
+
});
|
|
@@ -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
|
|
474
|
+
next: '>=15.0.3 <16.2.0'
|
|
475
475
|
redis: 4.7.0
|
|
476
476
|
|
|
477
477
|
'@tybys/wasm-util@0.9.0':
|
|
@@ -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,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,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
|
+
}
|