@@ -41,6 +41,8 @@ import {
4141 createRootFormatContext ,
4242} from 'react-dom-bindings/src/server/ReactFizzConfigDOM' ;
4343
44+ import { textEncoder } from 'react-server/src/ReactServerStreamConfigNode' ;
45+
4446import { ensureCorrectIsomorphicReactVersion } from '../shared/ensureCorrectIsomorphicReactVersion' ;
4547ensureCorrectIsomorphicReactVersion ( ) ;
4648
@@ -167,6 +169,141 @@ function renderToPipeableStream(
167169 } ;
168170}
169171
172+ function createFakeWritableFromReadableStreamController (
173+ controller : ReadableStreamController ,
174+ ) : Writable {
175+ // The current host config expects a Writable so we create
176+ // a fake writable for now to push into the Readable.
177+ return ( {
178+ write ( chunk : string | Uint8Array ) {
179+ if ( typeof chunk === 'string' ) {
180+ chunk = textEncoder . encode ( chunk ) ;
181+ }
182+ controller . enqueue ( chunk ) ;
183+ // in web streams there is no backpressure so we can alwas write more
184+ return true ;
185+ } ,
186+ end ( ) {
187+ controller . close ( ) ;
188+ } ,
189+ destroy(error) {
190+ // $FlowFixMe[method-unbinding]
191+ if ( typeof controller . error === 'function' ) {
192+ // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
193+ controller . error ( error ) ;
194+ } else {
195+ controller . close ( ) ;
196+ }
197+ } ,
198+ } : any ) ;
199+ }
200+
201+ // TODO: Move to sub-classing ReadableStream.
202+ type ReactDOMServerReadableStream = ReadableStream & {
203+ allReady : Promise < void > ,
204+ } ;
205+
206+ type WebStreamsOptions = Omit<
207+ Options ,
208+ 'onShellReady ' | 'onShellError ' | 'onAllReady ' | 'onHeaders ',
209+ > & { signal : AbortSignal , onHeaders ?: ( headers : Headers ) => void } ;
210+
211+ function renderToReadableStream(
212+ children: ReactNodeList,
213+ options?: WebStreamsOptions,
214+ ): Promise< ReactDOMServerReadableStream > {
215+ return new Promise ( ( resolve , reject ) => {
216+ let onFatalError ;
217+ let onAllReady ;
218+ const allReady = new Promise < void > ( ( res , rej ) => {
219+ onAllReady = res ;
220+ onFatalError = rej ;
221+ } ) ;
222+
223+ function onShellReady ( ) {
224+ let writable : Writable ;
225+ const stream : ReactDOMServerReadableStream = ( new ReadableStream (
226+ {
227+ type : 'bytes' ,
228+ start : ( controller ) : ?Promise < void > => {
229+ writable =
230+ createFakeWritableFromReadableStreamController ( controller ) ;
231+ } ,
232+ pull : ( controller ) : ?Promise < void > => {
233+ startFlowing ( request , writable ) ;
234+ } ,
235+ cancel : ( reason ) : ?Promise < void > => {
236+ stopFlowing ( request ) ;
237+ abort ( request , reason ) ;
238+ } ,
239+ } ,
240+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
241+ { highWaterMark : 0 } ,
242+ ) : any ) ;
243+ // TODO: Move to sub-classing ReadableStream.
244+ stream . allReady = allReady ;
245+ resolve ( stream ) ;
246+ }
247+ function onShellError ( error : mixed ) {
248+ // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
249+ // However, `allReady` will be rejected by `onFatalError` as well.
250+ // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
251+ allReady . catch ( ( ) => { } ) ;
252+ reject ( error ) ;
253+ }
254+
255+ const onHeaders = options ? options . onHeaders : undefined ;
256+ let onHeadersImpl ;
257+ if ( onHeaders ) {
258+ onHeadersImpl = ( headersDescriptor : HeadersDescriptor ) => {
259+ onHeaders ( new Headers ( headersDescriptor ) ) ;
260+ } ;
261+ }
262+
263+ const resumableState = createResumableState (
264+ options ? options . identifierPrefix : undefined ,
265+ options ? options . unstable_externalRuntimeSrc : undefined ,
266+ options ? options . bootstrapScriptContent : undefined ,
267+ options ? options . bootstrapScripts : undefined ,
268+ options ? options . bootstrapModules : undefined ,
269+ ) ;
270+ const request = createRequest (
271+ children ,
272+ resumableState ,
273+ createRenderState (
274+ resumableState ,
275+ options ? options . nonce : undefined ,
276+ options ? options . unstable_externalRuntimeSrc : undefined ,
277+ options ? options . importMap : undefined ,
278+ onHeadersImpl ,
279+ options ? options . maxHeadersLength : undefined ,
280+ ) ,
281+ createRootFormatContext ( options ? options . namespaceURI : undefined ) ,
282+ options ? options . progressiveChunkSize : undefined ,
283+ options ? options . onError : undefined ,
284+ onAllReady ,
285+ onShellReady ,
286+ onShellError ,
287+ onFatalError ,
288+ options ? options . onPostpone : undefined ,
289+ options ? options . formState : undefined ,
290+ ) ;
291+ if ( options && options . signal ) {
292+ const signal = options . signal ;
293+ if ( signal . aborted ) {
294+ abort ( request , ( signal : any ) . reason ) ;
295+ } else {
296+ const listener = ( ) => {
297+ abort ( request , ( signal : any ) . reason ) ;
298+ signal . removeEventListener ( 'abort' , listener ) ;
299+ } ;
300+ signal . addEventListener ( 'abort' , listener ) ;
301+ }
302+ }
303+ startWork ( request ) ;
304+ } ) ;
305+ }
306+
170307function resumeRequestImpl(
171308 children: ReactNodeList,
172309 postponedState: PostponedState,
@@ -225,8 +362,89 @@ function resumeToPipeableStream(
225362 } ;
226363}
227364
365+ type WebStreamsResumeOptions = Omit <
366+ Options ,
367+ 'onShellReady ' | 'onShellError ' | 'onAllReady ',
368+ > & { signal : AbortSignal } ;
369+
370+ function resume(
371+ children: ReactNodeList,
372+ postponedState: PostponedState,
373+ options?: WebStreamsResumeOptions,
374+ ): Promise< ReactDOMServerReadableStream > {
375+ return new Promise ( ( resolve , reject ) => {
376+ let onFatalError ;
377+ let onAllReady ;
378+ const allReady = new Promise < void > ( ( res , rej ) => {
379+ onAllReady = res ;
380+ onFatalError = rej ;
381+ } ) ;
382+
383+ function onShellReady ( ) {
384+ let writable : Writable ;
385+ const stream : ReactDOMServerReadableStream = ( new ReadableStream (
386+ {
387+ type : 'bytes' ,
388+ start : ( controller ) : ?Promise < void > => {
389+ writable =
390+ createFakeWritableFromReadableStreamController ( controller ) ;
391+ } ,
392+ pull : ( controller ) : ?Promise < void > => {
393+ startFlowing ( request , writable ) ;
394+ } ,
395+ cancel : ( reason ) : ?Promise < void > => {
396+ stopFlowing ( request ) ;
397+ abort ( request , reason ) ;
398+ } ,
399+ } ,
400+ // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
401+ { highWaterMark : 0 } ,
402+ ) : any ) ;
403+ // TODO: Move to sub-classing ReadableStream.
404+ stream . allReady = allReady ;
405+ resolve ( stream ) ;
406+ }
407+ function onShellError ( error : mixed ) {
408+ // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
409+ // However, `allReady` will be rejected by `onFatalError` as well.
410+ // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
411+ allReady . catch ( ( ) => { } ) ;
412+ reject ( error ) ;
413+ }
414+ const request = resumeRequest (
415+ children ,
416+ postponedState ,
417+ resumeRenderState (
418+ postponedState . resumableState ,
419+ options ? options . nonce : undefined ,
420+ ) ,
421+ options ? options . onError : undefined ,
422+ onAllReady ,
423+ onShellReady ,
424+ onShellError ,
425+ onFatalError ,
426+ options ? options . onPostpone : undefined ,
427+ ) ;
428+ if ( options && options . signal ) {
429+ const signal = options . signal ;
430+ if ( signal . aborted ) {
431+ abort ( request , ( signal : any ) . reason ) ;
432+ } else {
433+ const listener = ( ) => {
434+ abort ( request , ( signal : any ) . reason ) ;
435+ signal . removeEventListener ( 'abort' , listener ) ;
436+ } ;
437+ signal . addEventListener ( 'abort' , listener ) ;
438+ }
439+ }
440+ startWork ( request ) ;
441+ } ) ;
442+ }
443+
228444export {
229445 renderToPipeableStream ,
446+ renderToReadableStream ,
230447 resumeToPipeableStream ,
448+ resume ,
231449 ReactVersion as version ,
232450} ;
0 commit comments