@@ -20,6 +20,8 @@ import type {Thenable} from 'shared/ReactTypes';
2020
2121import { Readable } from 'stream' ;
2222
23+ import { ASYNC_ITERATOR } from 'shared/ReactSymbols' ;
24+
2325import {
2426 createRequest ,
2527 createPrerenderRequest ,
@@ -34,6 +36,7 @@ import {
3436 reportGlobalError ,
3537 close ,
3638 resolveField ,
39+ resolveFile ,
3740 resolveFileInfo ,
3841 resolveFileChunk ,
3942 resolveFileComplete ,
@@ -128,11 +131,88 @@ function renderToPipeableStream(
128131 } ;
129132}
130133
131- function createFakeWritable ( readable : any ) : Writable {
134+ function createFakeWritableFromReadableStreamController (
135+ controller : ReadableStreamController ,
136+ ) : Writable {
137+ // The current host config expects a Writable so we create
138+ // a fake writable for now to push into the Readable.
139+ return ( {
140+ write ( chunk : string | Uint8Array ) {
141+ controller . enqueue ( chunk ) ;
142+ // in web streams there is no backpressure so we can alwas write more
143+ return true ;
144+ } ,
145+ end ( ) {
146+ controller . close ( ) ;
147+ } ,
148+ destroy ( error ) {
149+ // $FlowFixMe[method-unbinding]
150+ if ( typeof controller . error === 'function' ) {
151+ // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
152+ controller . error ( error ) ;
153+ } else {
154+ controller . close ( ) ;
155+ }
156+ } ,
157+ } : any ) ;
158+ }
159+
160+ function renderToReadableStream (
161+ model : ReactClientValue ,
162+ webpackMap : ClientManifest ,
163+ options ?: Options & {
164+ signal ?: AbortSignal ,
165+ } ,
166+ ) : ReadableStream {
167+ const request = createRequest (
168+ model ,
169+ webpackMap ,
170+ options ? options . onError : undefined ,
171+ options ? options . identifierPrefix : undefined ,
172+ options ? options . onPostpone : undefined ,
173+ options ? options . temporaryReferences : undefined ,
174+ __DEV__ && options ? options . environmentName : undefined ,
175+ __DEV__ && options ? options . filterStackFrame : undefined ,
176+ ) ;
177+ if ( options && options . signal ) {
178+ const signal = options . signal ;
179+ if ( signal . aborted ) {
180+ abort ( request , ( signal : any ) . reason ) ;
181+ } else {
182+ const listener = ( ) => {
183+ abort ( request , ( signal : any ) . reason ) ;
184+ signal . removeEventListener ( 'abort' , listener ) ;
185+ } ;
186+ signal . addEventListener ( 'abort' , listener ) ;
187+ }
188+ }
189+ let writable : Writable ;
190+ const stream = new ReadableStream (
191+ {
192+ type : 'bytes' ,
193+ start : ( controller ) : ?Promise < void > => {
194+ writable = createFakeWritableFromReadableStreamController ( controller ) ;
195+ startWork ( request ) ;
196+ } ,
197+ pull : ( controller ) : ?Promise < void > => {
198+ startFlowing ( request , writable ) ;
199+ } ,
200+ cancel : ( reason ) : ?Promise < void > => {
201+ stopFlowing ( request ) ;
202+ abort ( request , reason ) ;
203+ } ,
204+ } ,
205+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
206+ { highWaterMark : 0 } ,
207+ ) ;
208+ return stream ;
209+ }
210+
211+ function createFakeWritableFromNodeReadable ( readable : any ) : Writable {
132212 // The current host config expects a Writable so we create
133213 // a fake writable for now to push into the Readable.
134214 return ( {
135- write ( chunk ) {
215+ write ( chunk : string | Uint8Array ) {
136216 return readable . push ( chunk ) ;
137217 } ,
138218 end ( ) {
@@ -171,7 +251,7 @@ function prerenderToNodeStream(
171251 startFlowing ( request , writable ) ;
172252 } ,
173253 } ) ;
174- const writable = createFakeWritable ( readable ) ;
254+ const writable = createFakeWritableFromNodeReadable ( readable ) ;
175255 resolve ( { prelude : readable } ) ;
176256 }
177257
@@ -205,6 +285,69 @@ function prerenderToNodeStream(
205285 } ) ;
206286}
207287
288+ function prerender (
289+ model : ReactClientValue ,
290+ webpackMap : ClientManifest ,
291+ options ?: Options & {
292+ signal ?: AbortSignal ,
293+ } ,
294+ ) : Promise < {
295+ prelude : ReadableStream ,
296+ } > {
297+ return new Promise ( ( resolve , reject ) => {
298+ const onFatalError = reject ;
299+ function onAllReady ( ) {
300+ let writable : Writable ;
301+ const stream = new ReadableStream (
302+ {
303+ type : 'bytes' ,
304+ start : ( controller ) : ?Promise < void > => {
305+ writable =
306+ createFakeWritableFromReadableStreamController ( controller ) ;
307+ } ,
308+ pull : ( controller ) : ?Promise < void > => {
309+ startFlowing ( request , writable ) ;
310+ } ,
311+ cancel : ( reason ) : ?Promise < void > => {
312+ stopFlowing ( request ) ;
313+ abort ( request , reason ) ;
314+ } ,
315+ } ,
316+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
317+ { highWaterMark : 0 } ,
318+ ) ;
319+ resolve ( { prelude : stream } ) ;
320+ }
321+ const request = createPrerenderRequest (
322+ model ,
323+ webpackMap ,
324+ onAllReady ,
325+ onFatalError ,
326+ options ? options . onError : undefined ,
327+ options ? options . identifierPrefix : undefined ,
328+ options ? options . onPostpone : undefined ,
329+ options ? options . temporaryReferences : undefined ,
330+ __DEV__ && options ? options . environmentName : undefined ,
331+ __DEV__ && options ? options . filterStackFrame : undefined ,
332+ ) ;
333+ if ( options && options . signal ) {
334+ const signal = options . signal ;
335+ if ( signal . aborted ) {
336+ const reason = ( signal : any ) . reason ;
337+ abort ( request , reason ) ;
338+ } else {
339+ const listener = ( ) => {
340+ const reason = ( signal : any ) . reason ;
341+ abort ( request , reason ) ;
342+ signal . removeEventListener ( 'abort' , listener ) ;
343+ } ;
344+ signal . addEventListener ( 'abort' , listener ) ;
345+ }
346+ }
347+ startWork ( request ) ;
348+ } ) ;
349+ }
350+
208351function decodeReplyFromBusboy < T > (
209352 busboyStream : Busboy ,
210353 webpackMap : ServerManifest ,
@@ -286,11 +429,59 @@ function decodeReply<T>(
286429 return root ;
287430}
288431
432+ function decodeReplyFromAsyncIterable < T > (
433+ iterable: AsyncIterable< [ string , string | File ] > ,
434+ webpackMap : ServerManifest ,
435+ options ?: { temporaryReferences ?: TemporaryReferenceSet } ,
436+ ) : Thenable < T > {
437+ const iterator : AsyncIterator < [ string , string | File ] > =
438+ iterable [ ASYNC_ITERATOR ] ( ) ;
439+
440+ const response = createResponse (
441+ webpackMap ,
442+ '' ,
443+ options ? options . temporaryReferences : undefined ,
444+ ) ;
445+
446+ function progress (
447+ entry :
448+ | { done : false , + value : [ string , string | File ] , ...}
449+ | { done : true , + value : void , ...} ,
450+ ) {
451+ if ( entry . done ) {
452+ close ( response ) ;
453+ } else {
454+ const [ name , value ] = entry . value ;
455+ if ( typeof value === 'string' ) {
456+ resolveField ( response , name , value ) ;
457+ } else {
458+ resolveFile ( response , name , value ) ;
459+ }
460+ iterator . next ( ) . then ( progress , error ) ;
461+ }
462+ }
463+ function error ( reason : Error ) {
464+ reportGlobalError ( response , reason ) ;
465+ if ( typeof ( iterator : any ) . throw === 'function' ) {
466+ // The iterator protocol doesn't necessarily include this but a generator do.
467+ // $FlowFixMe should be able to pass mixed
468+ iterator . throw ( reason ) . then ( error , error ) ;
469+ }
470+ }
471+
472+ iterator . next ( ) . then ( progress , error ) ;
473+
474+ return getRoot ( response ) ;
475+ }
476+
289477export {
478+ renderToReadableStream ,
290479 renderToPipeableStream,
480+ prerender,
291481 prerenderToNodeStream,
292- decodeReplyFromBusboy ,
293482 decodeReply,
483+ decodeReplyFromBusboy,
484+ decodeReplyFromAsyncIterable,
294485 decodeAction,
295486 decodeFormState,
296487} ;
0 commit comments