Skip to content

Commit 0f34ffb

Browse files
authored
test(e2e): Add Next.js cache component nesting scenarios (#24704)
Add tests for 4 different nested cache component scenarios. Currently all in a "failed" state as this is not yet implemented. ``` Legend T1, T2, T3 traces, one per request, in order ck:X cache.key digest of entry X o--> put span link (sentry.link.type: 'cache_origin'), target = the cache.put span ``` ## 1. Cached layout in the middle ``` app/(cached-nesting)/cached-mid-layout/ [id]/layout.tsx 'use cache' (hours), key <- awaited [id] [id]/page.tsx dynamic (awaits headers) Request 1 (fill) Request 2 (hit) T1: GET /cached-mid-layout/[id] T2: GET /cached-mid-layout/[id] |- cache.get hit=false ck:layout |- cache.get hit=true ck:layout |- cache.put ck:layout <-----------------o cache_origin |- dynamic page render |- dynamic page render (no cache spans) ``` ## 2. Dynamic Layouts, Cached Leaf ``` app/(cached-nesting)/dynamic-layouts/ [id]/layout.tsx dynamic (awaits headers) [id]/layout-cached-leaf/page.tsx page renders CachedLeaf ('use cache', hours) Request 1 (fill) Request 2 (hit) T1: GET /dynamic-layouts/[id]/... T2: GET /dynamic-layouts/[id]/... |- cache.put ck:leaf <-------------------o cache.get hit=true ck:leaf ``` ## 3. Mixed lifetimes: two cache links, but pointing to a different origin ``` app/(cached-nesting)/mixed-lifetimes/ [id]/layout.tsx 'use cache', expire: 2s <- short-lived [id]/page.tsx dynamic + LongLivedComponent ('use cache', hours) T1 (fill both) --sleep 3s--> T2 (layout expired) T3 (both hit) |- put ck:layout |- get hit=false ck:layout |- get hit=true ck:layout |- put ck:cmp |- put ck:layout <------------o cache_origin ^ |- get hit=true ck:cmp |- get hit=true ck:cmp | | +---------------------------------------------------------------o cache_origin ``` ## 4. Shared layout across sibling routes: Origin trace is different than serving (`/a` and `/b`) ``` app/(cached-nesting)/shared-layout/ [id]/layout.tsx 'use cache' (hours) — ONE entry shared by a and b [id]/a/page.tsx dynamic [id]/b/page.tsx dynamic Request 1: /shared-layout/<id>/a Request 2: /shared-layout/<id>/b T1: GET /shared-layout/[id]/a T2: GET /shared-layout/[id]/b |- cache.put ck:layout <-----------------o cache.get hit=true ck:layout ``` Closes #24701
1 parent b7b7e04 commit 0f34ffb

23 files changed

Lines changed: 753 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { ReactNode } from 'react';
2+
import { cacheLife } from 'next/cache';
3+
4+
// The awaited param puts the request id into the cache key, so a fresh id is a guaranteed miss
5+
// and the entry cannot come from a build-time fill. `children` passes through as an uncached hole.
6+
export default async function CachedMidLayout({
7+
children,
8+
params,
9+
}: {
10+
children: ReactNode;
11+
params: Promise<{ id: string }>;
12+
}) {
13+
'use cache';
14+
cacheLife('hours');
15+
const { id } = await params;
16+
await new Promise(resolve => setTimeout(resolve, 100));
17+
return (
18+
<div data-testid="cached-mid-layout">
19+
<p id="cached-layout-stamp">
20+
{id}:{Date.now()}
21+
</p>
22+
{children}
23+
</div>
24+
);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { headers } from 'next/headers';
2+
3+
export default async function Page() {
4+
await headers();
5+
return <p id="dynamic-leaf">Dynamic leaf: {Date.now()}</p>;
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { cacheLife } from 'next/cache';
2+
3+
async function CachedLeaf({ id }: { id: string }) {
4+
'use cache';
5+
cacheLife('hours');
6+
await new Promise(resolve => setTimeout(resolve, 100));
7+
return (
8+
<p id="cached-leaf">
9+
{id}:{Date.now()}
10+
</p>
11+
);
12+
}
13+
14+
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
15+
const { id } = await params;
16+
return <CachedLeaf id={id} />;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { ReactNode } from 'react';
2+
import { headers } from 'next/headers';
3+
4+
export default async function DynamicLayout({ children }: { children: ReactNode }) {
5+
await headers();
6+
return <div data-testid="dynamic-layout">{children}</div>;
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Suspense, type ReactNode } from 'react';
2+
3+
// The Suspense boundary lets the nested layouts/pages below use request APIs (`headers()`,
4+
// runtime params) without tripping the Cache Components prerender guards.
5+
export default function Layout({ children }: { children: ReactNode }) {
6+
return (
7+
<div data-testid="cached-nesting-group">
8+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
9+
</div>
10+
);
11+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ReactNode } from 'react';
2+
import { cacheLife } from 'next/cache';
3+
4+
// Hard-expires after 2s while the page's cached component lives for hours, so one request can
5+
// refill the layout while hitting the component: two cached levels with different origin traces.
6+
export default async function ShortLivedLayout({
7+
children,
8+
params,
9+
}: {
10+
children: ReactNode;
11+
params: Promise<{ id: string }>;
12+
}) {
13+
'use cache';
14+
cacheLife({ revalidate: 1, expire: 2 });
15+
const { id } = await params;
16+
return (
17+
<div data-testid="short-lived-layout">
18+
<p id="layout-stamp">
19+
{id}:{Date.now()}
20+
</p>
21+
{children}
22+
</div>
23+
);
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { headers } from 'next/headers';
2+
import { cacheLife } from 'next/cache';
3+
4+
async function LongLivedComponent({ id }: { id: string }) {
5+
'use cache';
6+
cacheLife('hours');
7+
await new Promise(resolve => setTimeout(resolve, 100));
8+
return (
9+
<p id="component-stamp">
10+
{id}:{Date.now()}
11+
</p>
12+
);
13+
}
14+
15+
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
16+
const { id } = await params;
17+
await headers();
18+
return <LongLivedComponent id={id} />;
19+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { headers } from 'next/headers';
2+
3+
export default async function Page() {
4+
await headers();
5+
return <p id="route-a">Route a: {Date.now()}</p>;
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { headers } from 'next/headers';
2+
3+
export default async function Page() {
4+
await headers();
5+
return <p id="route-b">Route b: {Date.now()}</p>;
6+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ReactNode } from 'react';
2+
import { cacheLife } from 'next/cache';
3+
4+
// One cache entry (keyed by id) shared by the sibling routes `a` and `b` below.
5+
export default async function SharedLayout({
6+
children,
7+
params,
8+
}: {
9+
children: ReactNode;
10+
params: Promise<{ id: string }>;
11+
}) {
12+
'use cache';
13+
cacheLife('hours');
14+
const { id } = await params;
15+
await new Promise(resolve => setTimeout(resolve, 100));
16+
return (
17+
<div data-testid="shared-layout">
18+
<p id="shared-layout-stamp">
19+
{id}:{Date.now()}
20+
</p>
21+
{children}
22+
</div>
23+
);
24+
}

0 commit comments

Comments
 (0)