@@ -20,6 +20,7 @@ import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
20
20
import {
21
21
enableRenderableContext ,
22
22
enableBinaryFlight ,
23
+ enableFlightReadableStream ,
23
24
} from 'shared/ReactFeatureFlags' ;
24
25
25
26
import {
@@ -28,6 +29,7 @@ import {
28
29
REACT_CONTEXT_TYPE ,
29
30
REACT_PROVIDER_TYPE ,
30
31
getIteratorFn ,
32
+ ASYNC_ITERATOR ,
31
33
} from 'shared/ReactSymbols' ;
32
34
33
35
import {
@@ -198,6 +200,123 @@ export function processReply(
198
200
return '$' + tag + blobId . toString ( 16 ) ;
199
201
}
200
202
203
+ function serializeReadableStream(stream: ReadableStream): string {
204
+ if ( formData === null ) {
205
+ // Upgrade to use FormData to allow us to stream this value.
206
+ formData = new FormData ( ) ;
207
+ }
208
+ const data = formData;
209
+
210
+ pendingParts++;
211
+ const streamId = nextPartId++;
212
+
213
+ // Detect if this is a BYOB stream. BYOB streams should be able to be read as bytes on the
214
+ // receiving side. It also implies that different chunks can be split up or merged as opposed
215
+ // to a readable stream that happens to have Uint8Array as the type which might expect it to be
216
+ // received in the same slices.
217
+ // $FlowFixMe: This is a Node.js extension.
218
+ let supportsBYOB: void | boolean = stream.supportsBYOB;
219
+ if (supportsBYOB === undefined) {
220
+ try {
221
+ // $FlowFixMe[extra-arg]: This argument is accepted.
222
+ stream . getReader ( { mode : 'byob' } ) . releaseLock ( ) ;
223
+ supportsBYOB = true ;
224
+ } catch (x) {
225
+ supportsBYOB = false ;
226
+ }
227
+ }
228
+
229
+ const reader = stream . getReader ( ) ;
230
+
231
+ function progress ( entry : { done : boolean , value : ReactServerValue , ...} ) {
232
+ if ( entry . done ) {
233
+ // eslint-disable-next-line react-internal/safe-string-coercion
234
+ data . append ( formFieldPrefix + streamId , 'C' ) ; // Close signal
235
+ pendingParts -- ;
236
+ if ( pendingParts === 0 ) {
237
+ resolve ( data ) ;
238
+ }
239
+ } else {
240
+ try {
241
+ // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
242
+ const partJSON : string = JSON . stringify ( entry . value , resolveToJSON ) ;
243
+ // eslint-disable-next-line react-internal/safe-string-coercion
244
+ data . append ( formFieldPrefix + streamId , partJSON ) ;
245
+ reader . read ( ) . then ( progress , reject ) ;
246
+ } catch ( x ) {
247
+ reject ( x ) ;
248
+ }
249
+ }
250
+ }
251
+ reader . read ( ) . then ( progress , reject ) ;
252
+
253
+ return '$ ' + ( supportsBYOB ? 'r ' : 'R ') + streamId . toString ( 16 ) ;
254
+ }
255
+
256
+ function serializeAsyncIterable (
257
+ iterable : $AsyncIterable < ReactServerValue , ReactServerValue , void > ,
258
+ iterator: $AsyncIterator< ReactServerValue , ReactServerValue , void > ,
259
+ ): string {
260
+ if ( formData === null ) {
261
+ // Upgrade to use FormData to allow us to stream this value.
262
+ formData = new FormData ( ) ;
263
+ }
264
+ const data = formData;
265
+
266
+ pendingParts++;
267
+ const streamId = nextPartId++;
268
+
269
+ // Generators/Iterators are Iterables but they're also their own iterator
270
+ // functions. If that's the case, we treat them as single-shot. Otherwise,
271
+ // we assume that this iterable might be a multi-shot and allow it to be
272
+ // iterated more than once on the client.
273
+ const isIterator = iterable === iterator;
274
+
275
+ // There's a race condition between when the stream is aborted and when the promise
276
+ // resolves so we track whether we already aborted it to avoid writing twice.
277
+ function progress(
278
+ entry:
279
+ | { done : false , + value : ReactServerValue , ...}
280
+ | { done : true , + value : ReactServerValue , ...} ,
281
+ ) {
282
+ if ( entry . done ) {
283
+ if ( entry . value === undefined ) {
284
+ // eslint-disable-next-line react-internal/safe-string-coercion
285
+ data . append ( formFieldPrefix + streamId , 'C' ) ; // Close signal
286
+ } else {
287
+ // Unlike streams, the last value may not be undefined. If it's not
288
+ // we outline it and encode a reference to it in the closing instruction.
289
+ try {
290
+ // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
291
+ const partJSON : string = JSON . stringify ( entry . value , resolveToJSON ) ;
292
+ data . append ( formFieldPrefix + streamId , 'C' + partJSON ) ; // Close signal
293
+ } catch ( x ) {
294
+ reject ( x ) ;
295
+ return ;
296
+ }
297
+ }
298
+ pendingParts -- ;
299
+ if ( pendingParts === 0 ) {
300
+ resolve ( data ) ;
301
+ }
302
+ } else {
303
+ try {
304
+ // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
305
+ const partJSON : string = JSON . stringify ( entry . value , resolveToJSON ) ;
306
+ // eslint-disable-next-line react-internal/safe-string-coercion
307
+ data. append ( formFieldPrefix + streamId , partJSON ) ;
308
+ iterator . next ( ) . then ( progress , reject ) ;
309
+ } catch (x) {
310
+ reject ( x ) ;
311
+ return ;
312
+ }
313
+ }
314
+ }
315
+
316
+ iterator . next ( ) . then ( progress , reject ) ;
317
+ return '$ ' + ( isIterator ? 'x ' : 'X ') + streamId . toString ( 16 ) ;
318
+ }
319
+
201
320
function resolveToJSON (
202
321
this :
203
322
| { + [ key : string | number ] : ReactServerValue }
@@ -341,11 +460,9 @@ export function processReply(
341
460
reject ( reason ) ;
342
461
}
343
462
} ,
344
- reason => {
345
- // In the future we could consider serializing this as an error
346
- // that throws on the server instead.
347
- reject ( reason ) ;
348
- } ,
463
+ // In the future we could consider serializing this as an error
464
+ // that throws on the server instead.
465
+ reject ,
349
466
) ;
350
467
return serializePromiseID ( promiseId ) ;
351
468
}
@@ -472,6 +589,25 @@ export function processReply(
472
589
return Array . from ( ( iterator : any ) ) ;
473
590
}
474
591
592
+ if (enableFlightReadableStream) {
593
+ // TODO: ReadableStream is not available in old Node. Remove the typeof check later.
594
+ if (
595
+ typeof ReadableStream === 'function' &&
596
+ value instanceof ReadableStream
597
+ ) {
598
+ return serializeReadableStream ( value ) ;
599
+ }
600
+ const getAsyncIterator: void | (() => $AsyncIterator < any , any , any > ) =
601
+ (value: any)[ASYNC_ITERATOR];
602
+ if (typeof getAsyncIterator === 'function') {
603
+ // We treat AsyncIterables as a Fragment and as such we might need to key them.
604
+ return serializeAsyncIterable (
605
+ ( value : any ) ,
606
+ getAsyncIterator . call ( ( value : any ) ) ,
607
+ ) ;
608
+ }
609
+ }
610
+
475
611
// Verify that this is a simple plain object.
476
612
const proto = getPrototypeOf ( value ) ;
477
613
if (
0 commit comments