@@ -21,6 +21,9 @@ import type {
2121} from '../client/ReactFlightClientConfigBundlerParcel' ;
2222
2323import { Readable } from 'stream' ;
24+
25+ import { ASYNC_ITERATOR } from 'shared/ReactSymbols' ;
26+
2427import {
2528 createRequest ,
2629 createPrerenderRequest ,
@@ -35,6 +38,7 @@ import {
3538 reportGlobalError ,
3639 close ,
3740 resolveField ,
41+ resolveFile ,
3842 resolveFileInfo ,
3943 resolveFileChunk ,
4044 resolveFileComplete ,
@@ -56,9 +60,12 @@ export {
5660 registerServerReference ,
5761} from '../ReactFlightParcelReferences' ;
5862
63+ import { textEncoder } from 'react-server/src/ReactServerStreamConfigNode' ;
64+
5965import type { TemporaryReferenceSet } from 'react-server/src/ReactFlightServerTemporaryReferences' ;
6066
6167export { createTemporaryReferenceSet } from 'react-server/src/ReactFlightServerTemporaryReferences' ;
68+
6269export type { TemporaryReferenceSet } ;
6370
6471function createDrainHandler ( destination : Destination , request : Request ) {
@@ -131,11 +138,91 @@ export function renderToPipeableStream(
131138 } ;
132139}
133140
134- function createFakeWritable ( readable : any ) : Writable {
141+ function createFakeWritableFromReadableStreamController (
142+ controller : ReadableStreamController ,
143+ ) : Writable {
135144 // The current host config expects a Writable so we create
136145 // a fake writable for now to push into the Readable.
137146 return ( {
138- write ( chunk ) {
147+ write ( chunk : string | Uint8Array ) {
148+ if ( typeof chunk === 'string' ) {
149+ chunk = textEncoder . encode ( chunk ) ;
150+ }
151+ controller . enqueue ( chunk ) ;
152+ // in web streams there is no backpressure so we can alwas write more
153+ return true ;
154+ } ,
155+ end ( ) {
156+ controller . close ( ) ;
157+ } ,
158+ destroy ( error ) {
159+ // $FlowFixMe[method-unbinding]
160+ if ( typeof controller . error === 'function' ) {
161+ // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
162+ controller . error ( error ) ;
163+ } else {
164+ controller . close ( ) ;
165+ }
166+ } ,
167+ } : any ) ;
168+ }
169+
170+ export function renderToReadableStream (
171+ model : ReactClientValue ,
172+
173+ options ?: Options & {
174+ signal ?: AbortSignal ,
175+ } ,
176+ ) : ReadableStream {
177+ const request = createRequest (
178+ model ,
179+ null ,
180+ options ? options . onError : undefined ,
181+ options ? options . identifierPrefix : undefined ,
182+ options ? options . onPostpone : undefined ,
183+ options ? options . temporaryReferences : undefined ,
184+ __DEV__ && options ? options . environmentName : undefined ,
185+ __DEV__ && options ? options . filterStackFrame : undefined ,
186+ ) ;
187+ if ( options && options . signal ) {
188+ const signal = options . signal ;
189+ if ( signal . aborted ) {
190+ abort ( request , ( signal : any ) . reason ) ;
191+ } else {
192+ const listener = ( ) => {
193+ abort ( request , ( signal : any ) . reason ) ;
194+ signal . removeEventListener ( 'abort' , listener ) ;
195+ } ;
196+ signal . addEventListener ( 'abort' , listener ) ;
197+ }
198+ }
199+ let writable : Writable ;
200+ const stream = new ReadableStream (
201+ {
202+ type : 'bytes' ,
203+ start : ( controller ) : ?Promise < void > => {
204+ writable = createFakeWritableFromReadableStreamController ( controller ) ;
205+ startWork ( request ) ;
206+ } ,
207+ pull : ( controller ) : ?Promise < void > => {
208+ startFlowing ( request , writable ) ;
209+ } ,
210+ cancel : ( reason ) : ?Promise < void > => {
211+ stopFlowing ( request ) ;
212+ abort ( request , reason ) ;
213+ } ,
214+ } ,
215+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
216+ { highWaterMark : 0 } ,
217+ ) ;
218+ return stream ;
219+ }
220+
221+ function createFakeWritableFromNodeReadable ( readable : any ) : Writable {
222+ // The current host config expects a Writable so we create
223+ // a fake writable for now to push into the Readable.
224+ return ( {
225+ write ( chunk : string | Uint8Array ) {
139226 return readable . push ( chunk ) ;
140227 } ,
141228 end ( ) {
@@ -173,7 +260,7 @@ export function prerenderToNodeStream(
173260 startFlowing ( request , writable ) ;
174261 } ,
175262 } ) ;
176- const writable = createFakeWritable ( readable ) ;
263+ const writable = createFakeWritableFromNodeReadable ( readable ) ;
177264 resolve ( { prelude : readable } ) ;
178265 }
179266
@@ -207,6 +294,69 @@ export function prerenderToNodeStream(
207294 } ) ;
208295}
209296
297+ export function prerender (
298+ model : ReactClientValue ,
299+
300+ options ?: Options & {
301+ signal ?: AbortSignal ,
302+ } ,
303+ ) : Promise < {
304+ prelude : ReadableStream ,
305+ } > {
306+ return new Promise ( ( resolve , reject ) => {
307+ const onFatalError = reject ;
308+ function onAllReady ( ) {
309+ let writable : Writable ;
310+ const stream = new ReadableStream (
311+ {
312+ type : 'bytes' ,
313+ start : ( controller ) : ?Promise < void > => {
314+ writable =
315+ createFakeWritableFromReadableStreamController ( controller ) ;
316+ } ,
317+ pull : ( controller ) : ?Promise < void > => {
318+ startFlowing ( request , writable ) ;
319+ } ,
320+ cancel : ( reason ) : ?Promise < void > => {
321+ stopFlowing ( request ) ;
322+ abort ( request , reason ) ;
323+ } ,
324+ } ,
325+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
326+ { highWaterMark : 0 } ,
327+ ) ;
328+ resolve ( { prelude : stream } ) ;
329+ }
330+ const request = createPrerenderRequest (
331+ model ,
332+ null ,
333+ onAllReady ,
334+ onFatalError ,
335+ options ? options . onError : undefined ,
336+ options ? options . identifierPrefix : undefined ,
337+ options ? options . onPostpone : undefined ,
338+ options ? options . temporaryReferences : undefined ,
339+ __DEV__ && options ? options . environmentName : undefined ,
340+ __DEV__ && options ? options . filterStackFrame : undefined ,
341+ ) ;
342+ if ( options && options . signal ) {
343+ const signal = options . signal ;
344+ if ( signal . aborted ) {
345+ const reason = ( signal : any ) . reason ;
346+ abort ( request , reason ) ;
347+ } else {
348+ const listener = ( ) => {
349+ const reason = ( signal : any ) . reason ;
350+ abort ( request , reason ) ;
351+ signal . removeEventListener ( 'abort' , listener ) ;
352+ } ;
353+ signal . addEventListener ( 'abort' , listener ) ;
354+ }
355+ }
356+ startWork ( request ) ;
357+ } ) ;
358+ }
359+
210360let serverManifest = { } ;
211361export function registerServerActions ( manifest : ServerManifest ) {
212362 // This function is called by the bundler to register the manifest.
@@ -292,6 +442,50 @@ export function decodeReply<T>(
292442 return root ;
293443}
294444
445+ export function decodeReplyFromAsyncIterable < T > (
446+ iterable : AsyncIterable < [ string , string | File ] > ,
447+ options ?: { temporaryReferences ?: TemporaryReferenceSet } ,
448+ ) : Thenable < T > {
449+ const iterator : AsyncIterator < [ string , string | File ] > =
450+ iterable [ ASYNC_ITERATOR ] ( ) ;
451+
452+ const response = createResponse (
453+ serverManifest ,
454+ '' ,
455+ options ? options . temporaryReferences : undefined ,
456+ ) ;
457+
458+ function progress (
459+ entry :
460+ | { done : false , + value : [ string , string | File ] , ...}
461+ | { done : true , + value : void , ...} ,
462+ ) {
463+ if ( entry . done ) {
464+ close ( response ) ;
465+ } else {
466+ const [ name , value ] = entry . value ;
467+ if ( typeof value === 'string' ) {
468+ resolveField ( response , name , value ) ;
469+ } else {
470+ resolveFile ( response , name , value ) ;
471+ }
472+ iterator . next ( ) . then ( progress , error ) ;
473+ }
474+ }
475+ function error ( reason : Error ) {
476+ reportGlobalError ( response , reason ) ;
477+ if ( typeof ( iterator : any ) . throw === 'function' ) {
478+ // The iterator protocol doesn't necessarily include this but a generator do.
479+ // $FlowFixMe should be able to pass mixed
480+ iterator . throw ( reason ) . then ( error , error ) ;
481+ }
482+ }
483+
484+ iterator . next ( ) . then ( progress , error ) ;
485+
486+ return getRoot ( response ) ;
487+ }
488+
295489export function decodeAction < T > ( body : FormData ) : Promise < ( ) => T > | null {
296490 return decodeActionImpl ( body , serverManifest ) ;
297491}
0 commit comments