Skip to content

Commit

Permalink
Add request callback in Flight client (#46650)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
shuding committed Mar 1, 2023
1 parent 5c18e9a commit dd2a1c6
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 13 deletions.
37 changes: 36 additions & 1 deletion packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/components/app-router-headers.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
35 changes: 24 additions & 11 deletions packages/next/src/server/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 38 additions & 0 deletions test/e2e/app-dir/actions/app-action.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions test/e2e/app-dir/actions/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function RootLayout({ children }) {
return (
<html>
<head />
<body>{children}</body>
</html>
)
}
9 changes: 9 additions & 0 deletions test/e2e/app-dir/actions/app/server/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use server'

export async function inc(value) {
return value + 1
}

export async function dec(value) {
return value - 1
}
31 changes: 31 additions & 0 deletions test/e2e/app-dir/actions/app/server/counter.js
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions test/e2e/app-dir/actions/app/server/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Counter from './counter'

import { inc, dec } from './actions'

export default function Page() {
return <Counter inc={inc} dec={dec} />
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/actions/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
appDir: true,
},
}

0 comments on commit dd2a1c6

Please sign in to comment.