1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {PostponedState} from 'react-server/src/ReactFizzServer';
    
  11. import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
    
  12. import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
    
  13. import type {ImportMap} from '../shared/ReactDOMTypes';
    
  14. 
    
  15. import ReactVersion from 'shared/ReactVersion';
    
  16. 
    
  17. import {
    
  18.   createRequest,
    
  19.   resumeRequest,
    
  20.   startWork,
    
  21.   startFlowing,
    
  22.   stopFlowing,
    
  23.   abort,
    
  24. } from 'react-server/src/ReactFizzServer';
    
  25. 
    
  26. import {
    
  27.   createResumableState,
    
  28.   createRenderState,
    
  29.   resumeRenderState,
    
  30.   createRootFormatContext,
    
  31. } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
    
  32. 
    
  33. type Options = {
    
  34.   identifierPrefix?: string,
    
  35.   namespaceURI?: string,
    
  36.   nonce?: string,
    
  37.   bootstrapScriptContent?: string,
    
  38.   bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
    
  39.   bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
    
  40.   progressiveChunkSize?: number,
    
  41.   signal?: AbortSignal,
    
  42.   onError?: (error: mixed) => ?string,
    
  43.   onPostpone?: (reason: string) => void,
    
  44.   unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
    
  45.   importMap?: ImportMap,
    
  46.   formState?: ReactFormState<any, any> | null,
    
  47. };
    
  48. 
    
  49. type ResumeOptions = {
    
  50.   nonce?: string,
    
  51.   signal?: AbortSignal,
    
  52.   onError?: (error: mixed) => ?string,
    
  53.   onPostpone?: (reason: string) => void,
    
  54.   unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
    
  55. };
    
  56. 
    
  57. // TODO: Move to sub-classing ReadableStream.
    
  58. type ReactDOMServerReadableStream = ReadableStream & {
    
  59.   allReady: Promise<void>,
    
  60. };
    
  61. 
    
  62. function renderToReadableStream(
    
  63.   children: ReactNodeList,
    
  64.   options?: Options,
    
  65. ): Promise<ReactDOMServerReadableStream> {
    
  66.   return new Promise((resolve, reject) => {
    
  67.     let onFatalError;
    
  68.     let onAllReady;
    
  69.     const allReady = new Promise<void>((res, rej) => {
    
  70.       onAllReady = res;
    
  71.       onFatalError = rej;
    
  72.     });
    
  73. 
    
  74.     function onShellReady() {
    
  75.       const stream: ReactDOMServerReadableStream = (new ReadableStream(
    
  76.         {
    
  77.           type: 'bytes',
    
  78.           pull: (controller): ?Promise<void> => {
    
  79.             startFlowing(request, controller);
    
  80.           },
    
  81.           cancel: (reason): ?Promise<void> => {
    
  82.             stopFlowing(request);
    
  83.             abort(request, reason);
    
  84.           },
    
  85.         },
    
  86.         // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
    
  87.         {highWaterMark: 0},
    
  88.       ): any);
    
  89.       // TODO: Move to sub-classing ReadableStream.
    
  90.       stream.allReady = allReady;
    
  91.       resolve(stream);
    
  92.     }
    
  93.     function onShellError(error: mixed) {
    
  94.       // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
    
  95.       // However, `allReady` will be rejected by `onFatalError` as well.
    
  96.       // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
    
  97.       allReady.catch(() => {});
    
  98.       reject(error);
    
  99.     }
    
  100.     const resumableState = createResumableState(
    
  101.       options ? options.identifierPrefix : undefined,
    
  102.       options ? options.unstable_externalRuntimeSrc : undefined,
    
  103.     );
    
  104.     const request = createRequest(
    
  105.       children,
    
  106.       resumableState,
    
  107.       createRenderState(
    
  108.         resumableState,
    
  109.         options ? options.nonce : undefined,
    
  110.         options ? options.bootstrapScriptContent : undefined,
    
  111.         options ? options.bootstrapScripts : undefined,
    
  112.         options ? options.bootstrapModules : undefined,
    
  113.         options ? options.unstable_externalRuntimeSrc : undefined,
    
  114.         options ? options.importMap : undefined,
    
  115.       ),
    
  116.       createRootFormatContext(options ? options.namespaceURI : undefined),
    
  117.       options ? options.progressiveChunkSize : undefined,
    
  118.       options ? options.onError : undefined,
    
  119.       onAllReady,
    
  120.       onShellReady,
    
  121.       onShellError,
    
  122.       onFatalError,
    
  123.       options ? options.onPostpone : undefined,
    
  124.       options ? options.formState : undefined,
    
  125.     );
    
  126.     if (options && options.signal) {
    
  127.       const signal = options.signal;
    
  128.       if (signal.aborted) {
    
  129.         abort(request, (signal: any).reason);
    
  130.       } else {
    
  131.         const listener = () => {
    
  132.           abort(request, (signal: any).reason);
    
  133.           signal.removeEventListener('abort', listener);
    
  134.         };
    
  135.         signal.addEventListener('abort', listener);
    
  136.       }
    
  137.     }
    
  138.     startWork(request);
    
  139.   });
    
  140. }
    
  141. 
    
  142. function resume(
    
  143.   children: ReactNodeList,
    
  144.   postponedState: PostponedState,
    
  145.   options?: ResumeOptions,
    
  146. ): Promise<ReactDOMServerReadableStream> {
    
  147.   return new Promise((resolve, reject) => {
    
  148.     let onFatalError;
    
  149.     let onAllReady;
    
  150.     const allReady = new Promise<void>((res, rej) => {
    
  151.       onAllReady = res;
    
  152.       onFatalError = rej;
    
  153.     });
    
  154. 
    
  155.     function onShellReady() {
    
  156.       const stream: ReactDOMServerReadableStream = (new ReadableStream(
    
  157.         {
    
  158.           type: 'bytes',
    
  159.           pull: (controller): ?Promise<void> => {
    
  160.             startFlowing(request, controller);
    
  161.           },
    
  162.           cancel: (reason): ?Promise<void> => {
    
  163.             stopFlowing(request);
    
  164.             abort(request, reason);
    
  165.           },
    
  166.         },
    
  167.         // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
    
  168.         {highWaterMark: 0},
    
  169.       ): any);
    
  170.       // TODO: Move to sub-classing ReadableStream.
    
  171.       stream.allReady = allReady;
    
  172.       resolve(stream);
    
  173.     }
    
  174.     function onShellError(error: mixed) {
    
  175.       // If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
    
  176.       // However, `allReady` will be rejected by `onFatalError` as well.
    
  177.       // So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
    
  178.       allReady.catch(() => {});
    
  179.       reject(error);
    
  180.     }
    
  181.     const request = resumeRequest(
    
  182.       children,
    
  183.       postponedState,
    
  184.       resumeRenderState(
    
  185.         postponedState.resumableState,
    
  186.         options ? options.nonce : undefined,
    
  187.       ),
    
  188.       options ? options.onError : undefined,
    
  189.       onAllReady,
    
  190.       onShellReady,
    
  191.       onShellError,
    
  192.       onFatalError,
    
  193.       options ? options.onPostpone : undefined,
    
  194.     );
    
  195.     if (options && options.signal) {
    
  196.       const signal = options.signal;
    
  197.       if (signal.aborted) {
    
  198.         abort(request, (signal: any).reason);
    
  199.       } else {
    
  200.         const listener = () => {
    
  201.           abort(request, (signal: any).reason);
    
  202.           signal.removeEventListener('abort', listener);
    
  203.         };
    
  204.         signal.addEventListener('abort', listener);
    
  205.       }
    
  206.     }
    
  207.     startWork(request);
    
  208.   });
    
  209. }
    
  210. 
    
  211. export {renderToReadableStream, resume, ReactVersion as version};