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 {Destination} from 'react-server/src/ReactServerStreamConfigNode';
15
+import type {Busboy} from 'busboy';
16
+import type {Writable} from 'stream';
17
+import type {Thenable} from 'shared/ReactTypes';
18
+
19
+import type {Duplex} from 'stream';
20
+
21
+import {Readable} from 'stream';
22
+
23
+import {
24
+ createRequest,
25
+ startWork,
26
+ startFlowing,
27
+ startFlowingDebug,
28
+ stopFlowing,
29
+ abort,
30
+ resolveDebugMessage,
31
+ closeDebugChannel,
32
+} from 'react-server/src/ReactFlightServer';
33
+
34
+import {
35
+ createResponse,
36
+ reportGlobalError,
37
+ close,
38
+ resolveField,
39
+ resolveFileInfo,
40
+ resolveFileChunk,
41
+ resolveFileComplete,
42
+ getRoot,
43
+} from 'react-server/src/ReactFlightReplyServer';
44
+
45
+import {
46
+ decodeAction,
47
+ decodeFormState,
48
+} from 'react-server/src/ReactFlightActionServer';
49
+
50
+export {
51
+ registerServerReference,
52
+ registerClientReference,
53
+} from '../ReactFlightFBReferences';
54
+
55
+// Buffer-based string decoder helpers. The FB server environment does not
56
+// have TextDecoder, so we use Buffer.toString('utf8') instead.
57
+type BufferDecoder = {_pendingBytes: Array<Uint8Array>};
58
+
59
+function createStringDecoder(): BufferDecoder {
60
+ return {_pendingBytes: []};
61
+}
62
+
63
+function readPartialStringChunk(
64
+ decoder: BufferDecoder,
65
+ buffer: Uint8Array,
66
+): string {
67
+ return Buffer.from(
68
+ buffer.buffer,
69
+ buffer.byteOffset,
70
+ buffer.byteLength,
71
+ ).toString('utf8');
72
+}
73
+
74
+function readFinalStringChunk(
75
+ decoder: BufferDecoder,
76
+ buffer: Uint8Array,
77
+): string {
78
+ return Buffer.from(
79
+ buffer.buffer,
80
+ buffer.byteOffset,
81
+ buffer.byteLength,
82
+ ).toString('utf8');
83
+}
84
+
85
+import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
86
+
87
+export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
88
+
89
+export type {TemporaryReferenceSet};
90
+
91
+function createDrainHandler(destination: Destination, request: Request) {
92
+ return () => startFlowing(request, destination);
93
+}
94
+
95
+function createCancelHandler(request: Request, reason: string) {
96
+ return () => {
97
+ stopFlowing(request);
98
+ abort(request, new Error(reason));
99
+ };
100
+}
101
+
102
+function startReadingFromDebugChannelReadable(
103
+ request: Request,
104
+ stream: Readable | WebSocket,
105
+): void {
106
+ const stringDecoder = createStringDecoder();
107
+ let lastWasPartial = false;
108
+ let stringBuffer = '';
109
+ function onData(chunk: string | Uint8Array) {
110
+ if (typeof chunk === 'string') {
111
+ if (lastWasPartial) {
112
+ stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
113
+ lastWasPartial = false;
114
+ }
115
+ stringBuffer += chunk;
116
+ } else {
117
+ const buffer: Uint8Array = (chunk: any);
118
+ stringBuffer += readPartialStringChunk(stringDecoder, buffer);
119
+ lastWasPartial = true;
120
+ }
121
+ const messages = stringBuffer.split('\n');
122
+ for (let i = 0; i < messages.length - 1; i++) {
123
+ resolveDebugMessage(request, messages[i]);
124
+ }
125
+ stringBuffer = messages[messages.length - 1];
126
+ }
127
+ function onError(error: mixed) {
128
+ abort(
129
+ request,
130
+ new Error('Lost connection to the Debug Channel.', {
131
+ cause: error,
132
+ }),
133
+ );
134
+ }
135
+ function onClose() {
136
+ closeDebugChannel(request);
137
+ }
138
+ if (
139
+ // $FlowFixMe[method-unbinding]
140
+ typeof stream.addEventListener === 'function' &&
141
+ // $FlowFixMe[method-unbinding]
142
+ typeof stream.binaryType === 'string'
143
+ ) {
144
+ const ws: WebSocket = (stream: any);
145
+ ws.binaryType = 'arraybuffer';
146
+ ws.addEventListener('message', event => {
147
+ // $FlowFixMe
148
+ onData(event.data);
149
+ });
150
+ ws.addEventListener('error', event => {
151
+ // $FlowFixMe
152
+ onError(event.error);
153
+ });
154
+ ws.addEventListener('close', onClose);
155
+ } else {
156
+ const readable: Readable = (stream: any);
157
+ readable.on('data', onData);
158
+ readable.on('error', onError);
159
+ readable.on('end', onClose);
160
+ }
161
+}
162
+
163
+type Options = {
164
+ debugChannel?: Readable | Writable | Duplex | WebSocket,
165
+ environmentName?: string | (() => string),
166
+ filterStackFrame?: (url: string, functionName: string) => boolean,
167
+ onError?: (error: mixed) => void,
168
+ identifierPrefix?: string,
169
+ temporaryReferences?: TemporaryReferenceSet,
170
+ startTime?: number,
171
+};
172
+
173
+type PipeableStream = {
174
+ abort(reason: mixed): void,
175
+ pipe<T: Writable>(destination: T): T,
176
+};
177
+
178
+function renderToPipeableStream(
179
+ model: ReactClientValue,
180
+ options?: Options,
181
+): PipeableStream {
182
+ const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
183
+ const debugChannelReadable: void | Readable | WebSocket =
184
+ __DEV__ &&
185
+ debugChannel !== undefined &&
186
+ // $FlowFixMe[method-unbinding]
187
+ (typeof debugChannel.read === 'function' ||
188
+ typeof debugChannel.readyState === 'number')
189
+ ? (debugChannel: any)
190
+ : undefined;
191
+ const debugChannelWritable: void | Writable =
192
+ __DEV__ && debugChannel !== undefined
193
+ ? // $FlowFixMe[method-unbinding]
194
+ typeof debugChannel.write === 'function'
195
+ ? (debugChannel: any)
196
+ : // $FlowFixMe[method-unbinding]
197
+ typeof debugChannel.send === 'function'
198
+ ? createFakeWritableFromWebSocket((debugChannel: any))
199
+ : undefined
200
+ : undefined;
201
+ const request = createRequest(
202
+ model,
203
+ null,
204
+ options ? options.onError : undefined,
205
+ options ? options.identifierPrefix : undefined,
206
+ options ? options.temporaryReferences : undefined,
207
+ options ? options.startTime : undefined,
208
+ __DEV__ && options ? options.environmentName : undefined,
209
+ __DEV__ && options ? options.filterStackFrame : undefined,
210
+ debugChannelReadable !== undefined,
211
+ );
212
+ let hasStartedFlowing = false;
213
+ startWork(request);
214
+ if (debugChannelWritable !== undefined) {
215
+ startFlowingDebug(request, debugChannelWritable);
216
+ }
217
+ if (debugChannelReadable !== undefined) {
218
+ startReadingFromDebugChannelReadable(request, debugChannelReadable);
219
+ }
220
+ return {
221
+ pipe<T: Writable>(destination: T): T {
222
+ if (hasStartedFlowing) {
223
+ throw new Error(
224
+ 'React currently only supports piping to one writable stream.',
225
+ );
226
+ }
227
+ hasStartedFlowing = true;
228
+ startFlowing(request, destination);
229
+ destination.on('drain', createDrainHandler(destination, request));
230
+ destination.on(
231
+ 'error',
232
+ createCancelHandler(
233
+ request,
234
+ 'The destination stream errored while writing data.',
235
+ ),
236
+ );
237
+ // We don't close until the debug channel closes.
238
+ if (!__DEV__ || debugChannelReadable === undefined) {
239
+ destination.on(
240
+ 'close',
241
+ createCancelHandler(request, 'The destination stream closed early.'),
242
+ );
243
+ }
244
+ return destination;
245
+ },
246
+ abort(reason: mixed) {
247
+ abort(request, reason);
248
+ },
249
+ };
250
+}
251
+
252
+function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable {
253
+ return ({
254
+ write(chunk: string | Uint8Array) {
255
+ webSocket.send((chunk: any));
256
+ return true;
257
+ },
258
+ end() {
259
+ webSocket.close();
260
+ },
261
+ destroy(reason) {
262
+ if (typeof reason === 'object' && reason !== null) {
263
+ reason = reason.message;
264
+ }
265
+ if (typeof reason === 'string') {
266
+ webSocket.close(1011, reason);
267
+ } else {
268
+ webSocket.close(1011);
269
+ }
270
+ },
271
+ }: any);
272
+}
273
+
274
+function decodeReplyFromBusboy<T>(
275
+ busboyStream: Busboy,
276
+ options?: {
277
+ temporaryReferences?: TemporaryReferenceSet,
278
+ arraySizeLimit?: number,
279
+ },
280
+): Thenable<T> {
281
+ const response = createResponse(
282
+ null,
283
+ '',
284
+ options ? options.temporaryReferences : undefined,
285
+ undefined,
286
+ options ? options.arraySizeLimit : undefined,
287
+ );
288
+ let pendingFiles = 0;
289
+ const queuedFields: Array<string> = [];
290
+ busboyStream.on('field', (name, value) => {
291
+ if (pendingFiles > 0) {
292
+ // Because the 'end' event fires two microtasks after the next 'field'
293
+ // we would resolve files and fields out of order. To handle this properly
294
+ // we queue any fields we receive until the previous file is done.
295
+ queuedFields.push(name, value);
296
+ } else {
297
+ try {
298
+ resolveField(response, name, value);
299
+ } catch (error) {
300
+ busboyStream.destroy(error);
301
+ }
302
+ }
303
+ });
304
+ busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
305
+ if (encoding.toLowerCase() === 'base64') {
306
+ busboyStream.destroy(
307
+ new Error(
308
+ "React doesn't accept base64 encoded file uploads because we don't expect " +
309
+ "form data passed from a browser to ever encode data that way. If that's " +
310
+ 'the wrong assumption, we can easily fix it.',
311
+ ),
312
+ );
313
+ return;
314
+ }
315
+ pendingFiles++;
316
+ const file = resolveFileInfo(response, name, filename, mimeType);
317
+ value.on('data', chunk => {
318
+ resolveFileChunk(response, file, chunk);
319
+ });
320
+ value.on('end', () => {
321
+ try {
322
+ resolveFileComplete(response, name, file);
323
+ pendingFiles--;
324
+ if (pendingFiles === 0) {
325
+ // Release any queued fields
326
+ for (let i = 0; i < queuedFields.length; i += 2) {
327
+ resolveField(response, queuedFields[i], queuedFields[i + 1]);
328
+ }
329
+ queuedFields.length = 0;
330
+ }
331
+ } catch (error) {
332
+ busboyStream.destroy(error);
333
+ }
334
+ });
335
+ });
336
+ busboyStream.on('finish', () => {
337
+ close(response);
338
+ });
339
+ busboyStream.on('error', err => {
340
+ reportGlobalError(
341
+ response,
342
+ // $FlowFixMe[incompatible-call] types Error and mixed are incompatible
343
+ err,
344
+ );
345
+ });
346
+ return getRoot(response);
347
+}
348
+
349
+function decodeReply<T>(
350
+ body: string | FormData,
351
+ options?: {
352
+ temporaryReferences?: TemporaryReferenceSet,
353
+ arraySizeLimit?: number,
354
+ },
355
+): Thenable<T> {
356
+ if (typeof body === 'string') {
357
+ const form = new FormData();
358
+ form.append('0', body);
359
+ body = form;
360
+ }
361
+ const response = createResponse(
362
+ null,
363
+ '',
364
+ options ? options.temporaryReferences : undefined,
365
+ body,
366
+ options ? options.arraySizeLimit : undefined,
367
+ );
368
+ const root = getRoot<T>(response);
369
+ close(response);
370
+ return root;
371
+}
372
+
373
+export {
374
+ renderToPipeableStream,
375
+ decodeReply,
376
+ decodeReplyFromBusboy,
377
+ decodeAction,
378
+ decodeFormState,
379
+};