Skip to content

Commit 826296e

Browse files
authored
Fixes fetch cache writing to file system allowing instrumentation to prepopulate it. (#65)
* Enables fetch cache writing Adds functionality to write fetch cache data to the file system, allowing for persistent storage of fetched data. * Adds ISR example * PPR example
1 parent 81a2565 commit 826296e

File tree

7 files changed

+203
-42
lines changed

7 files changed

+203
-42
lines changed

examples/redis-minimal/next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const nextConfig: NextConfig = {
66
? require.resolve("./cache-handler.mjs")
77
: undefined,
88
cacheMaxMemorySize: 0, // disable default in-memory caching
9+
experimental: {
10+
ppr: "incremental",
11+
},
912
};
1013

1114
export default nextConfig;

examples/redis-minimal/package-lock.json

Lines changed: 40 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/redis-minimal/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@fortedigital/nextjs-cache-handler": "^2.0.2",
17-
"next": "15.4.3",
17+
"next": "^15.5.1-canary.7",
1818
"react": "^19.1.0",
1919
"react-dom": "^19.1.0",
2020
"redis": "^5.6.1"
@@ -25,7 +25,7 @@
2525
"@types/node": "^24",
2626
"@types/react": "^19",
2727
"@types/react-dom": "^19",
28-
"eslint": "^9",
28+
"eslint": "^9",
2929
"eslint-config-next": "15.4.3",
3030
"tailwindcss": "^4",
3131
"typescript": "^5"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
interface Post {
2+
id: string
3+
title: string
4+
content: string
5+
}
6+
7+
// Next.js will invalidate the cache when a
8+
// request comes in, at most once every 60 seconds.
9+
export const revalidate = 60
10+
11+
export async function generateStaticParams() {
12+
const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
13+
res.json()
14+
)
15+
return posts.map((post) => ({
16+
id: String(post.id),
17+
}))
18+
}
19+
20+
export default async function Page({
21+
params,
22+
}: {
23+
params: Promise<{ id: string }>
24+
}) {
25+
const { id } = await params
26+
const post: Post = await fetch(`https://api.vercel.app/blog/${id}`).then(
27+
(res) => res.json()
28+
)
29+
return (
30+
<main>
31+
<h1>{post.title}</h1>
32+
<p>{post.content}</p>
33+
</main>
34+
)
35+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export async function Example({
2+
searchParams,
3+
}: {
4+
searchParams: Promise<{ characterId: string }>;
5+
}) {
6+
const characterId = (await searchParams).characterId;
7+
try {
8+
const characterResponse = await fetch(
9+
`https://api.sampleapis.com/futurama/characters/${characterId}`,
10+
{
11+
next: {
12+
revalidate: 86400, // 24 hours in seconds
13+
tags: ["futurama"],
14+
},
15+
}
16+
);
17+
const character = await characterResponse.json();
18+
const name = character.name.first;
19+
return (
20+
<div>
21+
<h1>Name: {name}</h1>
22+
<span>{new Date().toISOString()}</span>
23+
</div>
24+
);
25+
} catch (error) {
26+
console.error("Error fetching character data:", error);
27+
return (
28+
<div>
29+
<span>An error occurred during fetch</span>
30+
</div>
31+
);
32+
}
33+
}
34+
35+
export async function Skeleton() {
36+
return <div>Loading...</div>;
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Suspense } from "react";
2+
import { Example, Skeleton } from "./Example";
3+
4+
export default function Page({
5+
searchParams,
6+
}: {
7+
searchParams: Promise<{ characterId: string }>;
8+
}) {
9+
return (
10+
<section>
11+
<h1>This will be prerendered</h1>
12+
<Suspense fallback={<Skeleton />}>
13+
<Example searchParams={searchParams} />
14+
</Suspense>
15+
</section>
16+
);
17+
}
18+
19+
export const experimental_ppr = true;

packages/nextjs-cache-handler/src/handlers/cache-handler.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
type GetIncrementalResponseCacheContext,
2121
type GetIncrementalFetchCacheContext,
2222
IncrementalCacheValue,
23+
CachedFetchValue,
24+
SetIncrementalFetchCacheContext,
2325
} from "next/dist/server/response-cache/types";
2426
import { resolveRevalidateValue } from "../helpers/resolveRevalidateValue";
2527

@@ -223,6 +225,54 @@ export class CacheHandler implements NextCacheHandler {
223225
return cacheHandlerValue;
224226
}
225227

228+
static async #writeFetch(
229+
cacheKey: string,
230+
data: CachedFetchValue,
231+
ctx: SetIncrementalFetchCacheContext,
232+
): Promise<void> {
233+
try {
234+
const fetchDataPath = path.join(
235+
CacheHandler.#serverDistDir,
236+
"..",
237+
"cache",
238+
"fetch-cache",
239+
cacheKey,
240+
);
241+
242+
await fsPromises.mkdir(path.dirname(fetchDataPath), {
243+
recursive: true,
244+
});
245+
246+
await fsPromises.writeFile(
247+
fetchDataPath,
248+
JSON.stringify({
249+
...data,
250+
tags: ctx.fetchCache ? ctx.tags : [],
251+
}),
252+
);
253+
254+
if (CacheHandler.#debug) {
255+
console.info(
256+
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
257+
"file system",
258+
"set",
259+
cacheKey,
260+
"Successfully set value.",
261+
);
262+
}
263+
} catch (error) {
264+
if (CacheHandler.#debug) {
265+
console.warn(
266+
"[CacheHandler] [handler: %s] [method: %s] [key: %s] %s",
267+
"file system",
268+
"set",
269+
cacheKey,
270+
`Error: ${error}`,
271+
);
272+
}
273+
}
274+
}
275+
226276
static async #writePagesRouterPage(
227277
cacheKey: string,
228278
pageData: IncrementalCachedPageValue,
@@ -626,6 +676,15 @@ export class CacheHandler implements NextCacheHandler {
626676
}
627677
}
628678

679+
if (CacheHandler.#debug) {
680+
console.info(
681+
"[CacheHandler] [method: %s] [key: %s] %s",
682+
"get",
683+
cacheKey,
684+
`Retrieving value ${cachedData ? "found" : "not found"}.`,
685+
);
686+
}
687+
629688
return cachedData ?? null;
630689
}
631690

@@ -707,6 +766,14 @@ export class CacheHandler implements NextCacheHandler {
707766
cacheHandlerValue.value as unknown as IncrementalCachedPageValue,
708767
);
709768
}
769+
770+
if (cacheHandlerValue.value?.kind === "FETCH") {
771+
await CacheHandler.#writeFetch(
772+
cacheKey,
773+
cacheHandlerValue.value as unknown as CachedFetchValue,
774+
ctx as SetIncrementalFetchCacheContext,
775+
);
776+
}
710777
}
711778

712779
async revalidateTag(

0 commit comments

Comments
 (0)