@samitouri / QOS-React-2 / commits / cb1e7b1c6c

Move onCompleteAll to .allReady Promise (#24025)

* Move onCompleteAll to .allReady Promise The onCompleteAll callback can sometimes resolve before the promise that returns the stream which is tough to coordinate. A more idiomatic API for a one shot event is a Promise. That way the way you render for SEO or SSG is: const stream = await renderToReadableStream(...); await stream.readyAll; respondWith(stream); Ideally this should be a sub-class of ReadableStream but we don't yet compile these to ES6 and they'd had to be to native class to subclass a native stream. I have other ideas for overriding the .tee() method in a subclass anyway. So this is inline with that strategy. * Reject the Promise on fatal errors

Sebastian Markbåge committed Mar 3, 2022 at 12:46 UTC cb1e7b1c6ccd989d54b376ee4ae9da72a34f96e5
6 files changed +29 -10
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+3 -5
@@ -105,12 +105,10 @@ describe('ReactDOMFizzServer', () => {
105 <Wait />
106 </Suspense>
107 </div>,
108 - {
109 - onCompleteAll() {
110 - isComplete = true;
111 - },
112 - },
108 );
109 +
110 + stream.allReady.then(() => (isComplete = true));
111 +
112 await jest.runAllTimers();
113 expect(isComplete).toBe(false);
114 // Resolve the loading.
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+19 -5
@@ -32,23 +32,36 @@ type Options = {|
32 bootstrapModules?: Array<string>,
33 progressiveChunkSize?: number,
34 signal?: AbortSignal,
35 - onCompleteAll?: () => void,
35 onError?: (error: mixed) => void,
36 |};
37
38 +// TODO: Move to sub-classing ReadableStream.
39 +type ReactDOMServerReadableStream = ReadableStream & {
40 + allReady: Promise<void>,
41 +};
42 +
43 function renderToReadableStream(
44 children: ReactNodeList,
45 options?: Options,
42 -): Promise<ReadableStream> {
46 +): Promise<ReactDOMServerReadableStream> {
47 return new Promise((resolve, reject) => {
48 + let onFatalError;
49 + let onCompleteAll;
50 + const allReady = new Promise((res, rej) => {
51 + onCompleteAll = res;
52 + onFatalError = rej;
53 + });
54 +
55 function onCompleteShell() {
45 - const stream = new ReadableStream({
56 + const stream: ReactDOMServerReadableStream = (new ReadableStream({
57 type: 'bytes',
58 pull(controller) {
59 startFlowing(request, controller);
60 },
61 cancel(reason) {},
51 - });
62 + }): any);
63 + // TODO: Move to sub-classing ReadableStream.
64 + stream.allReady = allReady;
65 resolve(stream);
66 }
67 function onErrorShell(error: mixed) {
@@ -66,9 +79,10 @@ function renderToReadableStream(
79 createRootFormatContext(options ? options.namespaceURI : undefined),
80 options ? options.progressiveChunkSize : undefined,
81 options ? options.onError : undefined,
69 - options ? options.onCompleteAll : undefined,
82 + onCompleteAll,
83 onCompleteShell,
84 onErrorShell,
85 + onFatalError,
86 );
87 if (options && options.signal) {
88 const signal = options.signal;
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+1
@@ -65,6 +65,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
65 options ? options.onCompleteAll : undefined,
66 options ? options.onCompleteShell : undefined,
67 options ? options.onErrorShell : undefined,
68 + undefined,
69 );
70 }
71
packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js
+2
@@ -67,6 +67,8 @@ function renderToStringImpl(
67 onError,
68 undefined,
69 onCompleteShell,
70 + undefined,
71 + undefined,
72 );
73 startWork(request);
74 // If anything suspended and is still pending, we'll abort it before writing.
packages/react-dom/src/server/ReactDOMLegacyServerNode.js
+1
@@ -83,6 +83,7 @@ function renderToNodeStreamImpl(
83 onError,
84 onCompleteAll,
85 undefined,
86 + undefined,
87 );
88 destination.request = request;
89 startWork(request);
packages/react-server/src/ReactFizzServer.js
+3
@@ -204,6 +204,7 @@ export opaque type Request = {
204 // onErrorShell is called when the shell didn't complete. That means you probably want to
205 // emit a different response to the stream instead.
206 onErrorShell: (error: mixed) => void,
207 + onFatalError: (error: mixed) => void,
208 };
209
210 // This is a default heuristic for how to split up the HTML content into progressive
@@ -238,6 +239,7 @@ export function createRequest(
239 onCompleteAll: void | (() => void),
240 onCompleteShell: void | (() => void),
241 onErrorShell: void | ((error: mixed) => void),
242 + onFatalError: void | ((error: mixed) => void),
243 ): Request {
244 const pingedTasks = [];
245 const abortSet: Set<Task> = new Set();
@@ -263,6 +265,7 @@ export function createRequest(
265 onCompleteAll: onCompleteAll === undefined ? noop : onCompleteAll,
266 onCompleteShell: onCompleteShell === undefined ? noop : onCompleteShell,
267 onErrorShell: onErrorShell === undefined ? noop : onErrorShell,
268 + onFatalError: onFatalError === undefined ? noop : onFatalError,
269 };
270 // This segment represents the root fallback.
271 const rootSegment = createPendingSegment(request, 0, null, rootFormatContext);