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