diff --git a/packages/react-server-dom-parcel/npm/server.edge.js b/packages/react-server-dom-parcel/npm/server.edge.js index 5f13279f75e2b..356cce93a70bc 100644 --- a/packages/react-server-dom-parcel/npm/server.edge.js +++ b/packages/react-server-dom-parcel/npm/server.edge.js @@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') { exports.renderToReadableStream = s.renderToReadableStream; exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; exports.decodeAction = s.decodeAction; exports.decodeFormState = s.decodeFormState; exports.createClientReference = s.createClientReference; diff --git a/packages/react-server-dom-parcel/server.edge.js b/packages/react-server-dom-parcel/server.edge.js index 0974db3448fb7..42f5c3d65399c 100644 --- a/packages/react-server-dom-parcel/server.edge.js +++ b/packages/react-server-dom-parcel/server.edge.js @@ -10,6 +10,7 @@ export { renderToReadableStream, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, createClientReference, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js index 73a8741618213..2a365993a7cfb 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js @@ -17,6 +17,8 @@ import { type ServerReferenceId, } from '../client/ReactFlightClientConfigBundlerParcel'; +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + import { createRequest, createPrerenderRequest, @@ -30,6 +32,9 @@ import { createResponse, close, getRoot, + reportGlobalError, + resolveField, + resolveFile, } from 'react-server/src/ReactFlightReplyServer'; import { @@ -189,6 +194,50 @@ export function decodeReply( return root; } +export function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + serverManifest, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + export function decodeAction(body: FormData): Promise<() => T> | null { return decodeActionImpl(body, serverManifest); } diff --git a/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js index c6b3067fbcfdb..54f3dbb2ec346 100644 --- a/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-parcel/src/server/react-flight-dom-server.edge.js @@ -11,6 +11,7 @@ export { renderToReadableStream, prerender as unstable_prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, createClientReference, diff --git a/packages/react-server-dom-turbopack/npm/server.edge.js b/packages/react-server-dom-turbopack/npm/server.edge.js index e34b18fa0156a..c832080079dd5 100644 --- a/packages/react-server-dom-turbopack/npm/server.edge.js +++ b/packages/react-server-dom-turbopack/npm/server.edge.js @@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') { exports.renderToReadableStream = s.renderToReadableStream; exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; exports.decodeAction = s.decodeAction; exports.decodeFormState = s.decodeFormState; exports.registerServerReference = s.registerServerReference; diff --git a/packages/react-server-dom-turbopack/server.edge.js b/packages/react-server-dom-turbopack/server.edge.js index c527c7f76a74f..8f0347cd7b5a7 100644 --- a/packages/react-server-dom-turbopack/server.edge.js +++ b/packages/react-server-dom-turbopack/server.edge.js @@ -10,6 +10,7 @@ export { renderToReadableStream, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index 11dbe1a7c1358..e8256767fa5b1 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + import { createRequest, createPrerenderRequest, @@ -25,6 +27,9 @@ import { createResponse, close, getRoot, + reportGlobalError, + resolveField, + resolveFile, } from 'react-server/src/ReactFlightReplyServer'; import { @@ -183,10 +188,56 @@ function decodeReply( return root; } +function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + turbopackMap: ServerManifest, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + turbopackMap, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + export { renderToReadableStream, prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, }; diff --git a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js index 48c4fc4553e6b..9198f9913ed37 100644 --- a/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-turbopack/src/server/react-flight-dom-server.edge.js @@ -11,6 +11,7 @@ export { renderToReadableStream, prerender as unstable_prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference, diff --git a/packages/react-server-dom-webpack/npm/server.edge.js b/packages/react-server-dom-webpack/npm/server.edge.js index 591b84476884d..51a58ea7a9e30 100644 --- a/packages/react-server-dom-webpack/npm/server.edge.js +++ b/packages/react-server-dom-webpack/npm/server.edge.js @@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') { exports.renderToReadableStream = s.renderToReadableStream; exports.decodeReply = s.decodeReply; +exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable; exports.decodeAction = s.decodeAction; exports.decodeFormState = s.decodeFormState; exports.registerServerReference = s.registerServerReference; diff --git a/packages/react-server-dom-webpack/server.edge.js b/packages/react-server-dom-webpack/server.edge.js index c527c7f76a74f..8f0347cd7b5a7 100644 --- a/packages/react-server-dom-webpack/server.edge.js +++ b/packages/react-server-dom-webpack/server.edge.js @@ -10,6 +10,7 @@ export { renderToReadableStream, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js index f6157dff171d7..2effa9868e99b 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js @@ -272,4 +272,40 @@ describe('ReactFlightDOMReplyEdge', () => { expect(error).not.toBe(null); expect(error.message).toBe('Connection closed.'); }); + + it('can stream the decoding using an async iterable', async () => { + let resolve; + const promise = new Promise(r => (resolve = r)); + + const buffer = new Uint8Array([ + 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20, + ]); + + const formData = await ReactServerDOMClient.encodeReply({ + a: Promise.resolve('hello'), + b: Promise.resolve(buffer), + }); + + const iterable = { + async *[Symbol.asyncIterator]() { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const entry of formData) { + yield entry; + await promise; + } + }, + }; + + const decoded = await ReactServerDOMServer.decodeReplyFromAsyncIterable( + iterable, + webpackServerMap, + ); + + expect(Object.keys(decoded)).toEqual(['a', 'b']); + + await resolve(); + + expect(await decoded.a).toBe('hello'); + expect(Array.from(await decoded.b)).toEqual(Array.from(buffer)); + }); }); diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 7954417b95a25..e5b834be0543f 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; + import { createRequest, createPrerenderRequest, @@ -25,6 +27,9 @@ import { createResponse, close, getRoot, + reportGlobalError, + resolveField, + resolveFile, } from 'react-server/src/ReactFlightReplyServer'; import { @@ -183,10 +188,56 @@ function decodeReply( return root; } +function decodeReplyFromAsyncIterable( + iterable: AsyncIterable<[string, string | File]>, + webpackMap: ServerManifest, + options?: {temporaryReferences?: TemporaryReferenceSet}, +): Thenable { + const iterator: AsyncIterator<[string, string | File]> = + iterable[ASYNC_ITERATOR](); + + const response = createResponse( + webpackMap, + '', + options ? options.temporaryReferences : undefined, + ); + + function progress( + entry: + | {done: false, +value: [string, string | File], ...} + | {done: true, +value: void, ...}, + ) { + if (entry.done) { + close(response); + } else { + const [name, value] = entry.value; + if (typeof value === 'string') { + resolveField(response, name, value); + } else { + resolveFile(response, name, value); + } + iterator.next().then(progress, error); + } + } + function error(reason: Error) { + reportGlobalError(response, reason); + if (typeof (iterator: any).throw === 'function') { + // The iterator protocol doesn't necessarily include this but a generator do. + // $FlowFixMe should be able to pass mixed + iterator.throw(reason).then(error, error); + } + } + + iterator.next().then(progress, error); + + return getRoot(response); +} + export { renderToReadableStream, prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, }; diff --git a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js index 48c4fc4553e6b..9198f9913ed37 100644 --- a/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js +++ b/packages/react-server-dom-webpack/src/server/react-flight-dom-server.edge.js @@ -11,6 +11,7 @@ export { renderToReadableStream, prerender as unstable_prerender, decodeReply, + decodeReplyFromAsyncIterable, decodeAction, decodeFormState, registerServerReference,