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