[Flight] Add Web Stream support to the Flight Server in Node (#33474)

This needs some tweaks to the implementation and a conversion but simple
enough.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
This commit is contained in:
Sebastian Markbåge
2025-06-07 10:40:09 -04:00
committed by GitHub
parent 65ec57df37
commit 9666605abf
20 changed files with 696 additions and 27 deletions

View File

@@ -7,9 +7,11 @@ if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-parcel-server.node.development.js');
}
exports.renderToReadableStream = s.renderToReadableStream;
exports.renderToPipeableStream = s.renderToPipeableStream;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReply = s.decodeReply;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
exports.decodeAction = s.decodeAction;
exports.decodeFormState = s.decodeFormState;
exports.createClientReference = s.createClientReference;

View File

@@ -7,6 +7,9 @@ if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-parcel-server.node.development.js');
}
if (s.unstable_prerender) {
exports.unstable_prerender = s.unstable_prerender;
}
if (s.unstable_prerenderToNodeStream) {
exports.unstable_prerenderToNodeStream = s.unstable_prerenderToNodeStream;
}

View File

@@ -9,8 +9,10 @@
export {
renderToPipeableStream,
decodeReplyFromBusboy,
renderToReadableStream,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
createClientReference,

View File

@@ -21,6 +21,9 @@ import type {
} from '../client/ReactFlightClientConfigBundlerParcel';
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
import {
createRequest,
createPrerenderRequest,
@@ -35,6 +38,7 @@ import {
reportGlobalError,
close,
resolveField,
resolveFile,
resolveFileInfo,
resolveFileChunk,
resolveFileComplete,
@@ -56,9 +60,12 @@ export {
registerServerReference,
} from '../ReactFlightParcelReferences';
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export type {TemporaryReferenceSet};
function createDrainHandler(destination: Destination, request: Request) {
@@ -131,11 +138,91 @@ export function renderToPipeableStream(
};
}
function createFakeWritable(readable: any): Writable {
function createFakeWritableFromReadableStreamController(
controller: ReadableStreamController,
): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk) {
write(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
chunk = textEncoder.encode(chunk);
}
controller.enqueue(chunk);
// in web streams there is no backpressure so we can alwas write more
return true;
},
end() {
controller.close();
},
destroy(error) {
// $FlowFixMe[method-unbinding]
if (typeof controller.error === 'function') {
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
controller.error(error);
} else {
controller.close();
}
},
}: any);
}
export function renderToReadableStream(
model: ReactClientValue,
options?: Options & {
signal?: AbortSignal,
},
): ReadableStream {
const request = createRequest(
model,
null,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable = createFakeWritableFromReadableStreamController(controller);
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
return stream;
}
function createFakeWritableFromNodeReadable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk: string | Uint8Array) {
return readable.push(chunk);
},
end() {
@@ -173,7 +260,7 @@ export function prerenderToNodeStream(
startFlowing(request, writable);
},
});
const writable = createFakeWritable(readable);
const writable = createFakeWritableFromNodeReadable(readable);
resolve({prelude: readable});
}
@@ -207,6 +294,69 @@ export function prerenderToNodeStream(
});
}
export function prerender(
model: ReactClientValue,
options?: Options & {
signal?: AbortSignal,
},
): Promise<{
prelude: ReadableStream,
}> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable =
createFakeWritableFromReadableStreamController(controller);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
resolve({prelude: stream});
}
const request = createPrerenderRequest(
model,
null,
onAllReady,
onFatalError,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
const reason = (signal: any).reason;
abort(request, reason);
} else {
const listener = () => {
const reason = (signal: any).reason;
abort(request, reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}
let serverManifest = {};
export function registerServerActions(manifest: ServerManifest) {
// This function is called by the bundler to register the manifest.
@@ -292,6 +442,50 @@ export function decodeReply<T>(
return root;
}
export function decodeReplyFromAsyncIterable<T>(
iterable: AsyncIterable<[string, string | File]>,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Thenable<T> {
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<T>(body: FormData): Promise<() => T> | null {
return decodeActionImpl(body, serverManifest);
}

View File

@@ -8,10 +8,13 @@
*/
export {
renderToReadableStream,
renderToPipeableStream,
prerender as unstable_prerender,
prerenderToNodeStream as unstable_prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
createClientReference,

View File

@@ -7,4 +7,7 @@
* @flow
*/
export {unstable_prerenderToNodeStream} from './src/server/react-flight-dom-server.node';
export {
unstable_prerender,
unstable_prerenderToNodeStream,
} from './src/server/react-flight-dom-server.node';

View File

@@ -7,9 +7,11 @@ if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-turbopack-server.node.development.js');
}
exports.renderToReadableStream = s.renderToReadableStream;
exports.renderToPipeableStream = s.renderToPipeableStream;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReply = s.decodeReply;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
exports.decodeAction = s.decodeAction;
exports.decodeFormState = s.decodeFormState;
exports.registerServerReference = s.registerServerReference;

View File

@@ -7,6 +7,9 @@ if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-turbopack-server.node.development.js');
}
if (s.unstable_prerender) {
exports.unstable_prerender = s.unstable_prerender;
}
if (s.unstable_prerenderToNodeStream) {
exports.unstable_prerenderToNodeStream = s.unstable_prerenderToNodeStream;
}

View File

@@ -9,8 +9,10 @@
export {
renderToPipeableStream,
decodeReplyFromBusboy,
renderToReadableStream,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
registerServerReference,

View File

@@ -20,6 +20,8 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
import {
createRequest,
createPrerenderRequest,
@@ -34,6 +36,7 @@ import {
reportGlobalError,
close,
resolveField,
resolveFile,
resolveFileInfo,
resolveFileChunk,
resolveFileComplete,
@@ -51,6 +54,8 @@ export {
createClientModuleProxy,
} from '../ReactFlightTurbopackReferences';
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -128,11 +133,91 @@ function renderToPipeableStream(
};
}
function createFakeWritable(readable: any): Writable {
function createFakeWritableFromReadableStreamController(
controller: ReadableStreamController,
): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk) {
write(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
chunk = textEncoder.encode(chunk);
}
controller.enqueue(chunk);
// in web streams there is no backpressure so we can always write more
return true;
},
end() {
controller.close();
},
destroy(error) {
// $FlowFixMe[method-unbinding]
if (typeof controller.error === 'function') {
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
controller.error(error);
} else {
controller.close();
}
},
}: any);
}
function renderToReadableStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options & {
signal?: AbortSignal,
},
): ReadableStream {
const request = createRequest(
model,
turbopackMap,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable = createFakeWritableFromReadableStreamController(controller);
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
return stream;
}
function createFakeWritableFromNodeReadable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk: string | Uint8Array) {
return readable.push(chunk);
},
end() {
@@ -171,7 +256,7 @@ function prerenderToNodeStream(
startFlowing(request, writable);
},
});
const writable = createFakeWritable(readable);
const writable = createFakeWritableFromNodeReadable(readable);
resolve({prelude: readable});
}
@@ -205,6 +290,69 @@ function prerenderToNodeStream(
});
}
function prerender(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options & {
signal?: AbortSignal,
},
): Promise<{
prelude: ReadableStream,
}> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable =
createFakeWritableFromReadableStreamController(controller);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
resolve({prelude: stream});
}
const request = createPrerenderRequest(
model,
turbopackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
const reason = (signal: any).reason;
abort(request, reason);
} else {
const listener = () => {
const reason = (signal: any).reason;
abort(request, reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}
function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
turbopackMap: ServerManifest,
@@ -286,11 +434,59 @@ function decodeReply<T>(
return root;
}
function decodeReplyFromAsyncIterable<T>(
iterable: AsyncIterable<[string, string | File]>,
turbopackMap: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Thenable<T> {
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,
renderToPipeableStream,
prerender,
prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
};

View File

@@ -8,10 +8,13 @@
*/
export {
renderToReadableStream,
renderToPipeableStream,
prerender as unstable_prerender,
prerenderToNodeStream as unstable_prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
registerServerReference,

View File

@@ -7,4 +7,7 @@
* @flow
*/
export {unstable_prerenderToNodeStream} from './src/server/react-flight-dom-server.node';
export {
unstable_prerender,
unstable_prerenderToNodeStream,
} from './src/server/react-flight-dom-server.node';

View File

@@ -7,9 +7,11 @@ if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-webpack-server.node.development.js');
}
exports.renderToReadableStream = s.renderToReadableStream;
exports.renderToPipeableStream = s.renderToPipeableStream;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReply = s.decodeReply;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
exports.decodeAction = s.decodeAction;
exports.decodeFormState = s.decodeFormState;
exports.registerServerReference = s.registerServerReference;

View File

@@ -7,6 +7,9 @@ if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-webpack-server.node.development.js');
}
if (s.unstable_prerender) {
exports.unstable_prerender = s.unstable_prerender;
}
if (s.unstable_prerenderToNodeStream) {
exports.unstable_prerenderToNodeStream = s.unstable_prerenderToNodeStream;
}

View File

@@ -9,8 +9,10 @@
export {
renderToPipeableStream,
decodeReplyFromBusboy,
renderToReadableStream,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
registerServerReference,

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
@@ -92,6 +93,48 @@ describe('ReactFlightDOMNode', () => {
});
}
it('should support web streams in node', async () => {
function Text({children}) {
return <span>{children}</span>;
}
// Large strings can get encoded differently so we need to test that.
const largeString = 'world'.repeat(1000);
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>{largeString}</Text>
</div>
);
}
function App() {
const model = {
html: <HTML />,
};
return model;
}
const readable = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
);
const response = ReactServerDOMClient.createFromReadableStream(readable, {
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const model = await response;
expect(model).toEqual({
html: (
<div>
<span>hello</span>
<span>{largeString}</span>
</div>
),
});
});
it('should allow an alternative module mapping to be used for SSR', async () => {
function ClientComponent() {
return <span>Client Component</span>;
@@ -498,8 +541,6 @@ describe('ReactFlightDOMNode', () => {
expect(errors).toEqual([new Error('Connection closed.')]);
// Should still match the result when parsed
const result = await readResult(ssrStream);
const div = document.createElement('div');
div.innerHTML = result;
expect(div.textContent).toBe('loading...');
expect(result).toContain('loading...');
});
});

View File

@@ -20,6 +20,8 @@ import type {Thenable} from 'shared/ReactTypes';
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
import {
createRequest,
createPrerenderRequest,
@@ -34,6 +36,7 @@ import {
reportGlobalError,
close,
resolveField,
resolveFile,
resolveFileInfo,
resolveFileChunk,
resolveFileComplete,
@@ -51,6 +54,8 @@ export {
createClientModuleProxy,
} from '../ReactFlightWebpackReferences';
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -128,11 +133,91 @@ function renderToPipeableStream(
};
}
function createFakeWritable(readable: any): Writable {
function createFakeWritableFromReadableStreamController(
controller: ReadableStreamController,
): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk) {
write(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
chunk = textEncoder.encode(chunk);
}
controller.enqueue(chunk);
// in web streams there is no backpressure so we can always write more
return true;
},
end() {
controller.close();
},
destroy(error) {
// $FlowFixMe[method-unbinding]
if (typeof controller.error === 'function') {
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
controller.error(error);
} else {
controller.close();
}
},
}: any);
}
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options & {
signal?: AbortSignal,
},
): ReadableStream {
const request = createRequest(
model,
webpackMap,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable = createFakeWritableFromReadableStreamController(controller);
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
return stream;
}
function createFakeWritableFromNodeReadable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk: string | Uint8Array) {
return readable.push(chunk);
},
end() {
@@ -171,7 +256,7 @@ function prerenderToNodeStream(
startFlowing(request, writable);
},
});
const writable = createFakeWritable(readable);
const writable = createFakeWritableFromNodeReadable(readable);
resolve({prelude: readable});
}
@@ -205,6 +290,69 @@ function prerenderToNodeStream(
});
}
function prerender(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options & {
signal?: AbortSignal,
},
): Promise<{
prelude: ReadableStream,
}> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
let writable: Writable;
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
writable =
createFakeWritableFromReadableStreamController(controller);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, writable);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
resolve({prelude: stream});
}
const request = createPrerenderRequest(
model,
webpackMap,
onAllReady,
onFatalError,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
const reason = (signal: any).reason;
abort(request, reason);
} else {
const listener = () => {
const reason = (signal: any).reason;
abort(request, reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}
function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
webpackMap: ServerManifest,
@@ -286,11 +434,59 @@ function decodeReply<T>(
return root;
}
function decodeReplyFromAsyncIterable<T>(
iterable: AsyncIterable<[string, string | File]>,
webpackMap: ServerManifest,
options?: {temporaryReferences?: TemporaryReferenceSet},
): Thenable<T> {
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,
renderToPipeableStream,
prerender,
prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
};

View File

@@ -8,10 +8,13 @@
*/
export {
renderToReadableStream,
renderToPipeableStream,
prerender as unstable_prerender,
prerenderToNodeStream as unstable_prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
registerServerReference,

View File

@@ -8,10 +8,13 @@
*/
export {
renderToReadableStream,
renderToPipeableStream,
prerender as unstable_prerender,
prerenderToNodeStream as unstable_prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeReplyFromBusboy,
decodeReplyFromAsyncIterable,
decodeAction,
decodeFormState,
registerServerReference,

View File

@@ -7,4 +7,7 @@
* @flow
*/
export {unstable_prerenderToNodeStream} from './src/server/react-flight-dom-server.node';
export {
unstable_prerender,
unstable_prerenderToNodeStream,
} from './src/server/react-flight-dom-server.node';