Add request callback in Flight client (#46650)
Adding the `callServer` option to Flight client with a naive implementation. Fixes NEXT-393. ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
This commit is contained in:
parent
5c18e9ac92
commit
dd2a1c693a
9 changed files with 159 additions and 13 deletions
|
@ -66,6 +66,16 @@ const getCacheKey = () => {
|
|||
return pathname + search
|
||||
}
|
||||
|
||||
async function sha1(message: string) {
|
||||
const arrayBuffer = await crypto.subtle.digest(
|
||||
'SHA-1',
|
||||
new TextEncoder().encode(message)
|
||||
)
|
||||
const data = Array.from(new Uint8Array(arrayBuffer))
|
||||
const hex = data.map((b) => b.toString(16).padStart(2, '0')).join('')
|
||||
return hex
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
let initialServerDataBuffer: string[] | undefined = undefined
|
||||
|
@ -150,7 +160,32 @@ function useInitialServerResponse(cacheKey: string): Promise<JSX.Element> {
|
|||
},
|
||||
})
|
||||
|
||||
const newResponse = createFromReadableStream(readable)
|
||||
const newResponse = createFromReadableStream(readable, {
|
||||
async callServer(
|
||||
metadata: {
|
||||
id: string
|
||||
name: string
|
||||
},
|
||||
args: any[]
|
||||
) {
|
||||
const actionId = await sha1(metadata.id + ':' + metadata.name)
|
||||
|
||||
// Fetching the current url with the action header.
|
||||
// TODO: Refactor this to look up from a manifest.
|
||||
const res = await fetch('', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Next-Action': actionId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bound: args,
|
||||
}),
|
||||
})
|
||||
|
||||
return res.json()
|
||||
},
|
||||
})
|
||||
|
||||
rscCache.set(cacheKey, newResponse)
|
||||
return newResponse
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export const RSC = 'RSC' as const
|
||||
export const ACTION = 'Action' as const
|
||||
export const ACTION = 'Next-Action' as const
|
||||
|
||||
export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
|
||||
export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
|
||||
|
|
|
@ -1703,20 +1703,33 @@ export async function renderToHTMLOrFlight(
|
|||
}
|
||||
|
||||
// For action requests, we handle them differently with a sepcial render result.
|
||||
if (isAction && process.env.NEXT_RUNTIME !== 'edge') {
|
||||
const workerName = 'app' + renderOpts.pathname
|
||||
const actionModId = serverActionsManifest[actionId].workers[workerName]
|
||||
if (isAction) {
|
||||
if (process.env.NEXT_RUNTIME !== 'edge') {
|
||||
const workerName = 'app' + renderOpts.pathname
|
||||
const actionModId = serverActionsManifest[actionId].workers[workerName]
|
||||
|
||||
const { parseBody } =
|
||||
require('./api-utils/node') as typeof import('./api-utils/node')
|
||||
const actionData = (await parseBody(req, '1mb')) || {}
|
||||
const { parseBody } =
|
||||
require('./api-utils/node') as typeof import('./api-utils/node')
|
||||
const actionData = (await parseBody(req, '1mb')) || {}
|
||||
|
||||
const actionHandler =
|
||||
ComponentMod.__next_app_webpack_require__(actionModId).default
|
||||
const actionHandler =
|
||||
ComponentMod.__next_app_webpack_require__(actionModId).default
|
||||
|
||||
return new ActionRenderResult(
|
||||
JSON.stringify(await actionHandler(actionId, actionData.bound || []))
|
||||
)
|
||||
try {
|
||||
return new ActionRenderResult(
|
||||
JSON.stringify(
|
||||
await actionHandler(actionId, actionData.bound || [])
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
if (isRedirectError(err)) {
|
||||
throw new Error('Invariant: not implemented.')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
} else {
|
||||
throw new Error('Not implemented in Edge Runtime.')
|
||||
}
|
||||
}
|
||||
|
||||
// Below this line is handling for rendering to HTML.
|
||||
|
|
38
test/e2e/app-dir/actions/app-action.test.ts
Normal file
38
test/e2e/app-dir/actions/app-action.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { createNextDescribe } from 'e2e-utils'
|
||||
import { check } from 'next-test-utils'
|
||||
|
||||
createNextDescribe(
|
||||
'app-dir action handling',
|
||||
{
|
||||
files: __dirname,
|
||||
skipDeployment: true,
|
||||
},
|
||||
({ next, isNextDev }) => {
|
||||
if (!isNextDev) {
|
||||
it('should create the server reference manifest', async () => {
|
||||
const content = await next.readFile(
|
||||
'.next/server/server-reference-manifest.json'
|
||||
)
|
||||
// Make sure it's valid JSON
|
||||
JSON.parse(content)
|
||||
expect(content.length > 0).toBeTrue()
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Ensure this works in production.
|
||||
if (isNextDev) {
|
||||
it('should handle basic actions correctly', async () => {
|
||||
const browser = await next.browser('/server')
|
||||
|
||||
const cnt = await browser.elementByCss('h1').text()
|
||||
expect(cnt).toBe('0')
|
||||
|
||||
await browser.elementByCss('#inc').click()
|
||||
await check(() => browser.elementByCss('h1').text(), '1')
|
||||
|
||||
await browser.elementByCss('#dec').click()
|
||||
await check(() => browser.elementByCss('h1').text(), '0')
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
8
test/e2e/app-dir/actions/app/layout.js
Normal file
8
test/e2e/app-dir/actions/app/layout.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<head />
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
9
test/e2e/app-dir/actions/app/server/actions.js
Normal file
9
test/e2e/app-dir/actions/app/server/actions.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
'use server'
|
||||
|
||||
export async function inc(value) {
|
||||
return value + 1
|
||||
}
|
||||
|
||||
export async function dec(value) {
|
||||
return value - 1
|
||||
}
|
31
test/e2e/app-dir/actions/app/server/counter.js
Normal file
31
test/e2e/app-dir/actions/app/server/counter.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Counter({ inc, dec }) {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{count}</h1>
|
||||
<button
|
||||
id="inc"
|
||||
onClick={async () => {
|
||||
const newCount = await inc(count)
|
||||
setCount(newCount)
|
||||
}}
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
<button
|
||||
id="dec"
|
||||
onClick={async () => {
|
||||
const newCount = await dec(count)
|
||||
setCount(newCount)
|
||||
}}
|
||||
>
|
||||
-1
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
7
test/e2e/app-dir/actions/app/server/page.js
Normal file
7
test/e2e/app-dir/actions/app/server/page.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Counter from './counter'
|
||||
|
||||
import { inc, dec } from './actions'
|
||||
|
||||
export default function Page() {
|
||||
return <Counter inc={inc} dec={dec} />
|
||||
}
|
5
test/e2e/app-dir/actions/next.config.js
Normal file
5
test/e2e/app-dir/actions/next.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
}
|
Loading…
Reference in a new issue