@trieb.work/nextjs-turbo-redis-cache 1.8.1 → 1.9.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.
- package/.github/workflows/ci.yml +9 -2
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/RedisStringsHandler.ts +2 -2
- package/test/integration/next-app-15-0-3/pnpm-lock.yaml +1 -1
- package/test/integration/next-app-15-3-2/pnpm-lock.yaml +1 -1
- package/test/integration/next-app-15-4-7/README.md +36 -0
- package/test/integration/next-app-15-4-7/eslint.config.mjs +16 -0
- package/test/integration/next-app-15-4-7/next.config.js +6 -0
- package/test/integration/next-app-15-4-7/package-lock.json +5969 -0
- package/test/integration/next-app-15-4-7/package.json +33 -0
- package/test/integration/next-app-15-4-7/pnpm-lock.yaml +3707 -0
- package/test/integration/next-app-15-4-7/postcss.config.mjs +5 -0
- package/test/integration/next-app-15-4-7/public/file.svg +1 -0
- package/test/integration/next-app-15-4-7/public/globe.svg +1 -0
- package/test/integration/next-app-15-4-7/public/next.svg +1 -0
- package/test/integration/next-app-15-4-7/public/vercel.svg +1 -0
- package/test/integration/next-app-15-4-7/public/window.svg +1 -0
- package/test/integration/next-app-15-4-7/src/app/api/cached-static-fetch/route.ts +18 -0
- package/test/integration/next-app-15-4-7/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
- package/test/integration/next-app-15-4-7/src/app/api/revalidatePath/route.ts +15 -0
- package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +15 -0
- package/test/integration/next-app-15-4-7/src/app/api/revalidated-fetch/route.ts +17 -0
- package/test/integration/next-app-15-4-7/src/app/api/uncached-fetch/route.ts +15 -0
- package/test/integration/next-app-15-4-7/src/app/globals.css +26 -0
- package/test/integration/next-app-15-4-7/src/app/layout.tsx +59 -0
- package/test/integration/next-app-15-4-7/src/app/page.tsx +755 -0
- package/test/integration/next-app-15-4-7/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-4-7/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
- package/test/integration/next-app-15-4-7/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-4-7/src/app/pages/no-fetch/default-page/page.tsx +55 -0
- package/test/integration/next-app-15-4-7/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-4-7/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
- package/test/integration/next-app-15-4-7/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-4-7/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-4-7/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
- package/test/integration/next-app-15-4-7/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-4-7/src/app/revalidation-interface.tsx +267 -0
- package/test/integration/next-app-15-4-7/tsconfig.json +27 -0
- package/test/integration/nextjs-cache-handler.integration.test.ts +7 -5
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
export default async function TestPage() {
|
|
4
|
+
const res = await fetch(
|
|
5
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/cached-static-fetch`,
|
|
6
|
+
);
|
|
7
|
+
const data = await res.json();
|
|
8
|
+
return (
|
|
9
|
+
<main
|
|
10
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
11
|
+
>
|
|
12
|
+
<h1>Test Page</h1>
|
|
13
|
+
<p>Counter: {data.counter}</p>
|
|
14
|
+
<p>This is a test page for integration testing.</p>
|
|
15
|
+
<p>Timestamp: {Date.now()}</p>
|
|
16
|
+
<p>Slug: /test-page</p>
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default async function TestPage() {
|
|
2
|
+
let res;
|
|
3
|
+
try {
|
|
4
|
+
res = await fetch(
|
|
5
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/cached-static-fetch`,
|
|
6
|
+
{
|
|
7
|
+
next: {
|
|
8
|
+
revalidate: 15,
|
|
9
|
+
tags: ['cached-static-fetch-revalidate15-default-page'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// ECONNREFUSED is expected during build
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
if (((e as Error).cause as any)?.code !== 'ECONNREFUSED') {
|
|
17
|
+
throw e;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const data = res?.ok ? await res.json() : { counter: -1 };
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<main
|
|
25
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
26
|
+
>
|
|
27
|
+
<h1>Test Page</h1>
|
|
28
|
+
<p>Counter: {data.counter}</p>
|
|
29
|
+
<p>This is a test page for integration testing.</p>
|
|
30
|
+
<p>Timestamp: {Date.now()}</p>
|
|
31
|
+
<p>Slug: /test-page</p>
|
|
32
|
+
</main>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
export default async function TestPage() {
|
|
4
|
+
const res = await fetch(
|
|
5
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/cached-static-fetch`,
|
|
6
|
+
{
|
|
7
|
+
next: {
|
|
8
|
+
revalidate: 15,
|
|
9
|
+
tags: ['cached-static-fetch-revalidate15-force-dynamic-page'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
return (
|
|
15
|
+
<main
|
|
16
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
17
|
+
>
|
|
18
|
+
<h1>Test Page</h1>
|
|
19
|
+
<p>Counter: {data.counter}</p>
|
|
20
|
+
<p>This is a test page for integration testing.</p>
|
|
21
|
+
<p>Timestamp: {Date.now()}</p>
|
|
22
|
+
<p>Slug: /test-page</p>
|
|
23
|
+
</main>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export default async function TestPage() {
|
|
2
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
3
|
+
const rdm = Math.random();
|
|
4
|
+
return (
|
|
5
|
+
<main
|
|
6
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
7
|
+
>
|
|
8
|
+
<h1>Test Page</h1>
|
|
9
|
+
<p>Random number: {rdm}</p>
|
|
10
|
+
<p>This is a test page for integration testing.</p>
|
|
11
|
+
<p>Timestamp: {Date.now()}</p>
|
|
12
|
+
</main>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* TODO
|
|
18
|
+
* Test cases:
|
|
19
|
+
* 0. Store timestamp of the first call in a variable so we can later check if TTL in redis is correct
|
|
20
|
+
* 1. Call the page twice
|
|
21
|
+
* 2. Extract the Timestamp from both results
|
|
22
|
+
* 3. Compare the two timestamps
|
|
23
|
+
* 4. The timestamps should be the same, meaning that the page was deduplicated
|
|
24
|
+
*
|
|
25
|
+
* 5. Connect to redis and check if the page was cached in redis and if TTL is set correctly
|
|
26
|
+
*
|
|
27
|
+
* 6. Call the page again, but wait 3 seconds before calling it
|
|
28
|
+
* 7. Extract the Timestamp
|
|
29
|
+
* 8. Compare the timestamp to previous timestamp
|
|
30
|
+
* 9. The timestamp should be the same, meaning that the page was cached (By in-memory cache which is set to 10 seconds by default)
|
|
31
|
+
*
|
|
32
|
+
* 10. Call the page again, but wait 11 seconds before calling it
|
|
33
|
+
* 11. Extract the Timestamp
|
|
34
|
+
* 12. Compare the timestamp to previous timestamp
|
|
35
|
+
* 13. The timestamp should be the same, meaning that the page was cached (By redis cache which becomes active after in-memory cache expires)
|
|
36
|
+
*
|
|
37
|
+
* 14. Connect to redis and check if the page was cached in redis and if TTL is set correctly
|
|
38
|
+
*
|
|
39
|
+
* 15. Check expiration time of the page in redis
|
|
40
|
+
*
|
|
41
|
+
* 16. Call the page again after TTL expiration time
|
|
42
|
+
* 17. Extract the Timestamp
|
|
43
|
+
* 18. Compare the timestamp to previous timestamp
|
|
44
|
+
* 19. The timestamp should be different, meaning that the page was recreated
|
|
45
|
+
*
|
|
46
|
+
* 20. call API which will invalidate the page via a revalidatePage action
|
|
47
|
+
* 21. Call the page again
|
|
48
|
+
* 18. Compare the timestamp to previous timestamp
|
|
49
|
+
* 19. The timestamp should be different, meaning that the page was recreated
|
|
50
|
+
*
|
|
51
|
+
* 20. Connect to redis and delete the page from redis
|
|
52
|
+
* 21. Call the page again, but wait 11 seconds before calling it
|
|
53
|
+
* 22. Compare the timestamp to previous timestamp
|
|
54
|
+
* 23. The timestamp should be different, meaning that the page was recreated
|
|
55
|
+
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
export default async function TestPage() {
|
|
4
|
+
const res = await fetch(
|
|
5
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/revalidated-fetch`,
|
|
6
|
+
);
|
|
7
|
+
const data = await res.json();
|
|
8
|
+
return (
|
|
9
|
+
<main
|
|
10
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
11
|
+
>
|
|
12
|
+
<h1>Test Page</h1>
|
|
13
|
+
<p>Counter: {data.counter}</p>
|
|
14
|
+
<p>This is a test page for integration testing.</p>
|
|
15
|
+
<p>Timestamp: {Date.now()}</p>
|
|
16
|
+
<p>Slug: /test-page</p>
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// This page will inherit the revalidate from it's subsequent fetch request
|
|
2
|
+
// meaning that the page will be revalidated after 15 seconds
|
|
3
|
+
|
|
4
|
+
export default async function TestPage() {
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch(
|
|
7
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/revalidated-fetch`,
|
|
8
|
+
{
|
|
9
|
+
next: {
|
|
10
|
+
revalidate: 15,
|
|
11
|
+
tags: ['revalidated-fetch-revalidate15-default-page'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
);
|
|
15
|
+
const data = await res.json();
|
|
16
|
+
return (
|
|
17
|
+
<main
|
|
18
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
19
|
+
>
|
|
20
|
+
<h1>Test Page</h1>
|
|
21
|
+
<p>Counter: {data.counter}</p>
|
|
22
|
+
<p>This is a test page for integration testing.</p>
|
|
23
|
+
<p>Timestamp: {Date.now()}</p>
|
|
24
|
+
<p>Slug: /test-page</p>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return (
|
|
29
|
+
<p>
|
|
30
|
+
Error: {JSON.stringify(e)} (an error here is normal during build since
|
|
31
|
+
API is not available yet)
|
|
32
|
+
</p>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
export default async function TestPage() {
|
|
4
|
+
const res = await await fetch(
|
|
5
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/revalidated-fetch`,
|
|
6
|
+
{
|
|
7
|
+
next: {
|
|
8
|
+
revalidate: 15,
|
|
9
|
+
tags: ['revalidated-fetch-revalidate15-force-dynamic-page'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
return (
|
|
15
|
+
<main
|
|
16
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
17
|
+
>
|
|
18
|
+
<h1>Test Page</h1>
|
|
19
|
+
<p>Counter: {data.counter}</p>
|
|
20
|
+
<p>This is a test page for integration testing.</p>
|
|
21
|
+
<p>Timestamp: {Date.now()}</p>
|
|
22
|
+
<p>Slug: /test-page</p>
|
|
23
|
+
</main>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
export default async function TestPage() {
|
|
4
|
+
const res = await fetch(
|
|
5
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/uncached-fetch`,
|
|
6
|
+
);
|
|
7
|
+
const data = await res.json();
|
|
8
|
+
return (
|
|
9
|
+
<main
|
|
10
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
11
|
+
>
|
|
12
|
+
<h1>Test Page</h1>
|
|
13
|
+
<p>Counter: {data.counter}</p>
|
|
14
|
+
<p>This is a test page for integration testing.</p>
|
|
15
|
+
<p>Timestamp: {Date.now()}</p>
|
|
16
|
+
<p>Slug: /test-page</p>
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default async function TestPage() {
|
|
2
|
+
try {
|
|
3
|
+
const res = await fetch(
|
|
4
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/uncached-fetch`,
|
|
5
|
+
{
|
|
6
|
+
next: {
|
|
7
|
+
revalidate: 15,
|
|
8
|
+
tags: ['uncached-fetch-revalidate15-default-page'],
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
);
|
|
12
|
+
const data = await res.json();
|
|
13
|
+
return (
|
|
14
|
+
<main
|
|
15
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
16
|
+
>
|
|
17
|
+
<h1>Test Page</h1>
|
|
18
|
+
<p>Counter: {data.counter}</p>
|
|
19
|
+
<p>This is a test page for integration testing.</p>
|
|
20
|
+
<p>Timestamp: {Date.now()}</p>
|
|
21
|
+
<p>Slug: /test-page</p>
|
|
22
|
+
</main>
|
|
23
|
+
);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return (
|
|
26
|
+
<p>
|
|
27
|
+
Error: {JSON.stringify(e)} (an error here is normal during build since
|
|
28
|
+
API is not available yet)
|
|
29
|
+
</p>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
|
|
3
|
+
export default async function TestPage() {
|
|
4
|
+
const res = await fetch(
|
|
5
|
+
`http://localhost:${process.env.NEXT_START_PORT || 3000}/api/uncached-fetch`,
|
|
6
|
+
{
|
|
7
|
+
next: {
|
|
8
|
+
revalidate: 15,
|
|
9
|
+
tags: ['uncached-fetch-revalidate15-force-dynamic-page'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
return (
|
|
15
|
+
<main
|
|
16
|
+
style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
|
|
17
|
+
>
|
|
18
|
+
<h1>Test Page</h1>
|
|
19
|
+
<p>Counter: {data.counter}</p>
|
|
20
|
+
<p>This is a test page for integration testing.</p>
|
|
21
|
+
<p>Timestamp: {Date.now()}</p>
|
|
22
|
+
<p>Slug: /test-page</p>
|
|
23
|
+
</main>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -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,27 @@
|
|
|
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": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
@@ -6,8 +6,10 @@ import { join } from 'path';
|
|
|
6
6
|
import { CacheEntry } from '../../src/RedisStringsHandler';
|
|
7
7
|
import { revalidate as next1503_revalidatedFetch_route } from './next-app-15-0-3/src/app/api/revalidated-fetch/route';
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
|
|
9
|
+
// Select which Next.js test app to use. Can be overridden via NEXT_TEST_APP env var
|
|
10
|
+
// Examples: next-app-15-0-3, next-app-15-3-2, next-app-15-4-7
|
|
11
|
+
const NEXT_TEST_APP = process.env.NEXT_TEST_APP || 'next-app-15-4-7';
|
|
12
|
+
const NEXT_APP_DIR = join(__dirname, NEXT_TEST_APP);
|
|
11
13
|
console.log('NEXT_APP_DIR', NEXT_APP_DIR);
|
|
12
14
|
const NEXT_START_PORT = 3055;
|
|
13
15
|
const NEXT_START_URL = `http://localhost:${NEXT_START_PORT}`;
|
|
@@ -574,8 +576,9 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
574
576
|
expect(data).toBeDefined();
|
|
575
577
|
const cacheEntry: CacheEntry = JSON.parse(data);
|
|
576
578
|
|
|
577
|
-
// The format should be as expected
|
|
578
|
-
|
|
579
|
+
// The format should be as expected. We intentionally do not assert on an optional status field here
|
|
580
|
+
// so that different Next.js versions (which may include or omit it) are both supported.
|
|
581
|
+
expect(cacheEntry).toMatchObject({
|
|
579
582
|
value: {
|
|
580
583
|
kind: 'APP_PAGE',
|
|
581
584
|
html: expect.any(String),
|
|
@@ -587,7 +590,6 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
587
590
|
'x-next-cache-tags':
|
|
588
591
|
'_N_T_/layout,_N_T_/pages/layout,_N_T_/pages/no-fetch/layout,_N_T_/pages/no-fetch/default-page/layout,_N_T_/pages/no-fetch/default-page/page,_N_T_/pages/no-fetch/default-page',
|
|
589
592
|
},
|
|
590
|
-
status: 200,
|
|
591
593
|
},
|
|
592
594
|
lastModified: expect.any(Number),
|
|
593
595
|
tags: [
|