@trieb.work/nextjs-turbo-redis-cache 1.16.2 → 1.18.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 +11 -2
- package/.github/workflows/release.yml +8 -157
- package/ARCHITECTURE.md +86 -47
- package/README.md +91 -25
- package/dist/index.d.mts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +264 -96
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +261 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -7
- package/release.config.cjs +6 -25
- package/src/CacheComponentsHandler.ts +141 -111
- package/src/RedisStringsHandler.ts +37 -25
- package/src/SyncedMap.ts +16 -2
- package/src/index.ts +7 -0
- package/src/utils/cacheTtl.ts +71 -0
- package/src/utils/compareAndUnlink.ts +29 -0
- package/src/utils/redisConnection.ts +17 -0
- package/src/utils/tagRevalidation.ts +162 -0
- package/test/README.md +13 -13
- package/test/nextjs-test-projects/next-app-15-4-11/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-0-11/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/next.config.ts +2 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/route.ts +23 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/page.tsx +22 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
- package/test/nextjs-test-projects/next-app-16-2-6/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +2 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/route.ts +23 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +22 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
- package/test/nextjs-test-projects/next-app-16-3-0/README.md +36 -0
- package/test/nextjs-test-projects/next-app-16-3-0/eslint.config.mjs +18 -0
- package/test/nextjs-test-projects/next-app-16-3-0/next.config.ts +7 -0
- package/test/nextjs-test-projects/next-app-16-3-0/package.json +28 -0
- package/test/nextjs-test-projects/next-app-16-3-0/pnpm-lock.yaml +4284 -0
- package/test/nextjs-test-projects/next-app-16-3-0/postcss.config.mjs +7 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/cached-static-fetch/route.ts +18 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidatePath/route.ts +15 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidateTag/route.ts +20 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidated-fetch/route.ts +17 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/uncached-fetch/route.ts +15 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/favicon.ico +0 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/globals.css +26 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/layout.tsx +59 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/page.tsx +755 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/no-fetch/default-page/page.tsx +55 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/revalidation-interface.tsx +267 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/update-tag-test/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/tsconfig.json +34 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/README.md +36 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/cache-handler.js +3 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/eslint.config.mjs +18 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/next.config.ts +15 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/package.json +28 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/pnpm-lock.yaml +4284 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/postcss.config.mjs +7 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/file.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/globe.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/next.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/vercel.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/window.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/route.ts +23 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/page.tsx +112 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/favicon.ico +0 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/globals.css +26 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/layout.tsx +57 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/page.tsx +755 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/revalidation-interface.tsx +267 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/update-tag-test/page.tsx +22 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/tsconfig.json +34 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +4 -4
- package/test/nextjs-test-projects/next-pages-16-3-0/README.md +16 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/eslint.config.mjs +18 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/next.config.ts +7 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/package.json +26 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/pnpm-lock.yaml +3939 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/api/revalidate.ts +24 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/index.tsx +11 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/isr/[slug].tsx +49 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/static-forever.tsx +20 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/tsconfig.json +34 -0
- package/test/playwright/cache-lab.spec.ts +75 -0
- package/test/vitest/integration/cache-components/cache-components.integration.test.ts +194 -36
- package/test/vitest/integration/nextjs-cache-handler.integration.test.ts +67 -34
- package/test/vitest/integration/pages-router.integration.test.ts +16 -6
- package/test/vitest/unit/SyncedMap-orphan-cleanup.test.ts +111 -0
- package/test/vitest/unit/cache-components-update-tags.test.ts +350 -0
- package/test/vitest/unit/cache-ttl.test.ts +67 -0
- package/test/vitest/unit/compare-and-unlink.test.ts +22 -0
- package/test/vitest/unit/index.test.ts +17 -0
- package/test/vitest/unit/pages-router-kinds.test.ts +11 -0
- package/test/vitest/unit/redis-connection.test.ts +39 -0
- package/test/vitest/unit/tag-revalidation.test.ts +178 -0
- package/CHANGELOG.md +0 -404
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
import { cacheLife, cacheTag, revalidateTag } from 'next/cache';
|
|
4
|
+
|
|
5
|
+
function sleep(ms: number): Promise<void> {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function getSlowTaggedValue() {
|
|
10
|
+
'use cache';
|
|
11
|
+
|
|
12
|
+
// Keep this short so you can observe SWR behavior quickly.
|
|
13
|
+
// stale: after 2s the entry is considered stale
|
|
14
|
+
// revalidate: after 8s Next may attempt to refresh
|
|
15
|
+
// expire: after 60s it must be recomputed
|
|
16
|
+
cacheLife({ stale: 2, revalidate: 8, expire: 60 });
|
|
17
|
+
cacheTag('cache-lab:swr');
|
|
18
|
+
|
|
19
|
+
// Make regeneration visibly expensive so you can tell whether the request was
|
|
20
|
+
// served from stale cache (fast) or blocked on recomputation (slow).
|
|
21
|
+
await sleep(3000);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
computedAt: Date.now(),
|
|
25
|
+
value: Math.random(),
|
|
26
|
+
note: 'This function always sleeps ~3s when it actually executes.',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function triggerRevalidateTag() {
|
|
31
|
+
'use server';
|
|
32
|
+
revalidateTag('cache-lab:swr', { expire: 1 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function CachedPanel() {
|
|
36
|
+
const data = await getSlowTaggedValue();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="rounded-lg border p-5">
|
|
40
|
+
<div className="text-sm text-slate-700">
|
|
41
|
+
<div>
|
|
42
|
+
<span className="font-medium">computedAt:</span>{' '}
|
|
43
|
+
<span data-testid="computedAt" className="font-mono">
|
|
44
|
+
{data.computedAt}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="mt-1">
|
|
48
|
+
<span className="font-medium">value:</span>{' '}
|
|
49
|
+
<span data-testid="value" className="font-mono">
|
|
50
|
+
{data.value}
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="mt-1 text-xs text-slate-500">{data.note}</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function StaleWhileRevalidatePage() {
|
|
60
|
+
return (
|
|
61
|
+
<main className="mx-auto max-w-3xl p-10">
|
|
62
|
+
<div className="mb-6">
|
|
63
|
+
<Link
|
|
64
|
+
className="text-sm text-blue-600 hover:underline"
|
|
65
|
+
href="/cache-lab"
|
|
66
|
+
>
|
|
67
|
+
← Back to Cache Lab
|
|
68
|
+
</Link>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<header className="mb-6">
|
|
72
|
+
<h1 className="text-2xl font-semibold">
|
|
73
|
+
SWR: stale-while-revalidate (slow)
|
|
74
|
+
</h1>
|
|
75
|
+
<p className="mt-2 text-sm text-slate-600">
|
|
76
|
+
This page is designed to make stale-while-revalidate behavior visible.
|
|
77
|
+
The cached function sleeps ~3 seconds whenever it really runs.
|
|
78
|
+
</p>
|
|
79
|
+
</header>
|
|
80
|
+
|
|
81
|
+
<Suspense
|
|
82
|
+
fallback={
|
|
83
|
+
<div className="rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
|
|
84
|
+
Loading cached content… (first time can take ~3s)
|
|
85
|
+
</div>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<CachedPanel />
|
|
89
|
+
</Suspense>
|
|
90
|
+
|
|
91
|
+
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
|
92
|
+
<form action={triggerRevalidateTag}>
|
|
93
|
+
<button
|
|
94
|
+
className="w-full rounded-md border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
|
|
95
|
+
type="submit"
|
|
96
|
+
>
|
|
97
|
+
revalidateTag('cache-lab:swr')
|
|
98
|
+
</button>
|
|
99
|
+
</form>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="mt-6 rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
<p className="font-medium">How to observe SWR (non-blocking)</p>
|
|
105
|
+
<p>1) Load this page once (expect ~3s on the very first load).</p>
|
|
106
|
+
<p>
|
|
107
|
+
2) Wait ~3 seconds so the entry becomes <code>stale</code>{' '}
|
|
108
|
+
(stale=2s).
|
|
109
|
+
</p>
|
|
110
|
+
<p>
|
|
111
|
+
3) Click <code>revalidateTag</code>.
|
|
112
|
+
</p>
|
|
113
|
+
<p>4) Immediately reload the page:</p>
|
|
114
|
+
<div className="rounded bg-white p-3 text-xs">
|
|
115
|
+
If SWR is working, the reload should be fast and you may still see
|
|
116
|
+
the old computedAt/value (stale served).
|
|
117
|
+
<br />
|
|
118
|
+
Reload again after ~3-4 seconds and you should see a new
|
|
119
|
+
computedAt/value.
|
|
120
|
+
<br />
|
|
121
|
+
If SWR is not working, the reload will block for ~3 seconds.
|
|
122
|
+
</div>
|
|
123
|
+
<div className="text-xs text-slate-500">
|
|
124
|
+
cacheLife: {'{ stale: 2, revalidate: 8, expire: 60 }'}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</main>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { cacheTag, updateTag, revalidateTag, cacheLife } from 'next/cache';
|
|
3
|
+
import { Suspense } from 'react';
|
|
4
|
+
|
|
5
|
+
async function getTaggedData() {
|
|
6
|
+
'use cache';
|
|
7
|
+
|
|
8
|
+
cacheLife({ stale: 5, revalidate: 10, expire: 60 });
|
|
9
|
+
cacheTag('cache-lab:tag');
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
value: Math.random(),
|
|
13
|
+
createdAt: Date.now(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function doUpdateTag() {
|
|
18
|
+
'use server';
|
|
19
|
+
updateTag('cache-lab:tag');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function doRevalidateTag() {
|
|
23
|
+
'use server';
|
|
24
|
+
revalidateTag('cache-lab:tag', { expire: 1 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function CachedTaggedPanel() {
|
|
28
|
+
const data = await getTaggedData();
|
|
29
|
+
return (
|
|
30
|
+
<div className="rounded-lg border p-5">
|
|
31
|
+
<dl className="grid grid-cols-1 gap-3 text-sm">
|
|
32
|
+
<div>
|
|
33
|
+
<dt className="font-medium">createdAt</dt>
|
|
34
|
+
<dd data-testid="createdAt" className="font-mono text-slate-700">
|
|
35
|
+
{data.createdAt}
|
|
36
|
+
</dd>
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<dt className="font-medium">value</dt>
|
|
40
|
+
<dd data-testid="value" className="font-mono text-slate-700">
|
|
41
|
+
{data.value}
|
|
42
|
+
</dd>
|
|
43
|
+
</div>
|
|
44
|
+
<div>
|
|
45
|
+
<dt className="font-medium">cacheLife</dt>
|
|
46
|
+
<dd className="font-mono text-slate-700">
|
|
47
|
+
{'{ stale: 5, revalidate: 10, expire: 60 }'}
|
|
48
|
+
</dd>
|
|
49
|
+
</div>
|
|
50
|
+
</dl>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function TagInvalidationPage() {
|
|
56
|
+
return (
|
|
57
|
+
<main className="mx-auto max-w-3xl p-10">
|
|
58
|
+
<div className="mb-6">
|
|
59
|
+
<Link
|
|
60
|
+
className="text-sm text-blue-600 hover:underline"
|
|
61
|
+
href="/cache-lab"
|
|
62
|
+
>
|
|
63
|
+
← Back to Cache Lab
|
|
64
|
+
</Link>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<header className="mb-6">
|
|
68
|
+
<h1 className="text-2xl font-semibold">
|
|
69
|
+
Tags: updateTag vs revalidateTag
|
|
70
|
+
</h1>
|
|
71
|
+
<p className="mt-2 text-sm text-slate-600">
|
|
72
|
+
This page caches a function tagged with{' '}
|
|
73
|
+
<code>cacheTag('cache-lab:tag')</code>. Use the buttons to invalidate.
|
|
74
|
+
</p>
|
|
75
|
+
</header>
|
|
76
|
+
|
|
77
|
+
<Suspense
|
|
78
|
+
fallback={
|
|
79
|
+
<div className="rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
|
|
80
|
+
Loading cached tagged content…
|
|
81
|
+
</div>
|
|
82
|
+
}
|
|
83
|
+
>
|
|
84
|
+
<CachedTaggedPanel />
|
|
85
|
+
</Suspense>
|
|
86
|
+
|
|
87
|
+
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
|
88
|
+
<form action={doUpdateTag}>
|
|
89
|
+
<button
|
|
90
|
+
className="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
91
|
+
type="submit"
|
|
92
|
+
>
|
|
93
|
+
updateTag('cache-lab:tag')
|
|
94
|
+
</button>
|
|
95
|
+
</form>
|
|
96
|
+
|
|
97
|
+
<form action={doRevalidateTag}>
|
|
98
|
+
<button
|
|
99
|
+
className="w-full rounded-md border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
|
|
100
|
+
type="submit"
|
|
101
|
+
>
|
|
102
|
+
revalidateTag('cache-lab:tag')
|
|
103
|
+
</button>
|
|
104
|
+
</form>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="mt-6 rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
|
|
108
|
+
<div className="space-y-2">
|
|
109
|
+
<p>Expected: values should stay stable on reloads.</p>
|
|
110
|
+
<p>
|
|
111
|
+
<strong>updateTag</strong> is intended for “expire + immediately
|
|
112
|
+
refresh within the same request” (used after mutations in Server
|
|
113
|
+
Actions).
|
|
114
|
+
</p>
|
|
115
|
+
<p>
|
|
116
|
+
<strong>revalidateTag</strong> invalidates tagged entries with
|
|
117
|
+
stale-while-revalidate behavior.
|
|
118
|
+
</p>
|
|
119
|
+
<p>
|
|
120
|
+
This page deliberately puts the async work behind{' '}
|
|
121
|
+
<code>{'<Suspense>'}</code> to avoid the "Blocking Route" warning.
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</main>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { cacheTag, updateTag } from 'next/cache';
|
|
3
|
+
|
|
4
|
+
async function getCachedNonDeterministicValues() {
|
|
5
|
+
'use cache';
|
|
6
|
+
|
|
7
|
+
cacheTag('cache-lab:nondet');
|
|
8
|
+
|
|
9
|
+
const random1 = Math.random();
|
|
10
|
+
const random2 = Math.random();
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
const uuid = crypto.randomUUID();
|
|
13
|
+
const bytes = crypto.getRandomValues(new Uint8Array(8));
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
random1,
|
|
17
|
+
random2,
|
|
18
|
+
now,
|
|
19
|
+
uuid,
|
|
20
|
+
bytes: Array.from(bytes),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function refresh() {
|
|
25
|
+
'use server';
|
|
26
|
+
updateTag('cache-lab:nondet');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default async function UseCacheNonDeterministicPage() {
|
|
30
|
+
const data = await getCachedNonDeterministicValues();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<main className="mx-auto max-w-3xl p-10">
|
|
34
|
+
<div className="mb-6">
|
|
35
|
+
<Link
|
|
36
|
+
className="text-sm text-blue-600 hover:underline"
|
|
37
|
+
href="/cache-lab"
|
|
38
|
+
>
|
|
39
|
+
← Back to Cache Lab
|
|
40
|
+
</Link>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<header className="mb-6">
|
|
44
|
+
<h1 className="text-2xl font-semibold">use cache: non-deterministic</h1>
|
|
45
|
+
<p className="mt-2 text-sm text-slate-600">
|
|
46
|
+
Per docs, non-deterministic operations inside a <code>use cache</code>{' '}
|
|
47
|
+
scope run once and then stay stable for all requests until you
|
|
48
|
+
invalidate.
|
|
49
|
+
</p>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<form action={refresh} className="mb-6">
|
|
53
|
+
<button
|
|
54
|
+
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
55
|
+
type="submit"
|
|
56
|
+
>
|
|
57
|
+
updateTag('cache-lab:nondet')
|
|
58
|
+
</button>
|
|
59
|
+
</form>
|
|
60
|
+
|
|
61
|
+
<div className="rounded-lg border p-5">
|
|
62
|
+
<dl className="grid grid-cols-1 gap-3 text-sm">
|
|
63
|
+
<div>
|
|
64
|
+
<dt className="font-medium">random1</dt>
|
|
65
|
+
<dd data-testid="random1" className="font-mono text-slate-700">
|
|
66
|
+
{data.random1}
|
|
67
|
+
</dd>
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<dt className="font-medium">random2</dt>
|
|
71
|
+
<dd data-testid="random2" className="font-mono text-slate-700">
|
|
72
|
+
{data.random2}
|
|
73
|
+
</dd>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<dt className="font-medium">now (Date.now)</dt>
|
|
77
|
+
<dd data-testid="now" className="font-mono text-slate-700">
|
|
78
|
+
{data.now}
|
|
79
|
+
</dd>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<dt className="font-medium">uuid</dt>
|
|
83
|
+
<dd data-testid="uuid" className="font-mono text-slate-700">
|
|
84
|
+
{data.uuid}
|
|
85
|
+
</dd>
|
|
86
|
+
</div>
|
|
87
|
+
<div>
|
|
88
|
+
<dt className="font-medium">bytes</dt>
|
|
89
|
+
<dd data-testid="bytes" className="font-mono text-slate-700">
|
|
90
|
+
{JSON.stringify(data.bytes)}
|
|
91
|
+
</dd>
|
|
92
|
+
</div>
|
|
93
|
+
</dl>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="mt-6 rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
|
|
97
|
+
<div className="space-y-2">
|
|
98
|
+
<p>
|
|
99
|
+
Expected: Reloading the page should keep values identical across
|
|
100
|
+
requests.
|
|
101
|
+
</p>
|
|
102
|
+
<p>
|
|
103
|
+
Click the button (Server Action) to run <code>updateTag</code> and
|
|
104
|
+
refresh the cached entry.
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</main>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
import { cacheTag, updateTag } from 'next/cache';
|
|
4
|
+
|
|
5
|
+
async function getRemoteCachedValue() {
|
|
6
|
+
'use cache: remote';
|
|
7
|
+
|
|
8
|
+
cacheTag('cache-lab:remote');
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
value: Math.random(),
|
|
12
|
+
computedAt: Date.now(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function triggerUpdateTag() {
|
|
17
|
+
'use server';
|
|
18
|
+
updateTag('cache-lab:remote');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function RemotePanel() {
|
|
22
|
+
const data = await getRemoteCachedValue();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="rounded-lg border p-5">
|
|
26
|
+
<div className="text-sm text-slate-700">
|
|
27
|
+
<div>
|
|
28
|
+
<span className="font-medium">computedAt:</span>{' '}
|
|
29
|
+
<span data-testid="computedAt" className="font-mono">
|
|
30
|
+
{data.computedAt}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="mt-1">
|
|
34
|
+
<span className="font-medium">value:</span>{' '}
|
|
35
|
+
<span data-testid="value" className="font-mono">
|
|
36
|
+
{data.value}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function UseCacheRemotePage() {
|
|
45
|
+
return (
|
|
46
|
+
<main className="mx-auto max-w-3xl p-10">
|
|
47
|
+
<div className="mb-6">
|
|
48
|
+
<Link
|
|
49
|
+
className="text-sm text-blue-600 hover:underline"
|
|
50
|
+
href="/cache-lab"
|
|
51
|
+
>
|
|
52
|
+
← Back to Cache Lab
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<header className="mb-6">
|
|
57
|
+
<h1 className="text-2xl font-semibold">use cache: remote</h1>
|
|
58
|
+
<p className="mt-2 text-sm text-slate-600">
|
|
59
|
+
Exercises the <code>cacheHandlers.remote</code> handler backed by
|
|
60
|
+
Redis. Values should stay stable across reloads until{' '}
|
|
61
|
+
<code>updateTag</code> runs.
|
|
62
|
+
</p>
|
|
63
|
+
</header>
|
|
64
|
+
|
|
65
|
+
<Suspense
|
|
66
|
+
fallback={
|
|
67
|
+
<div className="rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
|
|
68
|
+
Loading remote cached content…
|
|
69
|
+
</div>
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
<RemotePanel />
|
|
73
|
+
</Suspense>
|
|
74
|
+
|
|
75
|
+
<form action={triggerUpdateTag} className="mt-6">
|
|
76
|
+
<button
|
|
77
|
+
className="rounded-md border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
|
|
78
|
+
type="submit"
|
|
79
|
+
>
|
|
80
|
+
updateTag('cache-lab:remote')
|
|
81
|
+
</button>
|
|
82
|
+
</form>
|
|
83
|
+
</main>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: #ffffff;
|
|
5
|
+
--foreground: #171717;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--background);
|
|
10
|
+
--color-foreground: var(--foreground);
|
|
11
|
+
--font-sans: var(--font-geist-sans);
|
|
12
|
+
--font-mono: var(--font-geist-mono);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--background: #0a0a0a;
|
|
18
|
+
--foreground: #ededed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
background: var(--background);
|
|
24
|
+
color: var(--foreground);
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// This layout is used to test the revalidation logic
|
|
2
|
+
// The layout itself will not create any cache entries
|
|
3
|
+
// It will just be used to render a page
|
|
4
|
+
// If the layout itself uses a fetch request, it will be handled the same way as if the page itself would use a fetch request
|
|
5
|
+
// The layout can be revalidated as well by using revalidatePath with the path of the layout file. However, this will not revalidate the subsequent fetch requests
|
|
6
|
+
|
|
7
|
+
import type { Metadata } from 'next';
|
|
8
|
+
import { Geist, Geist_Mono } from 'next/font/google';
|
|
9
|
+
import './globals.css';
|
|
10
|
+
|
|
11
|
+
const geistSans = Geist({
|
|
12
|
+
variable: '--font-geist-sans',
|
|
13
|
+
subsets: ['latin'],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const geistMono = Geist_Mono({
|
|
17
|
+
variable: '--font-geist-mono',
|
|
18
|
+
subsets: ['latin'],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const metadata: Metadata = {
|
|
22
|
+
title: 'Create Next App',
|
|
23
|
+
description: 'Generated by create next app',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default async function RootLayout({
|
|
27
|
+
children,
|
|
28
|
+
}: Readonly<{
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}>) {
|
|
31
|
+
// let res;
|
|
32
|
+
// try {
|
|
33
|
+
// res = await fetch(`https://httpbin.org/get`, {
|
|
34
|
+
// next: {
|
|
35
|
+
// revalidate: 15,
|
|
36
|
+
// tags: ['httpbin-layout-revalidate15'],
|
|
37
|
+
// },
|
|
38
|
+
// });
|
|
39
|
+
// } catch (e) {
|
|
40
|
+
// // ECONNREFUSED is expected during build
|
|
41
|
+
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
// if (((e as Error).cause as any)?.code !== 'ECONNREFUSED') {
|
|
43
|
+
// throw e;
|
|
44
|
+
// }
|
|
45
|
+
// }
|
|
46
|
+
|
|
47
|
+
// const data = res?.ok ? await res.json() : { origin: -1 };
|
|
48
|
+
return (
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<body
|
|
51
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
);
|
|
57
|
+
}
|