@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,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,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
|
+
}
|