@@ -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 {
@@ -206,6 +208,123 @@ export function processReply(
206
208
return '$' + tag + blobId . toString ( 16 ) ;
207
209
}
208
210
211
+ function serializeReadableStream ( stream : ReadableStream ) : string {
212
+ if ( formData === null ) {
213
+ // Upgrade to use FormData to allow us to stream this value.
214
+ formData = new FormData ( ) ;
215
+ }
216
+ const data = formData;
217
+
218
+ pendingParts++;
219
+ const streamId = nextPartId++;
220
+
221
+ // Detect if this is a BYOB stream. BYOB streams should be able to be read as bytes on the
222
+ // receiving side. It also implies that different chunks can be split up or merged as opposed
223
+ // to a readable stream that happens to have Uint8Array as the type which might expect it to be
224
+ // received in the same slices.
225
+ // $FlowFixMe: This is a Node.js extension.
226
+ let supportsBYOB: void | boolean = stream.supportsBYOB;
227
+ if (supportsBYOB === undefined) {
228
+ try {
229
+ // $FlowFixMe[extra-arg]: This argument is accepted.
230
+ stream . getReader ( { mode : 'byob' } ) . releaseLock ( ) ;
231
+ supportsBYOB = true ;
232
+ } catch (x) {
233
+ supportsBYOB = false ;
234
+ }
235
+ }
236
+
237
+ const reader = stream . getReader ( ) ;
238
+
239
+ function progress ( entry : { done : boolean , value : ReactServerValue , ...} ) {
240
+ if ( entry . done ) {
241
+ // eslint-disable-next-line react-internal/safe-string-coercion
242
+ data . append ( formFieldPrefix + streamId , 'C' ) ; // Close signal
243
+ pendingParts -- ;
244
+ if ( pendingParts === 0 ) {
245
+ resolve ( data ) ;
246
+ }
247
+ } else {
248
+ try {
249
+ // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
250
+ const partJSON : string = JSON . stringify ( entry . value , resolveToJSON ) ;
251
+ // eslint-disable-next-line react-internal/safe-string-coercion
252
+ data . append ( formFieldPrefix + streamId , partJSON ) ;
253
+ reader . read ( ) . then ( progress , reject ) ;
254
+ } catch ( x ) {
255
+ reject ( x ) ;
256
+ }
257
+ }
258
+ }
259
+ reader . read ( ) . then ( progress , reject ) ;
260
+
261
+ return '$ ' + ( supportsBYOB ? 'r ' : 'R ') + streamId . toString ( 16 ) ;
262
+ }
263
+
264
+ function serializeAsyncIterable (
265
+ iterable : $AsyncIterable < ReactServerValue , ReactServerValue , void > ,
266
+ iterator: $AsyncIterator< ReactServerValue , ReactServerValue , void > ,
267
+ ): string {
268
+ if ( formData === null ) {
269
+ // Upgrade to use FormData to allow us to stream this value.
270
+ formData = new FormData ( ) ;
271
+ }
272
+ const data = formData;
273
+
274
+ pendingParts++;
275
+ const streamId = nextPartId++;
276
+
277
+ // Generators/Iterators are Iterables but they're also their own iterator
278
+ // functions. If that's the case, we treat them as single-shot. Otherwise,
279
+ // we assume that this iterable might be a multi-shot and allow it to be
280
+ // iterated more than once on the receiving server.
281
+ const isIterator = iterable === iterator;
282
+
283
+ // There's a race condition between when the stream is aborted and when the promise
284
+ // resolves so we track whether we already aborted it to avoid writing twice.
285
+ function progress(
286
+ entry:
287
+ | { done : false , + value : ReactServerValue , ...}
288
+ | { done : true , + value : ReactServerValue , ...} ,
289
+ ) {
290
+ if ( entry . done ) {
291
+ if ( entry . value === undefined ) {
292
+ // eslint-disable-next-line react-internal/safe-string-coercion
293
+ data . append ( formFieldPrefix + streamId , 'C' ) ; // Close signal
294
+ } else {
295
+ // Unlike streams, the last value may not be undefined. If it's not
296
+ // we outline it and encode a reference to it in the closing instruction.
297
+ try {
298
+ // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
299
+ const partJSON : string = JSON . stringify ( entry . value , resolveToJSON ) ;
300
+ data . append ( formFieldPrefix + streamId , 'C' + partJSON ) ; // Close signal
301
+ } catch ( x ) {
302
+ reject ( x ) ;
303
+ return ;
304
+ }
305
+ }
306
+ pendingParts -- ;
307
+ if ( pendingParts === 0 ) {
308
+ resolve ( data ) ;
309
+ }
310
+ } else {
311
+ try {
312
+ // $FlowFixMe[incompatible-type]: While plain JSON can return undefined we never do here.
313
+ const partJSON : string = JSON . stringify ( entry . value , resolveToJSON ) ;
314
+ // eslint-disable-next-line react-internal/safe-string-coercion
315
+ data. append ( formFieldPrefix + streamId , partJSON ) ;
316
+ iterator . next ( ) . then ( progress , reject ) ;
317
+ } catch (x) {
318
+ reject ( x ) ;
319
+ return ;
320
+ }
321
+ }
322
+ }
323
+
324
+ iterator . next ( ) . then ( progress , reject ) ;
325
+ return '$ ' + ( isIterator ? 'x ' : 'X ') + streamId . toString ( 16 ) ;
326
+ }
327
+
209
328
function resolveToJSON (
210
329
this :
211
330
| { + [ key : string | number ] : ReactServerValue }
@@ -349,11 +468,9 @@ export function processReply(
349
468
reject ( reason ) ;
350
469
}
351
470
} ,
352
- reason => {
353
- // In the future we could consider serializing this as an error
354
- // that throws on the server instead.
355
- reject ( reason ) ;
356
- } ,
471
+ // In the future we could consider serializing this as an error
472
+ // that throws on the server instead.
473
+ reject ,
357
474
) ;
358
475
return serializePromiseID ( promiseId ) ;
359
476
}
@@ -486,6 +603,25 @@ export function processReply(
486
603
return Array . from ( ( iterator : any ) ) ;
487
604
}
488
605
606
+ if (enableFlightReadableStream) {
607
+ // TODO: ReadableStream is not available in old Node. Remove the typeof check later.
608
+ if (
609
+ typeof ReadableStream === 'function' &&
610
+ value instanceof ReadableStream
611
+ ) {
612
+ return serializeReadableStream ( value ) ;
613
+ }
614
+ const getAsyncIterator: void | (() => $AsyncIterator < any , any , any > ) =
615
+ (value: any)[ASYNC_ITERATOR];
616
+ if (typeof getAsyncIterator === 'function') {
617
+ // We treat AsyncIterables as a Fragment and as such we might need to key them.
618
+ return serializeAsyncIterable (
619
+ ( value : any ) ,
620
+ getAsyncIterator . call ( ( value : any ) ) ,
621
+ ) ;
622
+ }
623
+ }
624
+
489
625
// Verify that this is a simple plain object.
490
626
const proto = getPrototypeOf ( value ) ;
491
627
if (
0 commit comments