@@ -20,6 +20,7 @@ import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
2020import {
2121 enableRenderableContext ,
2222 enableBinaryFlight ,
23+ enableFlightReadableStream ,
2324} from 'shared/ReactFeatureFlags' ;
2425
2526import {
@@ -28,6 +29,7 @@ import {
2829 REACT_CONTEXT_TYPE ,
2930 REACT_PROVIDER_TYPE ,
3031 getIteratorFn ,
32+ ASYNC_ITERATOR ,
3133} from 'shared/ReactSymbols' ;
3234
3335import {
@@ -206,6 +208,123 @@ export function processReply(
206208 return '$' + tag + blobId . toString ( 16 ) ;
207209 }
208210
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+
209328 function resolveToJSON (
210329 this :
211330 | { + [ key : string | number ] : ReactServerValue }
@@ -349,11 +468,9 @@ export function processReply(
349468 reject ( reason ) ;
350469 }
351470 } ,
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 ,
357474 ) ;
358475 return serializePromiseID ( promiseId ) ;
359476 }
@@ -486,6 +603,25 @@ export function processReply(
486603 return Array . from ( ( iterator : any ) ) ;
487604 }
488605
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+
489625 // Verify that this is a simple plain object.
490626 const proto = getPrototypeOf ( value ) ;
491627 if (
0 commit comments