Skip to content

Commit

Permalink
Configure Flight rendering
Browse files Browse the repository at this point in the history
We render into an RSC payload using FlightServer, then parse it with
FlightClient and then render the result using Fizz.
  • Loading branch information
sebmarkbage committed Jun 26, 2024
1 parent 23a7c2d commit 5a7d35a
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
export * from 'react-client/src/ReactClientConsoleConfigPlain';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

export type Response = any;
export opaque type ModuleLoading = mixed;
export opaque type SSRModuleMap = mixed;
export opaque type ServerManifest = mixed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,82 @@
* @flow
*/

export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

export type Response = any;
export opaque type ModuleLoading = mixed;
export opaque type SSRModuleMap = mixed;
export opaque type ServerManifest = mixed;
import type {Thenable} from 'shared/ReactTypes';

export * from 'react-html/src/ReactHTMLLegacyClientStreamConfig.js';
export * from 'react-client/src/ReactClientConsoleConfigPlain';

export type ModuleLoading = null;
export type SSRModuleMap = null;
export opaque type ServerManifest = null;
export opaque type ServerReferenceId = string;
export opaque type ClientReferenceMetadata = mixed;
export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
export const resolveClientReference: any = null;
export const resolveServerReference: any = null;
export const preloadModule: any = null;
export const requireModule: any = null;
export const prepareDestinationForModule: any = null;
export opaque type ClientReferenceMetadata = null;
export opaque type ClientReference<T> = null; // eslint-disable-line no-unused-vars

export function prepareDestinationForModule(
moduleLoading: ModuleLoading,
nonce: ?string,
metadata: ClientReferenceMetadata,
) {
throw new Error(
'renderToMarkup should not have emitted Client References. This is a bug in React.',
);
}

export function resolveClientReference<T>(
bundlerConfig: SSRModuleMap,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
throw new Error(
'renderToMarkup should not have emitted Client References. This is a bug in React.',
);
}

export function resolveServerReference<T>(
config: ServerManifest,
id: ServerReferenceId,
): ClientReference<T> {
throw new Error(
'renderToMarkup should not have emitted Server References. This is a bug in React.',
);
}

export function preloadModule<T>(
metadata: ClientReference<T>,
): null | Thenable<T> {
return null;
}

export function requireModule<T>(metadata: ClientReference<T>): T {
throw new Error(
'renderToMarkup should not have emitted Client References. This is a bug in React.',
);
}

export const usedWithSSR = true;

type HintCode = string;
type HintModel<T: HintCode> = null; // eslint-disable-line no-unused-vars

export function dispatchHint<Code: HintCode>(
code: Code,
model: HintModel<Code>,
): void {
// Should never happen.
}

export function preinitModuleForSSR(
href: string,
nonce: ?string,
crossOrigin: ?string,
) {
// Should never happen.
}

export function preinitScriptForSSR(
href: string,
nonce: ?string,
crossOrigin: ?string,
) {
// Should never happen.
}
32 changes: 32 additions & 0 deletions packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

// TODO: The legacy one should not use binary.

export type StringDecoder = TextDecoder;

export function createStringDecoder(): StringDecoder {
return new TextDecoder();
}

const decoderOptions = {stream: true};

export function readPartialStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
return decoder.decode(buffer, decoderOptions);
}

export function readFinalStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
return decoder.decode(buffer);
}
115 changes: 95 additions & 20 deletions packages/react-html/src/ReactHTMLServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,29 @@
*/

import type {ReactNodeList} from 'shared/ReactTypes';

import type {
Request,
PostponedState,
ErrorInfo,
} from 'react-server/src/ReactFizzServer';
import type {LazyComponent} from 'react/src/ReactLazy';

import ReactVersion from 'shared/ReactVersion';

import {
createRequest,
startWork,
startFlowing,
abort,
createRequest as createFlightRequest,
startWork as startFlightWork,
startFlowing as startFlightFlowing,
abort as abortFlight,
} from 'react-server/src/ReactFlightServer';

import {
createResponse as createFlightResponse,
getRoot as getFlightRoot,
processBinaryChunk as processFlightBinaryChunk,
close as closeFlight,
} from 'react-client/src/ReactFlightClient';

import {
createRequest as createFizzRequest,
startWork as startFizzWork,
startFlowing as startFizzFlowing,
abort as abortFizz,
} from 'react-server/src/ReactFizzServer';

import {
Expand All @@ -30,20 +39,60 @@ import {
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';

type ReactMarkupNodeList =
// This is the intersection of ReactNodeList and ReactClientValue minus
// Client/ServerReferences.
| React$Element<React$AbstractComponent<any, any>>
| LazyComponent<ReactMarkupNodeList, any>
| React$Element<string>
| string
| boolean
| number
| symbol
| null
| void
| bigint
| $AsyncIterable<ReactMarkupNodeList, ReactMarkupNodeList, void>
| $AsyncIterator<ReactMarkupNodeList, ReactMarkupNodeList, void>
| Iterable<ReactMarkupNodeList>
| Iterator<ReactMarkupNodeList>
| Array<ReactMarkupNodeList>
| Promise<ReactMarkupNodeList>; // Thenable<ReactMarkupNodeList>

type MarkupOptions = {
identifierPrefix?: string,
signal?: AbortSignal,
};

function noServerCallOrFormAction() {
throw new Error(
'renderToMarkup should not have emitted Server References. This is a bug in React.',
);
}

export function renderToMarkup(
children: ReactNodeList,
children: ReactMarkupNodeList,
options?: MarkupOptions,
): Promise<string> {
return new Promise((resolve, reject) => {
let didFatal = false;
let fatalError = null;
const textEncoder = new TextEncoder();
const flightDestination = {
push(chunk: string | null): boolean {
if (chunk !== null) {
// TODO: Legacy should not use binary streams.
processFlightBinaryChunk(flightResponse, textEncoder.encode(chunk));
} else {
closeFlight(flightResponse);
}
return true;
},
destroy(error: mixed): void {
abortFizz(fizzRequest, error);
reject(error);
},
};
let buffer = '';
const destination = {
const fizzDestination = {
// $FlowFixMe[missing-local-annot]
push(chunk) {
if (chunk !== null) {
Expand All @@ -56,6 +105,7 @@ export function renderToMarkup(
},
// $FlowFixMe[missing-local-annot]
destroy(error) {
abortFlight(flightRequest, error);
reject(error);
},
};
Expand All @@ -65,12 +115,33 @@ export function renderToMarkup(
// client rendering mode because there's no client rendering here.
reject(error);
}
const flightRequest = createFlightRequest(
// $FlowFixMe: This should be a subtype but not everything is typed covariant.
children,
null,
onError,
options ? options.identifierPrefix : undefined,
undefined,
'Markup',
undefined,
);
const flightResponse = createFlightResponse(
null,
null,
noServerCallOrFormAction,
noServerCallOrFormAction,
undefined,
undefined,
undefined,
);
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,
);
const request = createRequest(
children,
const root = getFlightRoot<ReactNodeList>(flightResponse);
const fizzRequest = createFizzRequest(
// $FlowFixMe: Thenables as children are supported.
root,
resumableState,
createRenderState(resumableState, true),
createRootFormatContext(),
Expand All @@ -86,17 +157,21 @@ export function renderToMarkup(
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
abortFlight(flightRequest, (signal: any).reason);
abortFizz(fizzRequest, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
abortFlight(flightRequest, (signal: any).reason);
abortFizz(fizzRequest, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
startFlowing(request, destination);
startFlightWork(flightRequest);
startFlightFlowing(flightRequest, flightDestination);
startFizzWork(fizzRequest);
startFizzFlowing(fizzRequest, fizzDestination);
});
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react-html/src/__tests__/ReactHTMLServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

'use strict';

global.TextDecoder = require('util').TextDecoder;
global.TextEncoder = require('util').TextEncoder;

let React;
let ReactHTML;

Expand Down
Loading

0 comments on commit 5a7d35a

Please sign in to comment.