main
js 317 lines 8.56 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 {Thenable} from 'shared/ReactTypes.js';
11 import type {
12 DebugChannel,
13 DebugChannelCallback,
14 Response as FlightResponse,
15 } from 'react-client/src/ReactFlightClient';
16 import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
17 import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerParcel';
18
19 import {
20 createResponse,
21 createStreamState,
22 getRoot,
23 reportGlobalError,
24 processBinaryChunk,
25 processStringChunk,
26 close,
27 injectIntoDevTools,
28 } from 'react-client/src/ReactFlightClient';
29
30 import {
31 processReply,
32 createServerReference as createServerReferenceImpl,
33 } from 'react-client/src/ReactFlightReplyClient';
34
35 export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
36
37 import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
38
39 export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
40 export type {TemporaryReferenceSet};
41
42 function findSourceMapURL(filename: string, environmentName: string) {
43 const devServer = parcelRequire.meta.devServer;
44 if (devServer != null) {
45 const qs = new URLSearchParams();
46 qs.set('filename', filename);
47 qs.set('env', environmentName);
48 return devServer + '/__parcel_source_map?' + qs.toString();
49 }
50 return null;
51 }
52
53 type CallServerCallback = <A, T>(id: string, args: A) => Promise<T>;
54
55 let callServer: CallServerCallback | null = null;
56 export function setServerCallback(fn: CallServerCallback) {
57 callServer = fn;
58 }
59
60 function callCurrentServerCallback<A, T>(
61 id: ServerReferenceId,
62 args: A,
63 ): Promise<T> {
64 if (!callServer) {
65 throw new Error(
66 'No server callback has been registered. Call setServerCallback to register one.',
67 );
68 }
69 return callServer(id, args);
70 }
71
72 export function createServerReference<A: Iterable<any>, T>(
73 id: string,
74 exportName: string,
75 ): (...A) => Promise<T> {
76 return createServerReferenceImpl(
77 id + '#' + exportName,
78 callCurrentServerCallback,
79 undefined,
80 findSourceMapURL,
81 exportName,
82 );
83 }
84
85 function createDebugCallbackFromWritableStream(
86 debugWritable: WritableStream,
87 ): DebugChannelCallback {
88 const textEncoder = new TextEncoder();
89 const writer = debugWritable.getWriter();
90 return message => {
91 if (message === '') {
92 writer.close();
93 } else {
94 // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
95 // Therefore, we can't report errors from this write back to the Response object.
96 if (__DEV__) {
97 writer.write(textEncoder.encode(message + '\n')).catch(console.error);
98 }
99 }
100 };
101 }
102
103 function createResponseFromOptions(options: void | Options) {
104 const debugChannel: void | DebugChannel =
105 __DEV__ && options && options.debugChannel !== undefined
106 ? {
107 hasReadable: options.debugChannel.readable !== undefined,
108 callback:
109 options.debugChannel.writable !== undefined
110 ? createDebugCallbackFromWritableStream(
111 options.debugChannel.writable,
112 )
113 : null,
114 }
115 : undefined;
116
117 return createResponse(
118 null, // bundlerConfig
119 null, // serverReferenceConfig
120 null, // moduleLoading
121 callCurrentServerCallback,
122 undefined, // encodeFormAction
123 undefined, // nonce
124 options && options.temporaryReferences
125 ? options.temporaryReferences
126 : undefined,
127 options && options.unstable_allowPartialStream
128 ? options.unstable_allowPartialStream
129 : false,
130 __DEV__ ? findSourceMapURL : undefined,
131 __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true
132 __DEV__ && options && options.environmentName
133 ? options.environmentName
134 : undefined,
135 __DEV__ && options && options.startTime != null
136 ? options.startTime
137 : undefined,
138 __DEV__ && options && options.endTime != null ? options.endTime : undefined,
139 debugChannel,
140 );
141 }
142
143 function startReadingFromUniversalStream(
144 response: FlightResponse,
145 stream: ReadableStream,
146 onDone: () => void,
147 ): void {
148 // This is the same as startReadingFromStream except this allows WebSocketStreams which
149 // return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
150 // always allow streams with variable chunk types.
151 const streamState = createStreamState(response, stream);
152 const reader = stream.getReader();
153 function progress({
154 done,
155 value,
156 }: {
157 done: boolean,
158 value: any,
159 ...
160 }): void | Promise<void> {
161 if (done) {
162 return onDone();
163 }
164 if (value instanceof ArrayBuffer) {
165 // WebSockets can produce ArrayBuffer values in ReadableStreams.
166 processBinaryChunk(response, streamState, new Uint8Array(value));
167 } else if (typeof value === 'string') {
168 // WebSockets can produce string values in ReadableStreams.
169 processStringChunk(response, streamState, value);
170 } else {
171 processBinaryChunk(response, streamState, value);
172 }
173 return reader.read().then(progress).catch(error);
174 }
175 function error(e: any) {
176 reportGlobalError(response, e);
177 }
178 reader.read().then(progress).catch(error);
179 }
180
181 function startReadingFromStream(
182 response: FlightResponse,
183 stream: ReadableStream,
184 onDone: () => void,
185 debugValue: mixed,
186 ): void {
187 const streamState = createStreamState(response, debugValue);
188 const reader = stream.getReader();
189 function progress({
190 done,
191 value,
192 }: {
193 done: boolean,
194 value: ?any,
195 ...
196 }): void | Promise<void> {
197 if (done) {
198 return onDone();
199 }
200 const buffer: Uint8Array = value as any;
201 processBinaryChunk(response, streamState, buffer);
202 return reader.read().then(progress).catch(error);
203 }
204 function error(e: any) {
205 reportGlobalError(response, e);
206 }
207 reader.read().then(progress).catch(error);
208 }
209
210 export type Options = {
211 debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...},
212 temporaryReferences?: TemporaryReferenceSet,
213 unstable_allowPartialStream?: boolean,
214 replayConsoleLogs?: boolean,
215 environmentName?: string,
216 startTime?: number,
217 endTime?: number,
218 };
219
220 export function createFromReadableStream<T>(
221 stream: ReadableStream,
222 options?: Options,
223 ): Thenable<T> {
224 const response: FlightResponse = createResponseFromOptions(options);
225 if (
226 __DEV__ &&
227 options &&
228 options.debugChannel &&
229 options.debugChannel.readable
230 ) {
231 let streamDoneCount = 0;
232 const handleDone = () => {
233 if (++streamDoneCount === 2) {
234 close(response);
235 }
236 };
237 startReadingFromUniversalStream(
238 response,
239 options.debugChannel.readable,
240 handleDone,
241 );
242 startReadingFromStream(response, stream, handleDone, stream);
243 } else {
244 startReadingFromStream(
245 response,
246 stream,
247 close.bind(null, response),
248 stream,
249 );
250 }
251 return getRoot(response);
252 }
253
254 export function createFromFetch<T>(
255 promiseForResponse: Promise<Response>,
256 options?: Options,
257 ): Thenable<T> {
258 const response: FlightResponse = createResponseFromOptions(options);
259 promiseForResponse.then(
260 function (r) {
261 if (
262 __DEV__ &&
263 options &&
264 options.debugChannel &&
265 options.debugChannel.readable
266 ) {
267 let streamDoneCount = 0;
268 const handleDone = () => {
269 if (++streamDoneCount === 2) {
270 close(response);
271 }
272 };
273 startReadingFromUniversalStream(
274 response,
275 options.debugChannel.readable,
276 handleDone,
277 );
278 startReadingFromStream(response, r.body as any, handleDone, r);
279 } else {
280 startReadingFromStream(
281 response,
282 r.body as any,
283 close.bind(null, response),
284 r,
285 );
286 }
287 },
288 function (e) {
289 reportGlobalError(response, e);
290 },
291 );
292 return getRoot(response);
293 }
294
295 export function encodeReply(
296 value: ReactServerValue,
297 options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal},
298 ): Promise<
299 string | URLSearchParams | FormData,
300 > /* We don't use URLSearchParams yet but maybe */ {
301 return new Promise((resolve, reject) => {
302 processReply(
303 value,
304 '', // formFieldPrefix
305 options && options.temporaryReferences
306 ? options.temporaryReferences
307 : undefined,
308 resolve,
309 reject,
310 options ? options.signal : undefined,
311 );
312 });
313 }
314
315 if (__DEV__) {
316 injectIntoDevTools();
317 }