@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.
Files changed (125) hide show
  1. package/.github/workflows/ci.yml +11 -2
  2. package/.github/workflows/release.yml +8 -157
  3. package/ARCHITECTURE.md +86 -47
  4. package/README.md +91 -25
  5. package/dist/index.d.mts +40 -1
  6. package/dist/index.d.ts +40 -1
  7. package/dist/index.js +264 -96
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +261 -95
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +5 -7
  12. package/release.config.cjs +6 -25
  13. package/src/CacheComponentsHandler.ts +141 -111
  14. package/src/RedisStringsHandler.ts +37 -25
  15. package/src/SyncedMap.ts +16 -2
  16. package/src/index.ts +7 -0
  17. package/src/utils/cacheTtl.ts +71 -0
  18. package/src/utils/compareAndUnlink.ts +29 -0
  19. package/src/utils/redisConnection.ts +17 -0
  20. package/src/utils/tagRevalidation.ts +162 -0
  21. package/test/README.md +13 -13
  22. package/test/nextjs-test-projects/next-app-15-4-11/pnpm-lock.yaml +1 -1
  23. package/test/nextjs-test-projects/next-app-16-0-11/pnpm-lock.yaml +1 -1
  24. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/next.config.ts +2 -0
  25. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +1 -1
  26. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  27. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  28. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/page.tsx +22 -0
  29. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  30. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  31. package/test/nextjs-test-projects/next-app-16-2-6/pnpm-lock.yaml +1 -1
  32. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +2 -0
  33. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/pnpm-lock.yaml +1 -1
  34. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  35. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  36. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +22 -0
  37. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  38. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  39. package/test/nextjs-test-projects/next-app-16-3-0/README.md +36 -0
  40. package/test/nextjs-test-projects/next-app-16-3-0/eslint.config.mjs +18 -0
  41. package/test/nextjs-test-projects/next-app-16-3-0/next.config.ts +7 -0
  42. package/test/nextjs-test-projects/next-app-16-3-0/package.json +28 -0
  43. package/test/nextjs-test-projects/next-app-16-3-0/pnpm-lock.yaml +4284 -0
  44. package/test/nextjs-test-projects/next-app-16-3-0/postcss.config.mjs +7 -0
  45. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/cached-static-fetch/route.ts +18 -0
  46. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
  47. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidatePath/route.ts +15 -0
  48. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidateTag/route.ts +20 -0
  49. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidated-fetch/route.ts +17 -0
  50. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/uncached-fetch/route.ts +15 -0
  51. package/test/nextjs-test-projects/next-app-16-3-0/src/app/favicon.ico +0 -0
  52. package/test/nextjs-test-projects/next-app-16-3-0/src/app/globals.css +26 -0
  53. package/test/nextjs-test-projects/next-app-16-3-0/src/app/layout.tsx +59 -0
  54. package/test/nextjs-test-projects/next-app-16-3-0/src/app/page.tsx +755 -0
  55. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
  56. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
  57. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  58. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/no-fetch/default-page/page.tsx +55 -0
  59. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
  60. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
  61. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  62. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
  63. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
  64. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  65. package/test/nextjs-test-projects/next-app-16-3-0/src/app/revalidation-interface.tsx +267 -0
  66. package/test/nextjs-test-projects/next-app-16-3-0/src/app/update-tag-test/page.tsx +25 -0
  67. package/test/nextjs-test-projects/next-app-16-3-0/tsconfig.json +34 -0
  68. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/README.md +36 -0
  69. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/cache-handler.js +3 -0
  70. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/eslint.config.mjs +18 -0
  71. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/next.config.ts +15 -0
  72. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/package.json +28 -0
  73. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/pnpm-lock.yaml +4284 -0
  74. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/postcss.config.mjs +7 -0
  75. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/file.svg +1 -0
  76. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/globe.svg +1 -0
  77. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/next.svg +1 -0
  78. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/vercel.svg +1 -0
  79. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/window.svg +1 -0
  80. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  81. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  82. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  83. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  84. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  85. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  86. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  87. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  88. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/page.tsx +112 -0
  89. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  90. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  91. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  92. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  93. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  94. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  95. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/favicon.ico +0 -0
  96. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/globals.css +26 -0
  97. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/layout.tsx +57 -0
  98. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/page.tsx +755 -0
  99. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/revalidation-interface.tsx +267 -0
  100. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/update-tag-test/page.tsx +22 -0
  101. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/tsconfig.json +34 -0
  102. package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +4 -4
  103. package/test/nextjs-test-projects/next-pages-16-3-0/README.md +16 -0
  104. package/test/nextjs-test-projects/next-pages-16-3-0/eslint.config.mjs +18 -0
  105. package/test/nextjs-test-projects/next-pages-16-3-0/next.config.ts +7 -0
  106. package/test/nextjs-test-projects/next-pages-16-3-0/package.json +26 -0
  107. package/test/nextjs-test-projects/next-pages-16-3-0/pnpm-lock.yaml +3939 -0
  108. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/api/revalidate.ts +24 -0
  109. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/index.tsx +11 -0
  110. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/isr/[slug].tsx +49 -0
  111. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/static-forever.tsx +20 -0
  112. package/test/nextjs-test-projects/next-pages-16-3-0/tsconfig.json +34 -0
  113. package/test/playwright/cache-lab.spec.ts +75 -0
  114. package/test/vitest/integration/cache-components/cache-components.integration.test.ts +194 -36
  115. package/test/vitest/integration/nextjs-cache-handler.integration.test.ts +67 -34
  116. package/test/vitest/integration/pages-router.integration.test.ts +16 -6
  117. package/test/vitest/unit/SyncedMap-orphan-cleanup.test.ts +111 -0
  118. package/test/vitest/unit/cache-components-update-tags.test.ts +350 -0
  119. package/test/vitest/unit/cache-ttl.test.ts +67 -0
  120. package/test/vitest/unit/compare-and-unlink.test.ts +22 -0
  121. package/test/vitest/unit/index.test.ts +17 -0
  122. package/test/vitest/unit/pages-router-kinds.test.ts +11 -0
  123. package/test/vitest/unit/redis-connection.test.ts +39 -0
  124. package/test/vitest/unit/tag-revalidation.test.ts +178 -0
  125. 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(&apos;cache-lab:remote&apos;)
81
+ </button>
82
+ </form>
83
+ </main>
84
+ );
85
+ }
@@ -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
+ }