main
js 222 lines 6.83 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {PostponedState, ErrorInfo} from 'react-server/src/ReactFizzServer';
11 import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
12 import type {
13 BootstrapScriptDescriptor,
14 HeadersDescriptor,
15 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
16 import type {ImportMap} from '../shared/ReactDOMTypes';
17
18 import ReactVersion from 'shared/ReactVersion';
19
20 import {
21 createRequest,
22 resumeRequest,
23 startWork,
24 startFlowing,
25 stopFlowing,
26 abort,
27 attachAbortSignal,
28 } from 'react-server/src/ReactFizzServer';
29
30 import {
31 createResumableState,
32 createRenderState,
33 resumeRenderState,
34 createRootFormatContext,
35 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
36
37 import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
38 ensureCorrectIsomorphicReactVersion();
39
40 type NonceOption =
41 | string
42 | {
43 script?: string,
44 style?: string,
45 };
46
47 type Options = {
48 identifierPrefix?: string,
49 namespaceURI?: string,
50 nonce?: NonceOption,
51 bootstrapScriptContent?: string,
52 bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
53 bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
54 progressiveChunkSize?: number,
55 signal?: AbortSignal,
56 onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
57 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
58 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
59 importMap?: ImportMap,
60 formState?: ReactFormState<any, any> | null,
61 onHeaders?: (headers: Headers) => void,
62 maxHeadersLength?: number,
63 };
64
65 type ResumeOptions = {
66 nonce?: NonceOption,
67 signal?: AbortSignal,
68 onError?: (error: mixed) => ?string,
69 onBrowserBailout?: (error: mixed, errorInfo: ErrorInfo) => void,
70 unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
71 };
72
73 // TODO: Move to sub-classing ReadableStream.
74 type ReactDOMServerReadableStream = ReadableStream & {
75 allReady: Promise<void>,
76 };
77
78 function renderToReadableStream(
79 children: ReactNodeList,
80 options?: Options,
81 ): Promise<ReactDOMServerReadableStream> {
82 return new Promise((resolve, reject) => {
83 let onFatalError;
84 let onAllReady;
85 const allReady = new Promise<void>((res, rej) => {
86 onAllReady = res;
87 onFatalError = rej;
88 });
89
90 function onShellReady() {
91 const stream: ReactDOMServerReadableStream = new ReadableStream(
92 {
93 type: 'bytes',
94 pull: (controller): ?Promise<void> => {
95 startFlowing(request, controller);
96 },
97 cancel: (reason): ?Promise<void> => {
98 stopFlowing(request);
99 abort(request, reason);
100 },
101 },
102 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
103 // $FlowFixMe[incompatible-type]
104 {highWaterMark: 0},
105 ) as any;
106 // TODO: Move to sub-classing ReadableStream.
107 stream.allReady = allReady;
108 resolve(stream);
109 }
110 function onShellError(error: mixed) {
111 // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
112 // However, `allReady` will be rejected by `onFatalError` as well.
113 // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
114 allReady.catch(() => {});
115 reject(error);
116 }
117
118 const onHeaders = options ? options.onHeaders : undefined;
119 let onHeadersImpl;
120 if (onHeaders) {
121 onHeadersImpl = (headersDescriptor: HeadersDescriptor) => {
122 onHeaders(new Headers(headersDescriptor));
123 };
124 }
125
126 const resumableState = createResumableState(
127 options ? options.identifierPrefix : undefined,
128 options ? options.unstable_externalRuntimeSrc : undefined,
129 options ? options.bootstrapScriptContent : undefined,
130 options ? options.bootstrapScripts : undefined,
131 options ? options.bootstrapModules : undefined,
132 );
133 const request = createRequest(
134 children,
135 resumableState,
136 createRenderState(
137 resumableState,
138 options ? options.nonce : undefined,
139 options ? options.unstable_externalRuntimeSrc : undefined,
140 options ? options.importMap : undefined,
141 onHeadersImpl,
142 options ? options.maxHeadersLength : undefined,
143 ),
144 createRootFormatContext(options ? options.namespaceURI : undefined),
145 options ? options.progressiveChunkSize : undefined,
146 options ? options.onError : undefined,
147 options ? options.onBrowserBailout : undefined,
148 onAllReady,
149 onShellReady,
150 onShellError,
151 onFatalError,
152 options ? options.formState : undefined,
153 );
154 if (options && options.signal) {
155 attachAbortSignal(request, options.signal);
156 }
157 startWork(request);
158 });
159 }
160
161 function resume(
162 children: ReactNodeList,
163 postponedState: PostponedState,
164 options?: ResumeOptions,
165 ): Promise<ReactDOMServerReadableStream> {
166 return new Promise((resolve, reject) => {
167 let onFatalError;
168 let onAllReady;
169 const allReady = new Promise<void>((res, rej) => {
170 onAllReady = res;
171 onFatalError = rej;
172 });
173
174 function onShellReady() {
175 const stream: ReactDOMServerReadableStream = new ReadableStream(
176 {
177 type: 'bytes',
178 pull: (controller): ?Promise<void> => {
179 startFlowing(request, controller);
180 },
181 cancel: (reason): ?Promise<void> => {
182 stopFlowing(request);
183 abort(request, reason);
184 },
185 },
186 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
187 // $FlowFixMe[incompatible-type]
188 {highWaterMark: 0},
189 ) as any;
190 // TODO: Move to sub-classing ReadableStream.
191 stream.allReady = allReady;
192 resolve(stream);
193 }
194 function onShellError(error: mixed) {
195 // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
196 // However, `allReady` will be rejected by `onFatalError` as well.
197 // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
198 allReady.catch(() => {});
199 reject(error);
200 }
201 const request = resumeRequest(
202 children,
203 postponedState,
204 resumeRenderState(
205 postponedState.resumableState,
206 options ? options.nonce : undefined,
207 ),
208 options ? options.onError : undefined,
209 options ? options.onBrowserBailout : undefined,
210 onAllReady,
211 onShellReady,
212 onShellError,
213 onFatalError,
214 );
215 if (options && options.signal) {
216 attachAbortSignal(request, options.signal);
217 }
218 startWork(request);
219 });
220 }
221
222 export {renderToReadableStream, resume, ReactVersion as version};