@trieb.work/nextjs-turbo-redis-cache 1.10.0 → 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 +27 -11
- package/CHANGELOG.md +9 -0
- package/README.md +94 -0
- package/dist/index.d.mts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +318 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +315 -1
- 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/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,267 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
// Define API routes for path-based revalidation
|
|
6
|
+
const apiRoutePaths: Record<string, string> = {
|
|
7
|
+
'Cached Static Fetch': '/api/cached-static-fetch',
|
|
8
|
+
'Uncached Fetch': '/api/uncached-fetch',
|
|
9
|
+
'Revalidated Fetch': '/api/revalidated-fetch',
|
|
10
|
+
'Nested Fetch in API Route':
|
|
11
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Define API routes with tags for tag-based revalidation
|
|
15
|
+
const apiRouteTags: Record<string, string> = {
|
|
16
|
+
'Revalidated Fetch in Nested API Route':
|
|
17
|
+
'revalidated-fetch-revalidate3-nested-fetch-in-api-route',
|
|
18
|
+
'Revalidated Fetch API': 'revalidated-fetch-api',
|
|
19
|
+
'Cached Static Fetch API': 'cached-static-fetch-api',
|
|
20
|
+
'Uncached Fetch API': 'uncached-fetch-api',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Revalidation Interface Component
|
|
24
|
+
export function RevalidationInterface() {
|
|
25
|
+
const [selectedPathRoute, setSelectedPathRoute] = useState('');
|
|
26
|
+
const [selectedTagRoute, setSelectedTagRoute] = useState('');
|
|
27
|
+
const [revalidationStatus, setRevalidationStatus] = useState('');
|
|
28
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
29
|
+
|
|
30
|
+
const handlePathRevalidate = async () => {
|
|
31
|
+
if (!selectedPathRoute) {
|
|
32
|
+
setRevalidationStatus('Please select an API route to revalidate by path');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setIsLoading(true);
|
|
37
|
+
setRevalidationStatus('Revalidating API route by path...');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const path = apiRoutePaths[selectedPathRoute];
|
|
41
|
+
const response = await fetch(`/api/revalidatePath?path=${path}`);
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
|
|
44
|
+
if (response.ok) {
|
|
45
|
+
setRevalidationStatus(
|
|
46
|
+
`Successfully revalidated API route by path: ${selectedPathRoute} (${path})`,
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
setRevalidationStatus(`Error: ${data.error || 'Unknown error'}`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error: unknown) {
|
|
52
|
+
const errorMessage =
|
|
53
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
54
|
+
setRevalidationStatus(`Error: ${errorMessage}`);
|
|
55
|
+
} finally {
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleTagRevalidate = async () => {
|
|
61
|
+
if (!selectedTagRoute) {
|
|
62
|
+
setRevalidationStatus('Please select an API route to revalidate by tag');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setIsLoading(true);
|
|
67
|
+
setRevalidationStatus('Revalidating API route by tag...');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const tag = apiRouteTags[selectedTagRoute];
|
|
71
|
+
const response = await fetch(`/api/revalidateTag?tag=${tag}`);
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
|
|
74
|
+
if (response.ok) {
|
|
75
|
+
setRevalidationStatus(
|
|
76
|
+
`Successfully revalidated API route by tag: ${selectedTagRoute} (tag: ${tag})`,
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
setRevalidationStatus(`Error: ${data.error || 'Unknown error'}`);
|
|
80
|
+
}
|
|
81
|
+
} catch (error: unknown) {
|
|
82
|
+
const errorMessage =
|
|
83
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
84
|
+
setRevalidationStatus(`Error: ${errorMessage}`);
|
|
85
|
+
} finally {
|
|
86
|
+
setIsLoading(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
border: '1px solid #eaeaea',
|
|
94
|
+
padding: '25px',
|
|
95
|
+
borderRadius: '8px',
|
|
96
|
+
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
|
|
97
|
+
backgroundColor: '#fff',
|
|
98
|
+
marginBottom: '30px',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<h3
|
|
102
|
+
style={{
|
|
103
|
+
fontSize: '1.4rem',
|
|
104
|
+
color: '#0070f3',
|
|
105
|
+
marginTop: '0',
|
|
106
|
+
marginBottom: '15px',
|
|
107
|
+
borderBottom: '1px solid #eaeaea',
|
|
108
|
+
paddingBottom: '10px',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
API Route Revalidation Interface
|
|
112
|
+
</h3>
|
|
113
|
+
|
|
114
|
+
{/* Path-based revalidation section */}
|
|
115
|
+
<div
|
|
116
|
+
style={{
|
|
117
|
+
padding: '15px',
|
|
118
|
+
marginBottom: '20px',
|
|
119
|
+
backgroundColor: '#f9f9f9',
|
|
120
|
+
borderRadius: '5px',
|
|
121
|
+
border: '1px solid #eaeaea',
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<h4
|
|
125
|
+
style={{
|
|
126
|
+
fontSize: '1.1rem',
|
|
127
|
+
color: '#0070f3',
|
|
128
|
+
marginTop: '0',
|
|
129
|
+
marginBottom: '15px',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
Revalidate API Route by Path
|
|
133
|
+
</h4>
|
|
134
|
+
|
|
135
|
+
<div style={{ marginBottom: '15px' }}>
|
|
136
|
+
<label
|
|
137
|
+
style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}
|
|
138
|
+
>
|
|
139
|
+
Select API Route:
|
|
140
|
+
</label>
|
|
141
|
+
<select
|
|
142
|
+
value={selectedPathRoute}
|
|
143
|
+
onChange={(e) => setSelectedPathRoute(e.target.value)}
|
|
144
|
+
style={{
|
|
145
|
+
width: '100%',
|
|
146
|
+
padding: '10px',
|
|
147
|
+
borderRadius: '5px',
|
|
148
|
+
border: '1px solid #ddd',
|
|
149
|
+
fontSize: '1rem',
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
<option value="">-- Select an API route --</option>
|
|
153
|
+
{Object.keys(apiRoutePaths).map((route) => (
|
|
154
|
+
<option key={route} value={route}>
|
|
155
|
+
{route}
|
|
156
|
+
</option>
|
|
157
|
+
))}
|
|
158
|
+
</select>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<button
|
|
162
|
+
onClick={handlePathRevalidate}
|
|
163
|
+
disabled={isLoading || !selectedPathRoute}
|
|
164
|
+
style={{
|
|
165
|
+
backgroundColor: '#0070f3',
|
|
166
|
+
color: 'white',
|
|
167
|
+
border: 'none',
|
|
168
|
+
padding: '10px 20px',
|
|
169
|
+
borderRadius: '5px',
|
|
170
|
+
fontSize: '1rem',
|
|
171
|
+
fontWeight: '500',
|
|
172
|
+
cursor: isLoading || !selectedPathRoute ? 'not-allowed' : 'pointer',
|
|
173
|
+
opacity: isLoading || !selectedPathRoute ? 0.7 : 1,
|
|
174
|
+
transition: 'all 0.2s',
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
{isLoading ? 'Revalidating...' : 'Revalidate by Path'}
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Tag-based revalidation section */}
|
|
182
|
+
<div
|
|
183
|
+
style={{
|
|
184
|
+
padding: '15px',
|
|
185
|
+
marginBottom: '20px',
|
|
186
|
+
backgroundColor: '#f9f9f9',
|
|
187
|
+
borderRadius: '5px',
|
|
188
|
+
border: '1px solid #eaeaea',
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
<h4
|
|
192
|
+
style={{
|
|
193
|
+
fontSize: '1.1rem',
|
|
194
|
+
color: '#0070f3',
|
|
195
|
+
marginTop: '0',
|
|
196
|
+
marginBottom: '15px',
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
Revalidate API Route by Tag
|
|
200
|
+
</h4>
|
|
201
|
+
|
|
202
|
+
<div style={{ marginBottom: '15px' }}>
|
|
203
|
+
<label
|
|
204
|
+
style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}
|
|
205
|
+
>
|
|
206
|
+
Select API Route with Tag:
|
|
207
|
+
</label>
|
|
208
|
+
<select
|
|
209
|
+
value={selectedTagRoute}
|
|
210
|
+
onChange={(e) => setSelectedTagRoute(e.target.value)}
|
|
211
|
+
style={{
|
|
212
|
+
width: '100%',
|
|
213
|
+
padding: '10px',
|
|
214
|
+
borderRadius: '5px',
|
|
215
|
+
border: '1px solid #ddd',
|
|
216
|
+
fontSize: '1rem',
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<option value="">-- Select an API route with tag --</option>
|
|
220
|
+
{Object.keys(apiRouteTags).map((route) => (
|
|
221
|
+
<option key={route} value={route}>
|
|
222
|
+
{route}
|
|
223
|
+
</option>
|
|
224
|
+
))}
|
|
225
|
+
</select>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<button
|
|
229
|
+
onClick={handleTagRevalidate}
|
|
230
|
+
disabled={isLoading || !selectedTagRoute}
|
|
231
|
+
style={{
|
|
232
|
+
backgroundColor: '#0070f3',
|
|
233
|
+
color: 'white',
|
|
234
|
+
border: 'none',
|
|
235
|
+
padding: '10px 20px',
|
|
236
|
+
borderRadius: '5px',
|
|
237
|
+
fontSize: '1rem',
|
|
238
|
+
fontWeight: '500',
|
|
239
|
+
cursor: isLoading || !selectedTagRoute ? 'not-allowed' : 'pointer',
|
|
240
|
+
opacity: isLoading || !selectedTagRoute ? 0.7 : 1,
|
|
241
|
+
transition: 'all 0.2s',
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
{isLoading ? 'Revalidating...' : 'Revalidate by Tag'}
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Status display */}
|
|
249
|
+
{revalidationStatus && (
|
|
250
|
+
<div
|
|
251
|
+
style={{
|
|
252
|
+
marginTop: '15px',
|
|
253
|
+
padding: '10px',
|
|
254
|
+
borderRadius: '5px',
|
|
255
|
+
backgroundColor: revalidationStatus.includes('Error')
|
|
256
|
+
? '#ffebee'
|
|
257
|
+
: '#e3f2fd',
|
|
258
|
+
color: revalidationStatus.includes('Error') ? '#c62828' : '#0070f3',
|
|
259
|
+
border: `1px solid ${revalidationStatus.includes('Error') ? '#ffcdd2' : '#bbdefb'}`,
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{revalidationStatus}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { updateTag } from 'next/cache';
|
|
2
|
+
|
|
3
|
+
let clicks = 0;
|
|
4
|
+
|
|
5
|
+
async function increment() {
|
|
6
|
+
'use server';
|
|
7
|
+
|
|
8
|
+
clicks++;
|
|
9
|
+
updateTag('update-tag-test');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function UpdateTagTestPage() {
|
|
13
|
+
return (
|
|
14
|
+
<main style={{ padding: 40, fontFamily: 'system-ui, sans-serif' }}>
|
|
15
|
+
<h1>UpdateTag Test</h1>
|
|
16
|
+
<form action={increment}>
|
|
17
|
+
<p data-testid="clicks">Clicks: {clicks}</p>
|
|
18
|
+
<button type="submit">Increment</button>
|
|
19
|
+
</form>
|
|
20
|
+
</main>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// These tests assume the Cache Components Next.js app is already running on the same origin as baseURL.
|
|
4
|
+
// Recommended:
|
|
5
|
+
// PLAYWRIGHT_BASE_URL=http://localhost:3001 pnpm test:e2e
|
|
6
|
+
|
|
7
|
+
test.describe('Cache Lab (Cache Components)', () => {
|
|
8
|
+
test('use cache non-deterministic values are stable until updateTag', async ({
|
|
9
|
+
page,
|
|
10
|
+
}) => {
|
|
11
|
+
await page.goto('/cache-lab/use-cache-nondeterministic');
|
|
12
|
+
|
|
13
|
+
const random1 = page.getByTestId('random1');
|
|
14
|
+
const now = page.getByTestId('now');
|
|
15
|
+
|
|
16
|
+
await expect(random1).toBeVisible();
|
|
17
|
+
|
|
18
|
+
const beforeRandom = await random1.textContent();
|
|
19
|
+
const beforeNow = await now.textContent();
|
|
20
|
+
|
|
21
|
+
await page.reload();
|
|
22
|
+
await expect(random1).toBeVisible();
|
|
23
|
+
|
|
24
|
+
const afterReloadRandom = await random1.textContent();
|
|
25
|
+
const afterReloadNow = await now.textContent();
|
|
26
|
+
|
|
27
|
+
expect(afterReloadRandom).toBe(beforeRandom);
|
|
28
|
+
expect(afterReloadNow).toBe(beforeNow);
|
|
29
|
+
|
|
30
|
+
await page
|
|
31
|
+
.getByRole('button', { name: "updateTag('cache-lab:nondet')" })
|
|
32
|
+
.click();
|
|
33
|
+
await page.waitForLoadState('networkidle');
|
|
34
|
+
|
|
35
|
+
await expect(random1).toBeVisible();
|
|
36
|
+
|
|
37
|
+
const afterInvalidateRandom = await random1.textContent();
|
|
38
|
+
const afterInvalidateNow = await now.textContent();
|
|
39
|
+
|
|
40
|
+
expect(afterInvalidateRandom).not.toBe(beforeRandom);
|
|
41
|
+
expect(afterInvalidateNow).not.toBe(beforeNow);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('tag invalidation changes cached values (updateTag + revalidateTag)', async ({
|
|
45
|
+
page,
|
|
46
|
+
}) => {
|
|
47
|
+
await page.goto('/cache-lab/tag-invalidation');
|
|
48
|
+
|
|
49
|
+
const createdAt = page.getByTestId('createdAt');
|
|
50
|
+
const value = page.getByTestId('value');
|
|
51
|
+
|
|
52
|
+
await expect(createdAt).toBeVisible();
|
|
53
|
+
|
|
54
|
+
const beforeCreatedAt = await createdAt.textContent();
|
|
55
|
+
const beforeValue = await value.textContent();
|
|
56
|
+
|
|
57
|
+
await page.reload();
|
|
58
|
+
await expect(createdAt).toBeVisible();
|
|
59
|
+
expect(await createdAt.textContent()).toBe(beforeCreatedAt);
|
|
60
|
+
expect(await value.textContent()).toBe(beforeValue);
|
|
61
|
+
|
|
62
|
+
await page
|
|
63
|
+
.getByRole('button', { name: "updateTag('cache-lab:tag')" })
|
|
64
|
+
.click();
|
|
65
|
+
await page.waitForLoadState('networkidle');
|
|
66
|
+
|
|
67
|
+
await expect(createdAt).toBeVisible();
|
|
68
|
+
expect(await createdAt.textContent()).not.toBe(beforeCreatedAt);
|
|
69
|
+
expect(await value.textContent()).not.toBe(beforeValue);
|
|
70
|
+
|
|
71
|
+
const afterUpdateCreatedAt = await createdAt.textContent();
|
|
72
|
+
const afterUpdateValue = await value.textContent();
|
|
73
|
+
|
|
74
|
+
await page
|
|
75
|
+
.getByRole('button', { name: "revalidateTag('cache-lab:tag')" })
|
|
76
|
+
.click();
|
|
77
|
+
await page.waitForLoadState('networkidle');
|
|
78
|
+
|
|
79
|
+
await expect(createdAt).toBeVisible();
|
|
80
|
+
|
|
81
|
+
const maybeStaleCreatedAt = await createdAt.textContent();
|
|
82
|
+
const maybeStaleValue = await value.textContent();
|
|
83
|
+
|
|
84
|
+
// Revalidate can be SWR; allow either immediate refresh or stale served,
|
|
85
|
+
// but the value should eventually change on subsequent reloads.
|
|
86
|
+
if (maybeStaleCreatedAt === afterUpdateCreatedAt) {
|
|
87
|
+
// give background refresh some time
|
|
88
|
+
await page.waitForTimeout(4000);
|
|
89
|
+
await page.reload();
|
|
90
|
+
await expect(createdAt).toBeVisible();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
expect(await createdAt.textContent()).not.toBe(afterUpdateCreatedAt);
|
|
94
|
+
expect(await value.textContent()).not.toBe(afterUpdateValue);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('runtime cookie changes cache key (cached payload differs per cookie)', async ({
|
|
98
|
+
page,
|
|
99
|
+
}) => {
|
|
100
|
+
await page.goto('/cache-lab/runtime-data-suspense');
|
|
101
|
+
|
|
102
|
+
const cookieValue = page.getByTestId('cookie');
|
|
103
|
+
const payload = page.getByTestId('payload');
|
|
104
|
+
|
|
105
|
+
await expect(cookieValue).toBeVisible();
|
|
106
|
+
|
|
107
|
+
await page.getByRole('button', { name: 'Set cookie: user-a' }).click();
|
|
108
|
+
await page.waitForLoadState('networkidle');
|
|
109
|
+
|
|
110
|
+
await expect(cookieValue).toHaveText('user-a');
|
|
111
|
+
const payloadA1 = await payload.textContent();
|
|
112
|
+
|
|
113
|
+
await page.reload();
|
|
114
|
+
await expect(cookieValue).toHaveText('user-a');
|
|
115
|
+
const payloadA2 = await payload.textContent();
|
|
116
|
+
expect(payloadA2).toBe(payloadA1);
|
|
117
|
+
|
|
118
|
+
await page.getByRole('button', { name: 'Set cookie: user-b' }).click();
|
|
119
|
+
await page.waitForLoadState('networkidle');
|
|
120
|
+
|
|
121
|
+
await expect(cookieValue).toHaveText('user-b');
|
|
122
|
+
const payloadB = await payload.textContent();
|
|
123
|
+
|
|
124
|
+
expect(payloadB).not.toBe(payloadA1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('stale-while-revalidate demo eventually produces a new value after revalidateTag', async ({
|
|
128
|
+
page,
|
|
129
|
+
}) => {
|
|
130
|
+
await page.goto('/cache-lab/stale-while-revalidate');
|
|
131
|
+
|
|
132
|
+
const computedAt = page.getByTestId('computedAt');
|
|
133
|
+
const value = page.getByTestId('value');
|
|
134
|
+
|
|
135
|
+
await expect(computedAt).toBeVisible();
|
|
136
|
+
|
|
137
|
+
const beforeComputedAt = await computedAt.textContent();
|
|
138
|
+
const beforeValue = await value.textContent();
|
|
139
|
+
|
|
140
|
+
// Ensure it becomes stale.
|
|
141
|
+
await page.waitForTimeout(3000);
|
|
142
|
+
|
|
143
|
+
await page
|
|
144
|
+
.getByRole('button', { name: "revalidateTag('cache-lab:swr')" })
|
|
145
|
+
.click();
|
|
146
|
+
await page.waitForLoadState('networkidle');
|
|
147
|
+
|
|
148
|
+
// Allow some time for background refresh to potentially complete.
|
|
149
|
+
await page.waitForTimeout(4500);
|
|
150
|
+
|
|
151
|
+
await page.reload();
|
|
152
|
+
await expect(computedAt).toBeVisible();
|
|
153
|
+
|
|
154
|
+
expect(await computedAt.textContent()).not.toBe(beforeComputedAt);
|
|
155
|
+
expect(await value.textContent()).not.toBe(beforeValue);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['test/cache-components/**/*.spec.ts'],
|
|
8
|
+
exclude: [
|
|
9
|
+
'node_modules',
|
|
10
|
+
'dist',
|
|
11
|
+
'.git',
|
|
12
|
+
'**/node_modules/**',
|
|
13
|
+
'**/node_modules',
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
});
|