Skip to content

Commit 14c2be8

Browse files
authored
Rename Node SSR Callbacks to onShellReady/onAllReady and Other Fixes (#24030)
* I forgot to call onFatalError I can't figure out how to write a test for this because it only happens when there is a bug in React itself which would then be fixed if we found it. We're also covered by the protection of ReadableStream which doesn't leak other errors to us. * Abort requests if the reader cancels No need to continue computing at this point. * Abort requests if node streams get destroyed This is if the downstream cancels is for example. * Rename Node APIs for Parity with allReady The "Complete" terminology is a little misleading because not everything has been written yet. It's just "Ready" to be written now. onShellReady onShellError onAllReady * 'close' should be enough
1 parent cb1e7b1 commit 14c2be8

12 files changed

+151
-58
lines changed

fixtures/ssr/server/render.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ export default function render(url, res) {
2222
let didError = false;
2323
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
2424
bootstrapScripts: [assets['main.js']],
25-
onCompleteShell() {
25+
onShellReady() {
2626
// If something errored before we started streaming, we set the error code appropriately.
2727
res.statusCode = didError ? 500 : 200;
2828
res.setHeader('Content-type', 'text/html');
2929
pipe(res);
3030
},
31-
onErrorShell(x) {
31+
onShellError(x) {
3232
// Something errored before we could complete the shell so we emit an alternative shell.
3333
res.statusCode = 500;
3434
res.send('<!doctype><p>Error</p>');

fixtures/ssr2/server/render.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ module.exports = function render(url, res) {
4949
res.setHeader('Content-type', 'text/html');
5050
pipe(res);
5151
},
52-
onErrorShell(x) {
52+
onShellError(x) {
5353
// Something errored before we could complete the shell so we emit an alternative shell.
5454
res.statusCode = 500;
5555
res.send('<!doctype><p>Error</p>');

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,7 @@ describe('ReactDOMFizzServer', () => {
914914
</Suspense>,
915915
{
916916
identifierPrefix: 'A_',
917-
onCompleteShell() {
917+
onShellReady() {
918918
writableA.write('<div id="container-A">');
919919
pipe(writableA);
920920
writableA.write('</div>');
@@ -933,7 +933,7 @@ describe('ReactDOMFizzServer', () => {
933933
</Suspense>,
934934
{
935935
identifierPrefix: 'B_',
936-
onCompleteShell() {
936+
onShellReady() {
937937
writableB.write('<div id="container-B">');
938938
pipe(writableB);
939939
writableB.write('</div>');
@@ -1168,7 +1168,7 @@ describe('ReactDOMFizzServer', () => {
11681168

11691169
{
11701170
namespaceURI: 'http://www.w3.org/2000/svg',
1171-
onCompleteShell() {
1171+
onShellReady() {
11721172
writable.write('<svg>');
11731173
pipe(writable);
11741174
writable.write('</svg>');

packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js

+39
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,43 @@ describe('ReactDOMFizzServer', () => {
209209
const result = await readResult(stream);
210210
expect(result).toContain('Loading');
211211
});
212+
213+
// @gate experimental
214+
it('should not continue rendering after the reader cancels', async () => {
215+
let hasLoaded = false;
216+
let resolve;
217+
let isComplete = false;
218+
let rendered = false;
219+
const promise = new Promise(r => (resolve = r));
220+
function Wait() {
221+
if (!hasLoaded) {
222+
throw promise;
223+
}
224+
rendered = true;
225+
return 'Done';
226+
}
227+
const stream = await ReactDOMFizzServer.renderToReadableStream(
228+
<div>
229+
<Suspense fallback={<div>Loading</div>}>
230+
<Wait /> />
231+
</Suspense>
232+
</div>,
233+
);
234+
235+
stream.allReady.then(() => (isComplete = true));
236+
237+
expect(rendered).toBe(false);
238+
expect(isComplete).toBe(false);
239+
240+
const reader = stream.getReader();
241+
reader.cancel();
242+
243+
hasLoaded = true;
244+
resolve();
245+
246+
await jest.runAllTimers();
247+
248+
expect(rendered).toBe(false);
249+
expect(isComplete).toBe(true);
250+
});
212251
});

packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js

+51-6
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('ReactDOMFizzServer', () => {
138138
</div>,
139139

140140
{
141-
onCompleteAll() {
141+
onAllReady() {
142142
isCompleteCalls++;
143143
},
144144
},
@@ -179,7 +179,7 @@ describe('ReactDOMFizzServer', () => {
179179
onError(x) {
180180
reportedErrors.push(x);
181181
},
182-
onErrorShell(x) {
182+
onShellError(x) {
183183
reportedShellErrors.push(x);
184184
},
185185
},
@@ -213,7 +213,7 @@ describe('ReactDOMFizzServer', () => {
213213
onError(x) {
214214
reportedErrors.push(x);
215215
},
216-
onErrorShell(x) {
216+
onShellError(x) {
217217
reportedShellErrors.push(x);
218218
},
219219
},
@@ -244,7 +244,7 @@ describe('ReactDOMFizzServer', () => {
244244
onError(x) {
245245
reportedErrors.push(x);
246246
},
247-
onErrorShell(x) {
247+
onShellError(x) {
248248
reportedShellErrors.push(x);
249249
},
250250
},
@@ -298,7 +298,7 @@ describe('ReactDOMFizzServer', () => {
298298
</div>,
299299

300300
{
301-
onCompleteAll() {
301+
onAllReady() {
302302
isCompleteCalls++;
303303
},
304304
},
@@ -333,7 +333,7 @@ describe('ReactDOMFizzServer', () => {
333333
</div>,
334334

335335
{
336-
onCompleteAll() {
336+
onAllReady() {
337337
isCompleteCalls++;
338338
},
339339
},
@@ -537,4 +537,49 @@ describe('ReactDOMFizzServer', () => {
537537
expect(output.result).not.toContain('context never found');
538538
expect(output.result).toContain('OK');
539539
});
540+
541+
// @gate experimental
542+
it('should not continue rendering after the writable ends unexpectedly', async () => {
543+
let hasLoaded = false;
544+
let resolve;
545+
let isComplete = false;
546+
let rendered = false;
547+
const promise = new Promise(r => (resolve = r));
548+
function Wait() {
549+
if (!hasLoaded) {
550+
throw promise;
551+
}
552+
rendered = true;
553+
return 'Done';
554+
}
555+
const {writable, completed} = getTestWritable();
556+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
557+
<div>
558+
<Suspense fallback={<div>Loading</div>}>
559+
<Wait />
560+
</Suspense>
561+
</div>,
562+
{
563+
onAllReady() {
564+
isComplete = true;
565+
},
566+
},
567+
);
568+
pipe(writable);
569+
570+
expect(rendered).toBe(false);
571+
expect(isComplete).toBe(false);
572+
573+
writable.end();
574+
575+
await jest.runAllTimers();
576+
577+
hasLoaded = true;
578+
resolve();
579+
580+
await completed;
581+
582+
expect(rendered).toBe(false);
583+
expect(isComplete).toBe(true);
584+
});
540585
});

packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ module.exports = function(initModules) {
155155
new Promise((resolve, reject) => {
156156
const writable = new DrainWritable();
157157
const s = ReactDOMServer.renderToPipeableStream(reactElement, {
158-
onErrorShell(e) {
158+
onShellError(e) {
159159
reject(e);
160160
},
161161
});

packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,27 @@ function renderToReadableStream(
4646
): Promise<ReactDOMServerReadableStream> {
4747
return new Promise((resolve, reject) => {
4848
let onFatalError;
49-
let onCompleteAll;
49+
let onAllReady;
5050
const allReady = new Promise((res, rej) => {
51-
onCompleteAll = res;
51+
onAllReady = res;
5252
onFatalError = rej;
5353
});
5454

55-
function onCompleteShell() {
55+
function onShellReady() {
5656
const stream: ReactDOMServerReadableStream = (new ReadableStream({
5757
type: 'bytes',
5858
pull(controller) {
5959
startFlowing(request, controller);
6060
},
61-
cancel(reason) {},
61+
cancel(reason) {
62+
abort(request);
63+
},
6264
}): any);
6365
// TODO: Move to sub-classing ReadableStream.
6466
stream.allReady = allReady;
6567
resolve(stream);
6668
}
67-
function onErrorShell(error: mixed) {
69+
function onShellError(error: mixed) {
6870
reject(error);
6971
}
7072
const request = createRequest(
@@ -79,9 +81,9 @@ function renderToReadableStream(
7981
createRootFormatContext(options ? options.namespaceURI : undefined),
8082
options ? options.progressiveChunkSize : undefined,
8183
options ? options.onError : undefined,
82-
onCompleteAll,
83-
onCompleteShell,
84-
onErrorShell,
84+
onAllReady,
85+
onShellReady,
86+
onShellError,
8587
onFatalError,
8688
);
8789
if (options && options.signal) {

packages/react-dom/src/server/ReactDOMFizzServerNode.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ function createDrainHandler(destination, request) {
2828
return () => startFlowing(request, destination);
2929
}
3030

31+
function createAbortHandler(request) {
32+
return () => abort(request);
33+
}
34+
3135
type Options = {|
3236
identifierPrefix?: string,
3337
namespaceURI?: string,
@@ -36,9 +40,9 @@ type Options = {|
3640
bootstrapScripts?: Array<string>,
3741
bootstrapModules?: Array<string>,
3842
progressiveChunkSize?: number,
39-
onCompleteShell?: () => void,
40-
onErrorShell?: () => void,
41-
onCompleteAll?: () => void,
43+
onShellReady?: () => void,
44+
onShellError?: () => void,
45+
onAllReady?: () => void,
4246
onError?: (error: mixed) => void,
4347
|};
4448

@@ -62,9 +66,9 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
6266
createRootFormatContext(options ? options.namespaceURI : undefined),
6367
options ? options.progressiveChunkSize : undefined,
6468
options ? options.onError : undefined,
65-
options ? options.onCompleteAll : undefined,
66-
options ? options.onCompleteShell : undefined,
67-
options ? options.onErrorShell : undefined,
69+
options ? options.onAllReady : undefined,
70+
options ? options.onShellReady : undefined,
71+
options ? options.onShellError : undefined,
6872
undefined,
6973
);
7074
}
@@ -86,6 +90,7 @@ function renderToPipeableStream(
8690
hasStartedFlowing = true;
8791
startFlowing(request, destination);
8892
destination.on('drain', createDrainHandler(destination, request));
93+
destination.on('close', createAbortHandler(request));
8994
return destination;
9095
},
9196
abort() {

packages/react-dom/src/server/ReactDOMLegacyServerBrowser.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function renderToStringImpl(
5353
};
5454

5555
let readyToStream = false;
56-
function onCompleteShell() {
56+
function onShellReady() {
5757
readyToStream = true;
5858
}
5959
const request = createRequest(
@@ -66,7 +66,7 @@ function renderToStringImpl(
6666
Infinity,
6767
onError,
6868
undefined,
69-
onCompleteShell,
69+
onShellReady,
7070
undefined,
7171
undefined,
7272
);

packages/react-dom/src/server/ReactDOMLegacyServerNode.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function renderToNodeStreamImpl(
6868
options: void | ServerOptions,
6969
generateStaticMarkup: boolean,
7070
): Readable {
71-
function onCompleteAll() {
71+
function onAllReady() {
7272
// We wait until everything has loaded before starting to write.
7373
// That way we only end up with fully resolved HTML even if we suspend.
7474
destination.startedFlowing = true;
@@ -81,7 +81,7 @@ function renderToNodeStreamImpl(
8181
createRootFormatContext(),
8282
Infinity,
8383
onError,
84-
onCompleteAll,
84+
onAllReady,
8585
undefined,
8686
undefined,
8787
);

packages/react-noop-renderer/src/ReactNoopServer.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,8 @@ const ReactNoopServer = ReactFizzServer({
251251

252252
type Options = {
253253
progressiveChunkSize?: number,
254-
onCompleteShell?: () => void,
255-
onCompleteAll?: () => void,
254+
onShellReady?: () => void,
255+
onAllReady?: () => void,
256256
onError?: (error: mixed) => void,
257257
};
258258

@@ -272,8 +272,8 @@ function render(children: React$Element<any>, options?: Options): Destination {
272272
null,
273273
options ? options.progressiveChunkSize : undefined,
274274
options ? options.onError : undefined,
275-
options ? options.onCompleteAll : undefined,
276-
options ? options.onCompleteShell : undefined,
275+
options ? options.onAllReady : undefined,
276+
options ? options.onShellReady : undefined,
277277
);
278278
ReactNoopServer.startWork(request);
279279
ReactNoopServer.startFlowing(request, destination);

0 commit comments

Comments
 (0)