@@ -41,6 +41,8 @@ import {
41
41
createRootFormatContext ,
42
42
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM' ;
43
43
44
+ import { textEncoder } from 'react-server/src/ReactServerStreamConfigNode' ;
45
+
44
46
import { ensureCorrectIsomorphicReactVersion } from '../shared/ensureCorrectIsomorphicReactVersion' ;
45
47
ensureCorrectIsomorphicReactVersion ( ) ;
46
48
@@ -167,6 +169,141 @@ function renderToPipeableStream(
167
169
} ;
168
170
}
169
171
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
+
170
307
function resumeRequestImpl(
171
308
children: ReactNodeList,
172
309
postponedState: PostponedState,
@@ -225,8 +362,89 @@ function resumeToPipeableStream(
225
362
} ;
226
363
}
227
364
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
+
228
444
export {
229
445
renderToPipeableStream ,
446
+ renderToReadableStream ,
230
447
resumeToPipeableStream ,
448
+ resume ,
231
449
ReactVersion as version ,
232
450
} ;
0 commit comments