@samitouri / QOS-React-1 / commits / 960ed6ea43

[Flight] Aborting with a postpone instance as a reason should postpone remaining holes (#27576)

This lets you abort with postponing semantics.

Sebastian Markbåge committed Oct 24, 2023 at 13:41 UTC 960ed6ea4341b4189a4f6c4edfa6bfe4c712e097
2 files changed +77 -7
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+58
@@ -1309,4 +1309,62 @@ describe('ReactFlightDOMBrowser', () => {
1309 'The render was aborted by the server without a reason.',
1310 ]);
1311 });
1312 +
1313 + // @gate enablePostpone
1314 + it('postpones when abort passes a postpone signal', async () => {
1315 + const infinitePromise = new Promise(() => {});
1316 + function Server() {
1317 + return infinitePromise;
1318 + }
1319 +
1320 + let postponed = null;
1321 + let error = null;
1322 +
1323 + const controller = new AbortController();
1324 + const stream = ReactServerDOMServer.renderToReadableStream(
1325 + <Suspense fallback="Loading...">
1326 + <Server />
1327 + </Suspense>,
1328 + null,
1329 + {
1330 + onError(x) {
1331 + error = x;
1332 + },
1333 + onPostpone(reason) {
1334 + postponed = reason;
1335 + },
1336 + signal: controller.signal,
1337 + },
1338 + );
1339 +
1340 + try {
1341 + React.unstable_postpone('testing postpone');
1342 + } catch (reason) {
1343 + controller.abort(reason);
1344 + }
1345 +
1346 + const response = ReactServerDOMClient.createFromReadableStream(stream);
1347 +
1348 + function Client() {
1349 + return use(response);
1350 + }
1351 +
1352 + const container = document.createElement('div');
1353 + const root = ReactDOMClient.createRoot(container);
1354 + await act(async () => {
1355 + root.render(
1356 + <div>
1357 + Shell: <Client />
1358 + </div>,
1359 + );
1360 + });
1361 + // We should have reserved the shell already. Which means that the Server
1362 + // Component should've been a lazy component.
1363 + expect(container.innerHTML).toContain('Shell:');
1364 + expect(container.innerHTML).toContain('Loading...');
1365 + expect(container.innerHTML).not.toContain('Not shown');
1366 +
1367 + expect(postponed).toBe('testing postpone');
1368 + expect(error).toBe(null);
1369 + });
1370 });
packages/react-server/src/ReactFlightServer.js
+19 -7
@@ -1790,15 +1790,27 @@ export function abort(request: Request, reason: mixed): void {
1790 if (abortableTasks.size > 0) {
1791 // We have tasks to abort. We'll emit one error row and then emit a reference
1792 // to that row from every row that's still remaining.
1793 - const error =
1794 - reason === undefined
1795 - ? new Error('The render was aborted by the server without a reason.')
1796 - : reason;
1797 -
1798 - const digest = logRecoverableError(request, error);
1793 request.pendingChunks++;
1794 const errorId = request.nextChunkId++;
1801 - emitErrorChunk(request, errorId, digest, error);
1795 + if (
1796 + enablePostpone &&
1797 + typeof reason === 'object' &&
1798 + reason !== null &&
1799 + (reason: any).$$typeof === REACT_POSTPONE_TYPE
1800 + ) {
1801 + const postponeInstance: Postpone = (reason: any);
1802 + logPostpone(request, postponeInstance.message);
1803 + emitPostponeChunk(request, errorId, postponeInstance);
1804 + } else {
1805 + const error =
1806 + reason === undefined
1807 + ? new Error(
1808 + 'The render was aborted by the server without a reason.',
1809 + )
1810 + : reason;
1811 + const digest = logRecoverableError(request, error);
1812 + emitErrorChunk(request, errorId, digest, error);
1813 + }
1814 abortableTasks.forEach(task => abortTask(task, request, errorId));
1815 abortableTasks.clear();
1816 }