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);