main
js 284 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 function createFromReadableStream<T>(
186 stream: ReadableStream,
187 options?: Options,
188 ): Thenable<T> {
189 const response: FlightResponse = createResponseFromOptions(options);
190 if (
191 __DEV__ &&
192 options &&
193 options.debugChannel &&
194 options.debugChannel.readable
195 ) {
196 let streamDoneCount = 0;
197 const handleDone = () => {
198 if (++streamDoneCount === 2) {
199 close(response);
200 }
201 };
202 startReadingFromUniversalStream(
203 response,
204 options.debugChannel.readable,
205 handleDone,
206 );
207 startReadingFromStream(response, stream, handleDone, stream);
208 } else {
209 startReadingFromStream(
210 response,
211 stream,
212 close.bind(null, response),
213 stream,
214 );
215 }
216 return getRoot(response);
217 }
218
219 function createFromFetch<T>(
220 promiseForResponse: Promise<Response>,
221 options?: Options,
222 ): Thenable<T> {
223 const response: FlightResponse = createResponseFromOptions(options);
224 promiseForResponse.then(
225 function (r) {
226 if (
227 __DEV__ &&
228 options &&
229 options.debugChannel &&
230 options.debugChannel.readable
231 ) {
232 let streamDoneCount = 0;
233 const handleDone = () => {
234 if (++streamDoneCount === 2) {
235 close(response);
236 }
237 };
238 startReadingFromUniversalStream(
239 response,
240 options.debugChannel.readable,
241 handleDone,
242 );
243 startReadingFromStream(response, r.body as any, handleDone, r);
244 } else {
245 startReadingFromStream(
246 response,
247 r.body as any,
248 close.bind(null, response),
249 r,
250 );
251 }
252 },
253 function (e) {
254 reportGlobalError(response, e);
255 },
256 );
257 return getRoot(response);
258 }
259
260 function encodeReply(
261 value: ReactServerValue,
262 options?: {temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal},
263 ): Promise<
264 string | URLSearchParams | FormData,
265 > /* We don't use URLSearchParams yet but maybe */ {
266 return new Promise((resolve, reject) => {
267 processReply(
268 value,
269 '',
270 options && options.temporaryReferences
271 ? options.temporaryReferences
272 : undefined,
273 resolve,
274 reject,
275 options ? options.signal : undefined,
276 );
277 });
278 }
279
280 export {createFromFetch, createFromReadableStream, encodeReply};
281
282 if (__DEV__) {
283 injectIntoDevTools();
284 }