@@ -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 {
@@ -198,6 +200,123 @@ export function processReply(
198200 return '$' + tag + blobId . toString ( 16 ) ;
199201 }
200202
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+
201320 function resolveToJSON (
202321 this :
203322 | { + [ key : string | number ] : ReactServerValue }
@@ -341,11 +460,9 @@ export function processReply(
341460 reject ( reason ) ;
342461 }
343462 } ,
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 ,
349466 ) ;
350467 return serializePromiseID ( promiseId ) ;
351468 }
@@ -472,6 +589,25 @@ export function processReply(
472589 return Array . from ( ( iterator : any ) ) ;
473590 }
474591
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+
475611 // Verify that this is a simple plain object.
476612 const proto = getPrototypeOf ( value ) ;
477613 if (
0 commit comments