main
js 314 lines 8.3 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 {
11 Request,
12 ReactClientValue,
13 } from 'react-server/src/ReactFlightServer';
14 import type {Thenable} from 'shared/ReactTypes';
15 import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
16 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
17
18 import noop from 'shared/noop';
19 import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
20
21 import {
22 createRequest,
23 createPrerenderRequest,
24 startWork,
25 startFlowing,
26 startFlowingDebug,
27 stopFlowing,
28 abort,
29 attachAbortSignal,
30 resolveDebugMessage,
31 closeDebugChannel,
32 } from 'react-server/src/ReactFlightServer';
33
34 import {
35 createResponse,
36 close,
37 getRoot,
38 reportGlobalError,
39 resolveField,
40 resolveFile,
41 } from 'react-server/src/ReactFlightReplyServer';
42
43 import {
44 decodeAction,
45 decodeFormState,
46 } from 'react-server/src/ReactFlightActionServer';
47
48 export {
49 registerServerReference,
50 registerClientReference,
51 createClientModuleProxy,
52 } from '../ReactFlightWebpackReferences';
53
54 import {
55 createStringDecoder,
56 readPartialStringChunk,
57 readFinalStringChunk,
58 } from 'react-client/src/ReactFlightClientStreamConfigWeb';
59
60 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
61
62 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
63
64 export type {TemporaryReferenceSet};
65
66 type Options = {
67 debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
68 environmentName?: string | (() => string),
69 filterStackFrame?: (url: string, functionName: string) => boolean,
70 identifierPrefix?: string,
71 signal?: AbortSignal,
72 temporaryReferences?: TemporaryReferenceSet,
73 onError?: (error: mixed) => void,
74 startTime?: number,
75 };
76
77 function startReadingFromDebugChannelReadableStream(
78 request: Request,
79 stream: ReadableStream,
80 ): void {
81 const reader = stream.getReader();
82 const stringDecoder = createStringDecoder();
83 let stringBuffer = '';
84 function progress({
85 done,
86 value,
87 }: {
88 done: boolean,
89 value: ?any,
90 ...
91 }): void | Promise<void> {
92 const buffer: Uint8Array = value as any;
93 stringBuffer += done
94 ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
95 : readPartialStringChunk(stringDecoder, buffer);
96 const messages = stringBuffer.split('\n');
97 for (let i = 0; i < messages.length - 1; i++) {
98 resolveDebugMessage(request, messages[i]);
99 }
100 stringBuffer = messages[messages.length - 1];
101 if (done) {
102 closeDebugChannel(request);
103 return;
104 }
105 return reader.read().then(progress).catch(error);
106 }
107 function error(e: any) {
108 abort(
109 request,
110 new Error('Lost connection to the Debug Channel.', {
111 cause: e,
112 }),
113 );
114 }
115 reader.read().then(progress).catch(error);
116 }
117
118 function renderToReadableStream(
119 model: ReactClientValue,
120 webpackMap: ClientManifest,
121 options?: Options,
122 ): ReadableStream {
123 const debugChannelReadable =
124 __DEV__ && options && options.debugChannel
125 ? options.debugChannel.readable
126 : undefined;
127 const debugChannelWritable =
128 __DEV__ && options && options.debugChannel
129 ? options.debugChannel.writable
130 : undefined;
131 const request = createRequest(
132 model,
133 webpackMap,
134 options ? options.onError : undefined,
135 options ? options.identifierPrefix : undefined,
136 options ? options.temporaryReferences : undefined,
137 options ? options.startTime : undefined,
138 __DEV__ && options ? options.environmentName : undefined,
139 __DEV__ && options ? options.filterStackFrame : undefined,
140 debugChannelReadable !== undefined,
141 );
142 if (options && options.signal) {
143 attachAbortSignal(request, options.signal);
144 }
145 if (debugChannelWritable !== undefined) {
146 const debugStream = new ReadableStream(
147 {
148 type: 'bytes',
149 pull: (controller): ?Promise<void> => {
150 startFlowingDebug(request, controller);
151 },
152 },
153 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
154 // $FlowFixMe[incompatible-type]
155 {highWaterMark: 0},
156 );
157 debugStream.pipeTo(debugChannelWritable);
158 }
159 if (debugChannelReadable !== undefined) {
160 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
161 }
162 const stream = new ReadableStream(
163 {
164 type: 'bytes',
165 start: (controller): ?Promise<void> => {
166 startWork(request);
167 },
168 pull: (controller): ?Promise<void> => {
169 startFlowing(request, controller);
170 },
171 cancel: (reason): ?Promise<void> => {
172 stopFlowing(request);
173 abort(request, reason);
174 },
175 },
176 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
177 // $FlowFixMe[incompatible-type]
178 {highWaterMark: 0},
179 );
180 return stream;
181 }
182
183 type StaticResult = {
184 prelude: ReadableStream,
185 };
186
187 function prerender(
188 model: ReactClientValue,
189 webpackMap: ClientManifest,
190 options?: Options,
191 ): Promise<StaticResult> {
192 return new Promise((resolve, reject) => {
193 const onFatalError = reject;
194 function onAllReady() {
195 const stream = new ReadableStream(
196 {
197 type: 'bytes',
198 pull: (controller): ?Promise<void> => {
199 startFlowing(request, controller);
200 },
201 cancel: (reason): ?Promise<void> => {
202 stopFlowing(request);
203 abort(request, reason);
204 },
205 },
206 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
207 // $FlowFixMe[incompatible-type]
208 {highWaterMark: 0},
209 );
210 resolve({prelude: stream});
211 }
212 const request = createPrerenderRequest(
213 model,
214 webpackMap,
215 onAllReady,
216 onFatalError,
217 options ? options.onError : undefined,
218 options ? options.identifierPrefix : undefined,
219 options ? options.temporaryReferences : undefined,
220 options ? options.startTime : undefined,
221 __DEV__ && options ? options.environmentName : undefined,
222 __DEV__ && options ? options.filterStackFrame : undefined,
223 false,
224 );
225 if (options && options.signal) {
226 attachAbortSignal(request, options.signal);
227 }
228 startWork(request);
229 });
230 }
231
232 function decodeReply<T>(
233 body: string | FormData,
234 webpackMap: ServerManifest,
235 options?: {
236 temporaryReferences?: TemporaryReferenceSet,
237 arraySizeLimit?: number,
238 },
239 ): Thenable<T> {
240 if (typeof body === 'string') {
241 const form = new FormData();
242 form.append('0', body);
243 body = form;
244 }
245 const response = createResponse(
246 webpackMap,
247 '',
248 options ? options.temporaryReferences : undefined,
249 body,
250 options ? options.arraySizeLimit : undefined,
251 );
252 const root = getRoot<T>(response);
253 close(response);
254 return root;
255 }
256
257 function decodeReplyFromAsyncIterable<T>(
258 iterable: AsyncIterable<[string, string | File]>,
259 webpackMap: ServerManifest,
260 options?: {
261 temporaryReferences?: TemporaryReferenceSet,
262 arraySizeLimit?: number,
263 },
264 ): Thenable<T> {
265 const iterator: AsyncIterator<[string, string | File]> =
266 iterable[ASYNC_ITERATOR]();
267
268 const response = createResponse(
269 webpackMap,
270 '',
271 options ? options.temporaryReferences : undefined,
272 undefined,
273 options ? options.arraySizeLimit : undefined,
274 );
275
276 function progress(
277 entry:
278 | {done: false, +value: [string, string | File], ...}
279 | {done: true, +value: void, ...},
280 ) {
281 if (entry.done) {
282 close(response);
283 } else {
284 const [name, value] = entry.value;
285 if (typeof value === 'string') {
286 resolveField(response, name, value);
287 } else {
288 resolveFile(response, name, value);
289 }
290 iterator.next().then(progress, error);
291 }
292 }
293 function error(reason: Error) {
294 reportGlobalError(response, reason);
295 if (typeof (iterator as any).throw === 'function') {
296 // The iterator protocol doesn't necessarily include this but a generator do.
297 // $FlowFixMe[prop-missing] should be able to pass mixed
298 iterator.throw(reason).then(noop, noop);
299 }
300 }
301
302 iterator.next().then(progress, error);
303
304 return getRoot(response);
305 }
306
307 export {
308 renderToReadableStream,
309 prerender,
310 decodeReply,
311 decodeReplyFromAsyncIterable,
312 decodeAction,
313 decodeFormState,
314 };