Skip to content

Commit 466fff9

Browse files
committed
initial wiring up and research / investigation
1 parent 2491f3f commit 466fff9

File tree

15 files changed

+1376
-27
lines changed

15 files changed

+1376
-27
lines changed

package-lock.json

+1,045-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"msw": "^2.0.7",
8383
"netlify-cli": "^20.0.2",
8484
"next": "^15.0.0-canary.28",
85+
"next-next": "npm:next@^15.3.1-canary.7",
8586
"os": "^0.1.2",
8687
"outdent": "^0.8.0",
8788
"p-limit": "^5.0.0",
@@ -94,13 +95,18 @@
9495
"uuid": "^10.0.0",
9596
"vitest": "^3.0.0"
9697
},
98+
"overrides": {
99+
"react": "19.0.0-rc.0",
100+
"react-dom": "19.0.0-rc.0"
101+
},
97102
"clean-package": {
98103
"indent": 2,
99104
"remove": [
100105
"clean-package",
101106
"dependencies",
102107
"devDependencies",
103-
"scripts"
108+
"scripts",
109+
"overrides"
104110
]
105111
}
106112
}

src/run/handlers/server.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ import { setFetchBeforeNextPatchedIt } from '../storage/storage.cjs'
1818

1919
import { getLogger, type RequestContext } from './request-context.cjs'
2020
import { getTracer, recordWarning } from './tracer.cjs'
21+
import { configureUseCacheHandlers } from './use-cache-handler.js'
2122
import { setupWaitUntil } from './wait-until.cjs'
2223

24+
// make use of global fetch before Next.js applies any patching
2325
setFetchBeforeNextPatchedIt(globalThis.fetch)
26+
// configure some globals that Next.js make use of before we start importing any Next.js code
27+
// as some globals are consumed at import time
28+
configureUseCacheHandlers()
29+
setupWaitUntil()
2430

2531
const nextImportPromise = import('../next.cjs')
2632

27-
setupWaitUntil()
28-
2933
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete
3034

3135
/**
@@ -57,6 +61,8 @@ export default async (
5761
topLevelSpan: Span,
5862
requestContext: RequestContext,
5963
) => {
64+
console.log('[Netlify Request Handler] handling request', request.url)
65+
6066
const tracer = getTracer()
6167

6268
if (!nextHandler) {

src/run/handlers/use-cache-handler.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import defaultCacheHandlerImport from 'next-next/dist/server/lib/cache-handlers/default.js'
2+
import type {
3+
CacheEntry,
4+
CacheHandlerV2 as CacheHandler,
5+
Timestamp,
6+
} from 'next-next/dist/server/lib/cache-handlers/types.js'
7+
8+
import { getRequestContext } from './request-context.cjs'
9+
10+
const defaultCacheHandler = defaultCacheHandlerImport.default
11+
12+
export const NetlifyDefaultUseCacheHandler = {
13+
get: function (cacheKey: string): Promise<undefined | CacheEntry> {
14+
console.log('NetlifyDefaultUseCacheHandler::get start', { cacheKey })
15+
return defaultCacheHandler.get(cacheKey).then(async (entry) => {
16+
if (!entry) {
17+
console.log('NetlifyDefaultUseCacheHandler::get MISS', { cacheKey })
18+
return entry
19+
}
20+
21+
const [cloneStream, newSaved] = entry.value.tee()
22+
entry.value = newSaved
23+
24+
const [returnStream, debugStream] = cloneStream.tee()
25+
26+
const text = await new Response(debugStream).text()
27+
console.log('NetlifyDefaultUseCacheHandler::get HIT', { cacheKey, entry, text })
28+
29+
return {
30+
...entry,
31+
value: returnStream,
32+
}
33+
})
34+
},
35+
set: async function (cacheKey: string, pendingEntry: Promise<CacheEntry>): Promise<void> {
36+
console.log('NetlifyDefaultUseCacheHandler::set start', { cacheKey })
37+
const entry = await pendingEntry
38+
const [storeStream, debugStream] = entry.value.tee()
39+
40+
const toSetEntry = Promise.resolve({
41+
...entry,
42+
value: storeStream,
43+
})
44+
45+
const text = await new Response(debugStream).text()
46+
47+
console.log('NetlifyDefaultUseCacheHandler::set awaited pending entry', {
48+
cacheKey,
49+
entry,
50+
text,
51+
})
52+
53+
const setPromise = defaultCacheHandler.set(cacheKey, toSetEntry).then(() => {
54+
console.log('NetlifyDefaultUseCacheHandler::set finish', { cacheKey })
55+
})
56+
57+
getRequestContext()?.trackBackgroundWork(setPromise)
58+
59+
return setPromise
60+
},
61+
refreshTags: function (): Promise<void> {
62+
console.log('NetlifyDefaultUseCacheHandler::refreshTags')
63+
return defaultCacheHandler.refreshTags()
64+
},
65+
getExpiration: function (...tags: string[]): Promise<Timestamp> {
66+
console.log('NetlifyDefaultUseCacheHandler::getExpiration start', { tags })
67+
return defaultCacheHandler.getExpiration(...tags).then((expiration) => {
68+
console.log('NetlifyDefaultUseCacheHandler::getExpiration finish', { tags, expiration })
69+
return expiration
70+
})
71+
},
72+
expireTags: function (...tags: string[]): Promise<void> {
73+
console.log('NetlifyDefaultUseCacheHandler::expireTags', { tags })
74+
return defaultCacheHandler.expireTags(...tags)
75+
},
76+
} satisfies CacheHandler
77+
78+
const cacheHandlersSymbol = Symbol.for('@next/cache-handlers')
79+
80+
const extendedGlobalThis = globalThis as typeof globalThis & {
81+
[cacheHandlersSymbol]?: {
82+
RemoteCache?: CacheHandler
83+
DefaultCache?: CacheHandler
84+
}
85+
}
86+
87+
export function configureUseCacheHandlers() {
88+
extendedGlobalThis[cacheHandlersSymbol] = {
89+
DefaultCache: NetlifyDefaultUseCacheHandler,
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react'
2+
3+
async function getData() {
4+
'use cache'
5+
6+
return fetch('https://next-data-api-endpoint.vercel.app/api/random', {
7+
cache: 'no-store',
8+
}).then((res) => res.text())
9+
}
10+
11+
export default async function Page() {
12+
return (
13+
<>
14+
<p>index page</p>
15+
<p id="random">{await getData()}</p>
16+
</>
17+
)
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
3+
async function getData() {
4+
'use cache'
5+
6+
const data = await fetch('https://next-data-api-endpoint.vercel.app/api/random').then((res) =>
7+
res.text(),
8+
)
9+
10+
console.log('Running getData()', data)
11+
return data
12+
}
13+
14+
export default async function Page() {
15+
const data = await getData()
16+
console.log('Rendering page', data)
17+
return (
18+
<>
19+
<p>index page</p>
20+
<p id="random">{data}</p>
21+
</>
22+
)
23+
}
24+
25+
// export const dynamic = 'force-dynamic'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
3+
async function getData() {
4+
'use cache'
5+
6+
const data = await fetch('https://next-data-api-endpoint.vercel.app/api/random').then((res) =>
7+
res.text(),
8+
)
9+
10+
console.log('Running getData()', data)
11+
return data
12+
}
13+
14+
export default async function Page() {
15+
const data = await getData()
16+
console.log('Rendering page', data)
17+
return (
18+
<>
19+
<p>index page</p>
20+
<p id="random">{data}</p>
21+
</>
22+
)
23+
}
24+
25+
// export const dynamic = 'force-dynamic'
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const metadata = {
2+
title: 'Use cache App',
3+
description: 'Description for Use cache Next App',
4+
}
5+
6+
export default function RootLayout({ children }) {
7+
return (
8+
<html lang="en">
9+
<body>{children}</body>
10+
</html>
11+
)
12+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {
5+
output: 'standalone',
6+
eslint: {
7+
ignoreDuringBuilds: true,
8+
},
9+
experimental: {
10+
useCache: true,
11+
// cacheLife: {
12+
// frequent: {
13+
// stale: 19,
14+
// revalidate: 100,
15+
// expire: 250,
16+
// },
17+
// },
18+
},
19+
}
20+
21+
module.exports = nextConfig

tests/fixtures/use-cache/package.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "use-cache",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"postinstall": "next build",
7+
"dev": "next dev",
8+
"build": "next build"
9+
},
10+
"dependencies": {
11+
"next": "latest",
12+
"react": "18.2.0",
13+
"react-dom": "18.2.0"
14+
},
15+
"devDependencies": {
16+
"@types/react": "19.1.2"
17+
}
18+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2017",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"strict": false,
8+
"noEmit": true,
9+
"incremental": true,
10+
"module": "esnext",
11+
"esModuleInterop": true,
12+
"moduleResolution": "node",
13+
"resolveJsonModule": true,
14+
"isolatedModules": true,
15+
"jsx": "preserve",
16+
"plugins": [
17+
{
18+
"name": "next"
19+
}
20+
]
21+
},
22+
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
23+
"exclude": ["node_modules"]
24+
}

tests/integration/use-cache.test.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { load } from 'cheerio'
2+
import { getLogger } from 'lambda-local'
3+
import { v4 } from 'uuid'
4+
import { beforeEach, describe, expect, test, vi } from 'vitest'
5+
import { type FixtureTestContext } from '../utils/contexts.js'
6+
import {
7+
createFixture,
8+
invokeFunction,
9+
// invokeSandboxedFunction,
10+
runPlugin,
11+
} from '../utils/fixture.js'
12+
import {
13+
// countOfBlobServerGetsForKey,
14+
// decodeBlobKey,
15+
// encodeBlobKey,
16+
generateRandomObjectID,
17+
// getBlobEntries,
18+
startMockBlobStore,
19+
} from '../utils/helpers.js'
20+
21+
// Disable the verbose logging of the lambda-local runtime
22+
getLogger().level = 'alert'
23+
24+
beforeEach<FixtureTestContext>(async (ctx) => {
25+
// set for each test a new deployID and siteID
26+
ctx.deployID = generateRandomObjectID()
27+
ctx.siteID = v4()
28+
vi.stubEnv('SITE_ID', ctx.siteID)
29+
vi.stubEnv('DEPLOY_ID', ctx.deployID)
30+
vi.stubEnv('NETLIFY_PURGE_API_TOKEN', 'fake-token')
31+
// hide debug logs in tests
32+
// vi.spyOn(console, 'debug').mockImplementation(() => {})
33+
34+
await startMockBlobStore(ctx)
35+
})
36+
37+
test<FixtureTestContext>('use cache stuff prerendered', async (ctx) => {
38+
const url = '/cache-fetch/test'
39+
40+
await createFixture('use-cache', ctx)
41+
await runPlugin(ctx)
42+
43+
console.log('request 1 start')
44+
const call1 = await invokeFunction(ctx, { url })
45+
console.log('request 1 data', load(call1.body)('#random').text())
46+
47+
console.log('\n---\nrequest 2 start')
48+
const call2 = await invokeFunction(ctx, { url })
49+
50+
console.log('request 2 data', load(call2.body)('#random').text())
51+
})
52+
53+
test<FixtureTestContext>('use cache stuff not-prerendered', async (ctx) => {
54+
const url = '/cache-fetch'
55+
56+
await createFixture('use-cache', ctx)
57+
await runPlugin(ctx)
58+
59+
console.log('request 1 start')
60+
const call1 = await invokeFunction(ctx, { url })
61+
console.log('request 1 data', load(call1.body)('#random').text())
62+
63+
console.log('\n---\nrequest 2 start')
64+
const call2 = await invokeFunction(ctx, { url })
65+
66+
console.log('request 2 data', load(call2.body)('#random').text())
67+
})

tests/utils/create-e2e-fixture.ts

+1
Original file line numberDiff line numberDiff line change
@@ -442,4 +442,5 @@ export const fixtureFactories = {
442442
}),
443443
dynamicCms: () => createE2EFixture('dynamic-cms'),
444444
after: () => createE2EFixture('after'),
445+
useCache: () => createE2EFixture('use-cache'),
445446
}

0 commit comments

Comments
 (0)