@@ -56,7 +56,7 @@ function createCancelHandler(request: Request, reason: string) {
5656 } ;
5757}
5858
59- type Options = {
59+ type RenderToPipeableStreamOptions = {
6060 identifierPrefix ?: string ,
6161 namespaceURI ?: string ,
6262 nonce ?: string ,
@@ -92,7 +92,10 @@ type PipeableStream = {
9292 pipe < T : Writable > ( destination : T ) : T ,
9393} ;
9494
95- function createRequestImpl ( children : ReactNodeList , options : void | Options ) {
95+ function createRequestImpl (
96+ children : ReactNodeList ,
97+ options : void | RenderToPipeableStreamOptions ,
98+ ) {
9699 const resumableState = createResumableState (
97100 options ? options . identifierPrefix : undefined ,
98101 options ? options . unstable_externalRuntimeSrc : undefined ,
@@ -125,7 +128,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
125128
126129function renderToPipeableStream (
127130 children : ReactNodeList ,
128- options ?: Options ,
131+ options ?: RenderToPipeableStreamOptions ,
129132) : PipeableStream {
130133 const request = createRequestImpl ( children , options ) ;
131134 let hasStartedFlowing = false ;
@@ -160,6 +163,120 @@ function renderToPipeableStream(
160163 } ;
161164}
162165
166+ type RenderToReadableStreamOptions = {
167+ identifierPrefix ? : string ,
168+ namespaceURI ? : string ,
169+ nonce ? : string ,
170+ bootstrapScriptContent ? : string ,
171+ bootstrapScripts ?: Array < string | BootstrapScriptDescriptor > ,
172+ bootstrapModules ?: Array < string | BootstrapScriptDescriptor > ,
173+ progressiveChunkSize ? : number ,
174+ signal ? : AbortSignal ,
175+ onError ?: ( error : mixed , errorInfo : ErrorInfo ) => ?string ,
176+ onPostpone ?: ( reason : string , postponeInfo : PostponeInfo ) => void ,
177+ unstable_externalRuntimeSrc ?: string | BootstrapScriptDescriptor ,
178+ importMap ? : ImportMap ,
179+ formState ? : ReactFormState < any , any > | null ,
180+ onHeaders ?: ( headers : Headers ) => void ,
181+ maxHeadersLength ?: number ,
182+ } ;
183+
184+ // TODO: Move to sub-classing ReadableStream.
185+ type ReactDOMServerReadableStream = ReadableStream & {
186+ allReady : Promise < void > ,
187+ } ;
188+
189+ function renderToReadableStream(
190+ children: ReactNodeList,
191+ options?: RenderToReadableStreamOptions,
192+ ): Promise< ReactDOMServerReadableStream > {
193+ return new Promise ( ( resolve , reject ) => {
194+ let onFatalError ;
195+ let onAllReady ;
196+ const allReady = new Promise < void > ( ( res , rej ) => {
197+ onAllReady = res ;
198+ onFatalError = rej ;
199+ } ) ;
200+
201+ function onShellReady ( ) {
202+ const stream : ReactDOMServerReadableStream = ( new ReadableStream (
203+ {
204+ type : 'bytes' ,
205+ pull : ( controller ) : ?Promise < void > => {
206+ startFlowing ( request , controller ) ;
207+ } ,
208+ cancel : ( reason ) : ?Promise < void > => {
209+ stopFlowing ( request ) ;
210+ abort ( request , reason ) ;
211+ } ,
212+ } ,
213+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
214+ { highWaterMark : 0 } ,
215+ ) : any ) ;
216+ // TODO: Move to sub-classing ReadableStream.
217+ stream . allReady = allReady ;
218+ resolve ( stream ) ;
219+ }
220+ function onShellError ( error : mixed ) {
221+ // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
222+ // However, `allReady` will be rejected by `onFatalError` as well.
223+ // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
224+ allReady . catch ( ( ) => { } ) ;
225+ reject ( error ) ;
226+ }
227+
228+ const onHeaders = options ? options . onHeaders : undefined ;
229+ let onHeadersImpl ;
230+ if ( onHeaders ) {
231+ onHeadersImpl = ( headersDescriptor : HeadersDescriptor ) => {
232+ onHeaders ( new Headers ( headersDescriptor ) ) ;
233+ } ;
234+ }
235+
236+ const resumableState = createResumableState (
237+ options ? options . identifierPrefix : undefined ,
238+ options ? options . unstable_externalRuntimeSrc : undefined ,
239+ options ? options . bootstrapScriptContent : undefined ,
240+ options ? options . bootstrapScripts : undefined ,
241+ options ? options . bootstrapModules : undefined ,
242+ ) ;
243+ const request = createRequest (
244+ children ,
245+ resumableState ,
246+ createRenderState (
247+ resumableState ,
248+ options ? options . nonce : undefined ,
249+ options ? options . unstable_externalRuntimeSrc : undefined ,
250+ options ? options . importMap : undefined ,
251+ onHeadersImpl ,
252+ options ? options . maxHeadersLength : undefined ,
253+ ) ,
254+ createRootFormatContext ( options ? options . namespaceURI : undefined ) ,
255+ options ? options . progressiveChunkSize : undefined ,
256+ options ? options . onError : undefined ,
257+ onAllReady ,
258+ onShellReady ,
259+ onShellError ,
260+ onFatalError ,
261+ options ? options . onPostpone : undefined ,
262+ options ? options . formState : undefined ,
263+ ) ;
264+ if ( options && options . signal ) {
265+ const signal = options . signal ;
266+ if ( signal . aborted ) {
267+ abort ( request , ( signal : any ) . reason ) ;
268+ } else {
269+ const listener = ( ) => {
270+ abort ( request , ( signal : any ) . reason ) ;
271+ signal . removeEventListener ( 'abort' , listener ) ;
272+ } ;
273+ signal . addEventListener ( 'abort' , listener ) ;
274+ }
275+ }
276+ startWork ( request ) ;
277+ } ) ;
278+ }
279+
163280function resumeRequestImpl(
164281 children: ReactNodeList,
165282 postponedState: PostponedState,
@@ -220,6 +337,7 @@ function resumeToPipeableStream(
220337
221338export {
222339 renderToPipeableStream ,
340+ renderToReadableStream ,
223341 resumeToPipeableStream ,
224342 ReactVersion as version ,
225343} ;
0 commit comments