Skip to content

Commit

Permalink
Add support for "use cache" in route handlers
Browse files Browse the repository at this point in the history
When compiling a route handler entry, we don't generate a client
reference manifest since route handlers can't render client components.
So, to enable `"use cache"` in route handlers, we need to treat the
client reference manifest as optional when generating and reading cache
entries.
  • Loading branch information
unstubbable committed Oct 7, 2024
1 parent a5d2190 commit fc627c8
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 41 deletions.
59 changes: 29 additions & 30 deletions packages/next/src/server/app-render/encryption-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,43 +86,40 @@ export function setReferenceManifestsSingleton({
}
}

export function unsetReferenceManifestsSingleton() {
// @ts-ignore
delete globalThis[SERVER_ACTION_MANIFESTS_SINGLETON]
}

export function getServerModuleMap() {
const serverActionsManifestSingleton = (globalThis as any)[
SERVER_ACTION_MANIFESTS_SINGLETON
] as {
serverModuleMap: {
[id: string]: {
id: string
chunks: string[]
name: string
] as
| {
serverModuleMap: {
[id: string]: {
id: string
chunks: string[]
name: string
}
}
}
}
}

if (!serverActionsManifestSingleton) {
throw new Error(
'Missing manifest for Server Actions. This is a bug in Next.js'
)
}
| undefined

return serverActionsManifestSingleton.serverModuleMap
return serverActionsManifestSingleton?.serverModuleMap
}

export function getClientReferenceManifestSingleton() {
const serverActionsManifestSingleton = (globalThis as any)[
SERVER_ACTION_MANIFESTS_SINGLETON
] as {
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
serverActionsManifest: DeepReadonly<ActionManifest>
}

if (!serverActionsManifestSingleton) {
throw new Error(
'Missing manifest for Server Actions. This is a bug in Next.js'
)
}
] as
| {
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
serverActionsManifest: DeepReadonly<ActionManifest>
}
| undefined

return serverActionsManifestSingleton.clientReferenceManifest
return serverActionsManifestSingleton?.clientReferenceManifest
}

export async function getActionEncryptionKey() {
Expand All @@ -132,10 +129,12 @@ export async function getActionEncryptionKey() {

const serverActionsManifestSingleton = (globalThis as any)[
SERVER_ACTION_MANIFESTS_SINGLETON
] as {
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
serverActionsManifest: DeepReadonly<ActionManifest>
}
] as
| {
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
serverActionsManifest: DeepReadonly<ActionManifest>
}
| undefined

if (!serverActionsManifestSingleton) {
throw new Error(
Expand Down
19 changes: 19 additions & 0 deletions packages/next/src/server/app-render/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ async function encodeActionBoundArg(actionId: string, arg: string) {
export async function encryptActionBoundArgs(actionId: string, args: any[]) {
const clientReferenceManifestSingleton = getClientReferenceManifestSingleton()

if (!clientReferenceManifestSingleton) {
throw new Error(
'Missing manifest for Server Actions. This is a bug in Next.js'
)
}

// Using Flight to serialize the args into a string.
const serialized = await streamToString(
renderToReadableStream(args, clientReferenceManifestSingleton.clientModules)
Expand All @@ -98,6 +104,12 @@ export async function decryptActionBoundArgs(
) {
const clientReferenceManifestSingleton = getClientReferenceManifestSingleton()

if (!clientReferenceManifestSingleton) {
throw new Error(
'Missing manifest for Server Actions. This is a bug in Next.js'
)
}

// Decrypt the serialized string with the action id as the salt.
const decryped = await decodeActionBoundArg(actionId, await encrypted)

Expand All @@ -124,6 +136,13 @@ export async function decryptActionBoundArgs(

// This extra step ensures that the server references are recovered.
const serverModuleMap = getServerModuleMap()

if (!serverModuleMap) {
throw new Error(
'Missing manifest for Server Actions. This is a bug in Next.js'
)
}

const transformed = await decodeReply(
await encodeReply(deserialized),
serverModuleMap
Expand Down
8 changes: 7 additions & 1 deletion packages/next/src/server/load-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import { getTracer } from './lib/trace/tracer'
import { LoadComponentsSpan } from './lib/trace/constants'
import { evalManifest, loadManifest } from './load-manifest'
import { wait } from '../lib/wait'
import { setReferenceManifestsSingleton } from './app-render/encryption-utils'
import {
setReferenceManifestsSingleton,
unsetReferenceManifestsSingleton,
} from './app-render/encryption-utils'
import { createServerModuleMap } from './app-render/action-utils'
import type { DeepReadonly } from '../shared/lib/deep-readonly'

Expand Down Expand Up @@ -185,6 +188,9 @@ async function loadComponentsImpl<N = any>({
pageName: page,
}),
})
} else {
// Otherwise we need to make sure to unset previously set manifests.
unsetReferenceManifestsSingleton()
}

const ComponentMod = await requirePage(page, distDir, isAppPath)
Expand Down
31 changes: 22 additions & 9 deletions packages/next/src/server/use-cache/use-cache-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ cacheHandlerMap.set('default', {

function generateCacheEntry(
workStore: WorkStore,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest> | undefined,
cacheHandler: CacheHandler,
serializedCacheKey: string | ArrayBuffer,
encodedArguments: FormData | string,
Expand All @@ -101,7 +101,7 @@ function generateCacheEntry(

function generateCacheEntryWithRestoredWorkStore(
workStore: WorkStore,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest> | undefined,
cacheHandler: CacheHandler,
serializedCacheKey: string | ArrayBuffer,
encodedArguments: FormData | string,
Expand All @@ -128,7 +128,7 @@ function generateCacheEntryWithRestoredWorkStore(

function generateCacheEntryWithCacheContext(
workStore: WorkStore,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest> | undefined,
cacheHandler: CacheHandler,
serializedCacheKey: string | ArrayBuffer,
encodedArguments: FormData | string,
Expand All @@ -150,7 +150,7 @@ function generateCacheEntryWithCacheContext(

async function generateCacheEntryImpl(
workStore: WorkStore,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest> | undefined,
cacheHandler: CacheHandler,
serializedCacheKey: string | ArrayBuffer,
encodedArguments: FormData | string,
Expand All @@ -160,7 +160,7 @@ async function generateCacheEntryImpl(

const [, , args] = await decodeReply<any[]>(
encodedArguments,
getServerModuleMap(),
getServerModuleMap() ?? {},
{
temporaryReferences,
}
Expand All @@ -174,7 +174,7 @@ async function generateCacheEntryImpl(

const stream = renderToReadableStream(
result,
clientReferenceManifest.clientModules,
clientReferenceManifest ? clientReferenceManifest.clientModules : null,
{
environmentName: 'Cache',
temporaryReferences,
Expand Down Expand Up @@ -285,6 +285,16 @@ export function cache(kind: string, id: string, fn: any) {
const clientReferenceManifestSingleton =
getClientReferenceManifestSingleton()

if (
// Route handlers don't need a client reference manifest.
workStore.page.endsWith('/page') &&
!clientReferenceManifestSingleton
) {
throw new Error(
'Missing manifest for "use cache". This is a bug in Next.js'
)
}

let stream
if (
entry === undefined ||
Expand Down Expand Up @@ -341,10 +351,13 @@ export function cache(kind: string, id: string, fn: any) {
// to be added to the consumer. Instead, we'll wait for any ClientReference to be emitted
// which themselves will handle the preloading.
moduleLoading: null,
moduleMap: isEdgeRuntime
? clientReferenceManifestSingleton.edgeRscModuleMapping
: clientReferenceManifestSingleton.rscModuleMapping,
moduleMap: clientReferenceManifestSingleton
? isEdgeRuntime
? clientReferenceManifestSingleton.edgeRscModuleMapping
: clientReferenceManifestSingleton.rscModuleMapping
: null,
}

return createFromReadableStream(stream, {
ssrManifest,
temporaryReferences,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/types/$$compiled.internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ declare module 'react-server-dom-webpack/server.edge' {
readonly name: string
readonly async?: boolean
}
},
} | null,
options?: {
temporaryReferences?: string
environmentName?: string
Expand Down
15 changes: 15 additions & 0 deletions test/e2e/app-dir/use-cache/app/api/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
async function getCachedRandom() {
'use cache'
return Math.random()
}

export async function GET() {
const response = JSON.stringify({
rand1: await getCachedRandom(),
rand2: await getCachedRandom(),
})

return new Response(response, {
headers: { 'content-type': 'application/json' },
})
}
11 changes: 11 additions & 0 deletions test/e2e/app-dir/use-cache/use-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ describe('use-cache', () => {

const itSkipTurbopack = isTurbopack ? it.skip : it

// Running this test first, to avoid the client manifest leaking into
// `globalThis` if a page is compiled before the route handler. This would
// have previously led to a false-positive result for this test (before we
// called `unsetReferenceManifestsSingleton`).
itSkipTurbopack('should cache results in route handlers', async () => {
const response = await next.fetch('/api')
const { rand1, rand2 } = await response.json()

expect(rand1).toEqual(rand2)
})

// TODO: Fix the following error with Turbopack:
// Error: Module [project]/app/client.tsx [app-client] (ecmascript) was
// instantiated because it was required from module...
Expand Down

0 comments on commit fc627c8

Please sign in to comment.