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