Skip to content

Commit 58ce563

Browse files
authored
feat: add cache set for route handler (#45)
* feat: add cache set for route handler * chore: increase timeout as tests are flaky * chore: update
1 parent 17e5074 commit 58ce563

File tree

9 files changed

+177
-13
lines changed

9 files changed

+177
-13
lines changed

src/build/content/prerendered.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ type PageCacheValue = {
2929
type RouteCacheValue = {
3030
kind: 'ROUTE'
3131
body: string
32-
headers?: { [k: string]: string }
33-
status?: number
32+
headers: { [k: string]: string }
33+
status: number
3434
}
3535

3636
type FetchCacheValue = {

src/run/handlers/cache.cts

+14-3
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,24 @@ export default class NetlifyCacheHandler implements CacheHandler {
6464

6565
switch (blob.value.kind) {
6666
// TODO:
67-
// case 'ROUTE':
6867
// case 'FETCH':
68+
case 'ROUTE':
69+
return {
70+
value: {
71+
body: Buffer.from(blob.value.body),
72+
kind: blob.value.kind,
73+
status: blob.value.status,
74+
headers: blob.value.headers,
75+
},
76+
}
6977
case 'PAGE':
7078
return {
7179
lastModified: blob.lastModified,
7280
value: blob.value,
7381
}
7482

75-
// default:
76-
// console.log('TODO: implmenet', blob)
83+
default:
84+
console.log('TODO: implement NetlifyCacheHandler.get', blob)
7785
}
7886
return null
7987
}
@@ -83,6 +91,9 @@ export default class NetlifyCacheHandler implements CacheHandler {
8391
console.debug(`[NetlifyCacheHandler.set]: ${key}`)
8492
let cacheKey: string | null = null
8593
switch (data?.kind) {
94+
case 'ROUTE':
95+
cacheKey = join('server/app', key)
96+
break
8697
case 'FETCH':
8798
cacheKey = join('cache/fetch-cache', key)
8899
break
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export async function GET() {
4+
const res = await fetch(`https://api.tvmaze.com/shows/1`, {
5+
next: { revalidate: 3 },
6+
})
7+
const data = await res.json()
8+
9+
return NextResponse.json({ data, time: new Date().toISOString() })
10+
}
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/basic-features/typescript for more information.

tests/fixtures/server-components/package-lock.json

+32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/fixtures/server-components/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@
1111
"next": "13.5.2",
1212
"react": "18.2.0",
1313
"react-dom": "18.2.0"
14+
},
15+
"devDependencies": {
16+
"@types/react": "18.2.34"
1417
}
1518
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"compilerOptions": {
3+
"lib": [
4+
"dom",
5+
"dom.iterable",
6+
"esnext"
7+
],
8+
"allowJs": true,
9+
"skipLibCheck": true,
10+
"strict": false,
11+
"noEmit": true,
12+
"incremental": true,
13+
"esModuleInterop": true,
14+
"module": "esnext",
15+
"moduleResolution": "node",
16+
"resolveJsonModule": true,
17+
"isolatedModules": true,
18+
"jsx": "preserve",
19+
"plugins": [
20+
{
21+
"name": "next"
22+
}
23+
]
24+
},
25+
"include": [
26+
"next-env.d.ts",
27+
".next/types/**/*.ts",
28+
"**/*.ts",
29+
"**/*.tsx"
30+
],
31+
"exclude": [
32+
"node_modules"
33+
]
34+
}

tests/integration/cache-handler.test.ts

+68-7
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ import {
1919
getLogger().level = 'alert'
2020

2121
beforeEach<FixtureTestContext>(async (ctx) => {
22-
await startMockBlobStore(ctx)
2322
// set for each test a new deployID and siteID
2423
ctx.deployID = generateRandomObjectID()
2524
ctx.siteID = v4()
2625
vi.stubEnv('DEPLOY_ID', ctx.deployID)
27-
vi.stubEnv('NETLIFY_BLOBS_CONTEXT', createBlobContext(ctx))
26+
27+
await startMockBlobStore(ctx)
2828
})
2929

3030
describe('page router', () => {
@@ -58,13 +58,13 @@ describe('page router', () => {
5858
// now it should be a cache miss
5959
const call2 = await invokeFunction(ctx, { url: 'static/revalidate' })
6060
const call2Date = load(call2.body)('[data-testid="date-now"]').text()
61-
expect(call2Date, 'the date was cached and is matching the initial one').not.toBe(call1Date)
6261
expect(call2.statusCode).toBe(200)
6362
expect(call2.headers, 'a cache miss on a stale page').toEqual(
6463
expect.objectContaining({
6564
'x-nextjs-cache': 'MISS',
6665
}),
6766
)
67+
expect(call2Date, 'the date was cached and is matching the initial one').not.toBe(call1Date)
6868

6969
// it does not wait for the cache.set so we have to manually wait here until the blob storage got populated
7070
await new Promise<void>((resolve) => setTimeout(resolve, 100))
@@ -150,20 +150,20 @@ describe('app router', () => {
150150
)
151151

152152
// wait to have a stale page
153-
await new Promise<void>((resolve) => setTimeout(resolve, 1_000))
153+
await new Promise<void>((resolve) => setTimeout(resolve, 2_000))
154154
// after the dynamic call of `posts/3` it should be in cache, not this is after the timout as the cache set happens async
155155
expect(await ctx.blobStore.get('server/app/posts/3')).not.toBeNull()
156156

157157
const stale = await invokeFunction(ctx, { url: 'posts/1' })
158158
const staleDate = load(stale.body)('[data-testid="date-now"]').text()
159159
expect(stale.statusCode).toBe(200)
160-
// it should have a new date rendered
161-
expect(staleDate, 'the date was cached and is matching the initial one').not.toBe(post1Date)
162160
expect(stale.headers, 'a cache miss on a stale page').toEqual(
163161
expect.objectContaining({
164162
'x-nextjs-cache': 'MISS',
165163
}),
166164
)
165+
// it should have a new date rendered
166+
expect(staleDate, 'the date was cached and is matching the initial one').not.toBe(post1Date)
167167

168168
// it does not wait for the cache.set so we have to manually wait here until the blob storage got populated
169169
await new Promise<void>((resolve) => setTimeout(resolve, 100))
@@ -180,21 +180,82 @@ describe('app router', () => {
180180
)
181181
})
182182

183-
test<FixtureTestContext>('react-server-components', async (ctx) => {
183+
test<FixtureTestContext>('server-components blob store created correctly', async (ctx) => {
184184
await createFixture('server-components', ctx)
185185
await runPlugin(ctx)
186186
// check if the blob entries where successful set on the build plugin
187187
const blobEntries = await getBlobEntries(ctx)
188188
expect(blobEntries).toEqual([
189+
{
190+
key: 'cache/fetch-cache/460ed46cd9a194efa197be9f2571e51b729a039d1cff9834297f416dce5ada29',
191+
etag: expect.any(String),
192+
},
189193
{
190194
key: 'cache/fetch-cache/ac26c54e17c3018c17bfe5ae6adc0e6d37dbfaf28445c1f767ff267144264ac9',
191195
etag: expect.any(String),
192196
},
193197
{ key: 'server/app/_not-found', etag: expect.any(String) },
198+
{ key: 'server/app/api/revalidate-handler', etag: expect.any(String) },
194199
{ key: 'server/app/index', etag: expect.any(String) },
195200
{ key: 'server/app/revalidate-fetch', etag: expect.any(String) },
196201
{ key: 'server/app/static-fetch-1', etag: expect.any(String) },
197202
{ key: 'server/app/static-fetch-2', etag: expect.any(String) },
198203
])
199204
})
205+
206+
test<FixtureTestContext>('route handler with revalidate', async (ctx) => {
207+
await createFixture('server-components', ctx)
208+
await runPlugin(ctx)
209+
210+
// check if the route got prerendered
211+
const blobEntry = await ctx.blobStore.get('server/app/api/revalidate-handler', { type: 'json' })
212+
expect(blobEntry).not.toBeNull()
213+
214+
// test the first invocation of the route
215+
const call1 = await invokeFunction(ctx, { url: '/api/revalidate-handler' })
216+
const call1Body = JSON.parse(call1.body)
217+
const call1Time = call1Body.time
218+
expect(call1.statusCode).toBe(200)
219+
expect(call1Body).toMatchObject({
220+
data: expect.objectContaining({
221+
id: 1,
222+
name: 'Under the Dome',
223+
}),
224+
})
225+
expect(call1.headers, 'a cache hit on the first invocation of a prerendered route').toEqual(
226+
expect.objectContaining({
227+
'x-nextjs-cache': 'HIT',
228+
}),
229+
)
230+
// wait to have a stale route
231+
await new Promise<void>((resolve) => setTimeout(resolve, 1_500))
232+
233+
const call2 = await invokeFunction(ctx, { url: '/api/revalidate-handler' })
234+
const call2Body = JSON.parse(call2.body)
235+
const call2Time = call2Body.time
236+
expect(call2.statusCode).toBe(200)
237+
// it should have a new date rendered
238+
expect(call1Time, 'the date is a new one on a stale route').not.toBe(call2Time)
239+
expect(call2Body).toMatchObject({ data: expect.objectContaining({ id: 1 }) })
240+
expect(call2.headers, 'a cache miss on a stale route').toEqual(
241+
expect.objectContaining({
242+
'x-nextjs-cache': 'MISS',
243+
}),
244+
)
245+
246+
// it does not wait for the cache.set so we have to manually wait here until the blob storage got populated
247+
await new Promise<void>((resolve) => setTimeout(resolve, 100))
248+
249+
const call3 = await invokeFunction(ctx, { url: '/api/revalidate-handler' })
250+
expect(call3.statusCode).toBe(200)
251+
const call3Body = JSON.parse(call3.body)
252+
const call3Time = call3Body.time
253+
expect(call3Time, 'the date was cached as well').toBe(call2Time)
254+
expect(call3Body).toMatchObject({ data: expect.objectContaining({ id: 1 }) })
255+
expect(call3.headers, 'a cache hit after dynamically regenerating the stale route').toEqual(
256+
expect.objectContaining({
257+
'x-nextjs-cache': 'HIT',
258+
}),
259+
)
260+
})
200261
})

tests/utils/helpers.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { NetlifyPluginUtils } from '@netlify/build'
66
import { mkdtemp } from 'node:fs/promises'
77
import { tmpdir } from 'node:os'
88
import { join } from 'node:path'
9-
import { assert } from 'vitest'
9+
import { assert, vi } from 'vitest'
1010

1111
/**
1212
* Generates a 24char deploy ID (this is validated in the blob storage so we cant use a uuidv4)
@@ -47,6 +47,14 @@ export const startMockBlobStore = async (ctx: FixtureTestContext) => {
4747
})
4848
await ctx.blobServer.start()
4949
ctx.blobStoreHost = `localhost:${port}`
50+
vi.stubEnv('NETLIFY_BLOBS_CONTEXT', createBlobContext(ctx))
51+
52+
ctx.blobStore = getDeployStore({
53+
apiURL: `http://${ctx.blobStoreHost}`,
54+
deployID: ctx.deployID,
55+
siteID: ctx.siteID,
56+
token: BLOB_TOKEN,
57+
})
5058
}
5159

5260
/**

0 commit comments

Comments
 (0)