[Flight] Allow aborting during render (#29764)
Stacked on #29491 Previously if you aborted during a render the currently rendering task would itself be aborted which will cause the entire model to be replaced by the aborted error rather than just the slot currently being rendered. This change updates the abort logic to mark currently rendering tasks as aborted but allowing the current render to emit a partially serialized model with an error reference in place of the current model. The intent is to support aborting from rendering synchronously, in microtasks (after an await or in a .then) and in lazy initializers. We don't specifically support aborting from things like proxies that might be triggered during serialization of props
Josh Story committed
Jun 6, 2024 at 14:41 UTC
c4b433f8cb31d6f73d4a800fcf11ed55c8689daf
3 files changed
+675
-29
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+573
-14
@@ -36,6 +36,7 @@ let ErrorBoundary;
36
let JSDOM;
37
let ReactServerScheduler;
38
let reactServerAct;
39
+let assertConsoleErrorDev;
40
41
describe('ReactFlightDOM', () => {
42
beforeEach(() => {
@@ -70,6 +71,8 @@ describe('ReactFlightDOM', () => {
71
__unmockReact();
72
jest.resetModules();
73
act = require('internal-test-utils').act;
74
+ assertConsoleErrorDev =
75
+ require('internal-test-utils').assertConsoleErrorDev;
76
Stream = require('stream');
77
React = require('react');
78
use = React.use;
@@ -107,6 +110,38 @@ describe('ReactFlightDOM', () => {
110
return maybePromise;
111
}
112
113
+ async function readInto(
114
+ container: Document | HTMLElement,
115
+ stream: ReadableStream,
116
+ ) {
117
+ const reader = stream.getReader();
118
+ const decoder = new TextDecoder();
119
+ let content = '';
120
+ while (true) {
121
+ const {done, value} = await reader.read();
122
+ if (done) {
123
+ content += decoder.decode();
124
+ break;
125
+ }
126
+ content += decoder.decode(value, {stream: true});
127
+ }
128
+ if (container.nodeType === 9 /* DOCUMENT */) {
129
+ const doc = new JSDOM(content).window.document;
130
+ container.documentElement.innerHTML = doc.documentElement.innerHTML;
131
+ while (container.documentElement.attributes.length > 0) {
132
+ container.documentElement.removeAttribute(
133
+ container.documentElement.attributes[0].name,
134
+ );
135
+ }
136
+ const attrs = doc.documentElement.attributes;
137
+ for (let i = 0; i < attrs.length; i++) {
138
+ container.documentElement.setAttribute(attrs[i].name, attrs[i].value);
139
+ }
140
+ } else {
141
+ container.innerHTML = content;
142
+ }
143
+ }
144
+
145
function getTestStream() {
146
const writable = new Stream.PassThrough();
147
const readable = new ReadableStream({
@@ -1633,20 +1668,8 @@ describe('ReactFlightDOM', () => {
1668
ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1669
});
1670
1636
- const decoder = new TextDecoder();
1637
- const reader = fizzReadable.getReader();
1638
- let content = '';
1639
- while (true) {
1640
- const {done, value} = await reader.read();
1641
- if (done) {
1642
- content += decoder.decode();
1643
- break;
1644
- }
1645
- content += decoder.decode(value, {stream: true});
1646
- }
1647
-
1648
- const doc = new JSDOM(content).window.document;
1649
- expect(getMeaningfulChildren(doc)).toEqual(
1671
+ await readInto(document, fizzReadable);
1672
+ expect(getMeaningfulChildren(document)).toEqual(
1673
<html>
1674
<head>
1675
<link rel="dns-prefetch" href="d before" />
@@ -1912,4 +1935,540 @@ describe('ReactFlightDOM', () => {
1935
});
1936
expect(container.innerHTML).toBe('Hello World');
1937
});
1938
+
1939
+ it('can abort synchronously during render', async () => {
1940
+ function Sibling() {
1941
+ return <p>sibling</p>;
1942
+ }
1943
+
1944
+ function App() {
1945
+ return (
1946
+ <div>
1947
+ <Suspense fallback={<p>loading 1...</p>}>
1948
+ <ComponentThatAborts />
1949
+ <Sibling />
1950
+ </Suspense>
1951
+ <Suspense fallback={<p>loading 2...</p>}>
1952
+ <Sibling />
1953
+ </Suspense>
1954
+ <div>
1955
+ <Suspense fallback={<p>loading 3...</p>}>
1956
+ <div>
1957
+ <Sibling />
1958
+ </div>
1959
+ </Suspense>
1960
+ </div>
1961
+ </div>
1962
+ );
1963
+ }
1964
+
1965
+ const abortRef = {current: null};
1966
+ function ComponentThatAborts() {
1967
+ abortRef.current();
1968
+ return <p>hello world</p>;
1969
+ }
1970
+
1971
+ const {writable: flightWritable, readable: flightReadable} =
1972
+ getTestStream();
1973
+
1974
+ await serverAct(() => {
1975
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
1976
+ <App />,
1977
+ webpackMap,
1978
+ );
1979
+ abortRef.current = abort;
1980
+ pipe(flightWritable);
1981
+ });
1982
+ assertConsoleErrorDev([
1983
+ 'The render was aborted by the server without a reason.',
1984
+ ]);
1985
+
1986
+ const response =
1987
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
1988
+
1989
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1990
+
1991
+ function ClientApp() {
1992
+ return use(response);
1993
+ }
1994
+
1995
+ const shellErrors = [];
1996
+ await serverAct(async () => {
1997
+ ReactDOMFizzServer.renderToPipeableStream(
1998
+ React.createElement(ClientApp),
1999
+ {
2000
+ onShellError(error) {
2001
+ shellErrors.push(error.message);
2002
+ },
2003
+ },
2004
+ ).pipe(fizzWritable);
2005
+ });
2006
+ assertConsoleErrorDev([
2007
+ 'The render was aborted by the server without a reason.',
2008
+ 'The render was aborted by the server without a reason.',
2009
+ 'The render was aborted by the server without a reason.',
2010
+ ]);
2011
+
2012
+ expect(shellErrors).toEqual([]);
2013
+
2014
+ const container = document.createElement('div');
2015
+ await readInto(container, fizzReadable);
2016
+ expect(getMeaningfulChildren(container)).toEqual(
2017
+ <div>
2018
+ <p>loading 1...</p>
2019
+ <p>loading 2...</p>
2020
+ <div>
2021
+ <p>loading 3...</p>
2022
+ </div>
2023
+ </div>,
2024
+ );
2025
+ });
2026
+
2027
+ it('can abort during render in an async tick', async () => {
2028
+ async function Sibling() {
2029
+ return <p>sibling</p>;
2030
+ }
2031
+
2032
+ function App() {
2033
+ return (
2034
+ <div>
2035
+ <Suspense fallback={<p>loading 1...</p>}>
2036
+ <ComponentThatAborts />
2037
+ <Sibling />
2038
+ </Suspense>
2039
+ <Suspense fallback={<p>loading 2...</p>}>
2040
+ <Sibling />
2041
+ </Suspense>
2042
+ <div>
2043
+ <Suspense fallback={<p>loading 3...</p>}>
2044
+ <div>
2045
+ <Sibling />
2046
+ </div>
2047
+ </Suspense>
2048
+ </div>
2049
+ </div>
2050
+ );
2051
+ }
2052
+
2053
+ const abortRef = {current: null};
2054
+ async function ComponentThatAborts() {
2055
+ await 1;
2056
+ abortRef.current();
2057
+ return <p>hello world</p>;
2058
+ }
2059
+
2060
+ const {writable: flightWritable, readable: flightReadable} =
2061
+ getTestStream();
2062
+
2063
+ await serverAct(() => {
2064
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2065
+ <App />,
2066
+ webpackMap,
2067
+ );
2068
+ abortRef.current = abort;
2069
+ pipe(flightWritable);
2070
+ });
2071
+
2072
+ assertConsoleErrorDev([
2073
+ 'The render was aborted by the server without a reason.',
2074
+ ]);
2075
+
2076
+ const response =
2077
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2078
+
2079
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2080
+
2081
+ function ClientApp() {
2082
+ return use(response);
2083
+ }
2084
+
2085
+ const shellErrors = [];
2086
+ await serverAct(async () => {
2087
+ ReactDOMFizzServer.renderToPipeableStream(
2088
+ React.createElement(ClientApp),
2089
+ {
2090
+ onShellError(error) {
2091
+ shellErrors.push(error.message);
2092
+ },
2093
+ },
2094
+ ).pipe(fizzWritable);
2095
+ });
2096
+
2097
+ assertConsoleErrorDev([
2098
+ 'The render was aborted by the server without a reason.',
2099
+ 'The render was aborted by the server without a reason.',
2100
+ 'The render was aborted by the server without a reason.',
2101
+ ]);
2102
+
2103
+ expect(shellErrors).toEqual([]);
2104
+
2105
+ const container = document.createElement('div');
2106
+ await readInto(container, fizzReadable);
2107
+ expect(getMeaningfulChildren(container)).toEqual(
2108
+ <div>
2109
+ <p>loading 1...</p>
2110
+ <p>loading 2...</p>
2111
+ <div>
2112
+ <p>loading 3...</p>
2113
+ </div>
2114
+ </div>,
2115
+ );
2116
+ });
2117
+
2118
+ it('can abort during render in a lazy initializer for a component', async () => {
2119
+ function Sibling() {
2120
+ return <p>sibling</p>;
2121
+ }
2122
+
2123
+ function App() {
2124
+ return (
2125
+ <div>
2126
+ <Suspense fallback={<p>loading 1...</p>}>
2127
+ <LazyAbort />
2128
+ </Suspense>
2129
+ <Suspense fallback={<p>loading 2...</p>}>
2130
+ <Sibling />
2131
+ </Suspense>
2132
+ <div>
2133
+ <Suspense fallback={<p>loading 3...</p>}>
2134
+ <div>
2135
+ <Sibling />
2136
+ </div>
2137
+ </Suspense>
2138
+ </div>
2139
+ </div>
2140
+ );
2141
+ }
2142
+
2143
+ const abortRef = {current: null};
2144
+ const LazyAbort = React.lazy(() => {
2145
+ abortRef.current();
2146
+ return {
2147
+ then(cb) {
2148
+ cb({default: 'div'});
2149
+ },
2150
+ };
2151
+ });
2152
+
2153
+ const {writable: flightWritable, readable: flightReadable} =
2154
+ getTestStream();
2155
+
2156
+ await serverAct(() => {
2157
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2158
+ <App />,
2159
+ webpackMap,
2160
+ );
2161
+ abortRef.current = abort;
2162
+ pipe(flightWritable);
2163
+ });
2164
+ assertConsoleErrorDev([
2165
+ 'The render was aborted by the server without a reason.',
2166
+ ]);
2167
+
2168
+ const response =
2169
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2170
+
2171
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2172
+
2173
+ function ClientApp() {
2174
+ return use(response);
2175
+ }
2176
+
2177
+ const shellErrors = [];
2178
+ await serverAct(async () => {
2179
+ ReactDOMFizzServer.renderToPipeableStream(
2180
+ React.createElement(ClientApp),
2181
+ {
2182
+ onShellError(error) {
2183
+ shellErrors.push(error.message);
2184
+ },
2185
+ },
2186
+ ).pipe(fizzWritable);
2187
+ });
2188
+ assertConsoleErrorDev([
2189
+ 'The render was aborted by the server without a reason.',
2190
+ 'The render was aborted by the server without a reason.',
2191
+ 'The render was aborted by the server without a reason.',
2192
+ ]);
2193
+
2194
+ expect(shellErrors).toEqual([]);
2195
+
2196
+ const container = document.createElement('div');
2197
+ await readInto(container, fizzReadable);
2198
+ expect(getMeaningfulChildren(container)).toEqual(
2199
+ <div>
2200
+ <p>loading 1...</p>
2201
+ <p>loading 2...</p>
2202
+ <div>
2203
+ <p>loading 3...</p>
2204
+ </div>
2205
+ </div>,
2206
+ );
2207
+ });
2208
+
2209
+ it('can abort during render in a lazy initializer for an element', async () => {
2210
+ function Sibling() {
2211
+ return <p>sibling</p>;
2212
+ }
2213
+
2214
+ function App() {
2215
+ return (
2216
+ <div>
2217
+ <Suspense fallback={<p>loading 1...</p>}>{lazyAbort}</Suspense>
2218
+ <Suspense fallback={<p>loading 2...</p>}>
2219
+ <Sibling />
2220
+ </Suspense>
2221
+ <div>
2222
+ <Suspense fallback={<p>loading 3...</p>}>
2223
+ <div>
2224
+ <Sibling />
2225
+ </div>
2226
+ </Suspense>
2227
+ </div>
2228
+ </div>
2229
+ );
2230
+ }
2231
+
2232
+ const abortRef = {current: null};
2233
+ const lazyAbort = React.lazy(() => {
2234
+ abortRef.current();
2235
+ return {
2236
+ then(cb) {
2237
+ cb({default: 'hello world'});
2238
+ },
2239
+ };
2240
+ });
2241
+
2242
+ const {writable: flightWritable, readable: flightReadable} =
2243
+ getTestStream();
2244
+
2245
+ await serverAct(() => {
2246
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2247
+ <App />,
2248
+ webpackMap,
2249
+ );
2250
+ abortRef.current = abort;
2251
+ pipe(flightWritable);
2252
+ });
2253
+ assertConsoleErrorDev([
2254
+ 'The render was aborted by the server without a reason.',
2255
+ ]);
2256
+
2257
+ const response =
2258
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2259
+
2260
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2261
+
2262
+ function ClientApp() {
2263
+ return use(response);
2264
+ }
2265
+
2266
+ const shellErrors = [];
2267
+ await serverAct(async () => {
2268
+ ReactDOMFizzServer.renderToPipeableStream(
2269
+ React.createElement(ClientApp),
2270
+ {
2271
+ onShellError(error) {
2272
+ shellErrors.push(error.message);
2273
+ },
2274
+ },
2275
+ ).pipe(fizzWritable);
2276
+ });
2277
+ assertConsoleErrorDev([
2278
+ 'The render was aborted by the server without a reason.',
2279
+ 'The render was aborted by the server without a reason.',
2280
+ 'The render was aborted by the server without a reason.',
2281
+ ]);
2282
+
2283
+ expect(shellErrors).toEqual([]);
2284
+
2285
+ const container = document.createElement('div');
2286
+ await readInto(container, fizzReadable);
2287
+ expect(getMeaningfulChildren(container)).toEqual(
2288
+ <div>
2289
+ <p>loading 1...</p>
2290
+ <p>loading 2...</p>
2291
+ <div>
2292
+ <p>loading 3...</p>
2293
+ </div>
2294
+ </div>,
2295
+ );
2296
+ });
2297
+
2298
+ it('can abort during a synchronous thenable resolution', async () => {
2299
+ function Sibling() {
2300
+ return <p>sibling</p>;
2301
+ }
2302
+
2303
+ function App() {
2304
+ return (
2305
+ <div>
2306
+ <Suspense fallback={<p>loading 1...</p>}>{thenable}</Suspense>
2307
+ <Suspense fallback={<p>loading 2...</p>}>
2308
+ <Sibling />
2309
+ </Suspense>
2310
+ <div>
2311
+ <Suspense fallback={<p>loading 3...</p>}>
2312
+ <div>
2313
+ <Sibling />
2314
+ </div>
2315
+ </Suspense>
2316
+ </div>
2317
+ </div>
2318
+ );
2319
+ }
2320
+
2321
+ const abortRef = {current: null};
2322
+ const thenable = {
2323
+ then(cb) {
2324
+ abortRef.current();
2325
+ cb(thenable.value);
2326
+ },
2327
+ };
2328
+
2329
+ const {writable: flightWritable, readable: flightReadable} =
2330
+ getTestStream();
2331
+
2332
+ await serverAct(() => {
2333
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2334
+ <App />,
2335
+ webpackMap,
2336
+ );
2337
+ abortRef.current = abort;
2338
+ pipe(flightWritable);
2339
+ });
2340
+
2341
+ assertConsoleErrorDev([
2342
+ 'The render was aborted by the server without a reason.',
2343
+ ]);
2344
+
2345
+ const response =
2346
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2347
+
2348
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2349
+
2350
+ function ClientApp() {
2351
+ return use(response);
2352
+ }
2353
+
2354
+ const shellErrors = [];
2355
+ await serverAct(async () => {
2356
+ ReactDOMFizzServer.renderToPipeableStream(
2357
+ React.createElement(ClientApp),
2358
+ {
2359
+ onShellError(error) {
2360
+ shellErrors.push(error.message);
2361
+ },
2362
+ },
2363
+ ).pipe(fizzWritable);
2364
+ });
2365
+ assertConsoleErrorDev([
2366
+ 'The render was aborted by the server without a reason.',
2367
+ 'The render was aborted by the server without a reason.',
2368
+ 'The render was aborted by the server without a reason.',
2369
+ ]);
2370
+
2371
+ expect(shellErrors).toEqual([]);
2372
+
2373
+ const container = document.createElement('div');
2374
+ await readInto(container, fizzReadable);
2375
+ expect(getMeaningfulChildren(container)).toEqual(
2376
+ <div>
2377
+ <p>loading 1...</p>
2378
+ <p>loading 2...</p>
2379
+ <div>
2380
+ <p>loading 3...</p>
2381
+ </div>
2382
+ </div>,
2383
+ );
2384
+ });
2385
+
2386
+ it('wont serialize thenables that were not already settled by the time an abort happens', async () => {
2387
+ function App() {
2388
+ return (
2389
+ <div>
2390
+ <Suspense fallback={<p>loading 1...</p>}>
2391
+ <ComponentThatAborts />
2392
+ </Suspense>
2393
+ <Suspense fallback={<p>loading 2...</p>}>{thenable1}</Suspense>
2394
+ <div>
2395
+ <Suspense fallback={<p>loading 3...</p>}>{thenable2}</Suspense>
2396
+ </div>
2397
+ </div>
2398
+ );
2399
+ }
2400
+
2401
+ const abortRef = {current: null};
2402
+ const thenable1 = {
2403
+ then(cb) {
2404
+ cb('hello world');
2405
+ },
2406
+ };
2407
+
2408
+ const thenable2 = {
2409
+ then(cb) {
2410
+ cb('hello world');
2411
+ },
2412
+ status: 'fulfilled',
2413
+ value: 'hello world',
2414
+ };
2415
+
2416
+ function ComponentThatAborts() {
2417
+ abortRef.current();
2418
+ return thenable1;
2419
+ }
2420
+
2421
+ const {writable: flightWritable, readable: flightReadable} =
2422
+ getTestStream();
2423
+
2424
+ await serverAct(() => {
2425
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2426
+ <App />,
2427
+ webpackMap,
2428
+ );
2429
+ abortRef.current = abort;
2430
+ pipe(flightWritable);
2431
+ });
2432
+
2433
+ assertConsoleErrorDev([
2434
+ 'The render was aborted by the server without a reason.',
2435
+ ]);
2436
+
2437
+ const response =
2438
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2439
+
2440
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2441
+
2442
+ function ClientApp() {
2443
+ return use(response);
2444
+ }
2445
+
2446
+ const shellErrors = [];
2447
+ await serverAct(async () => {
2448
+ ReactDOMFizzServer.renderToPipeableStream(
2449
+ React.createElement(ClientApp),
2450
+ {
2451
+ onShellError(error) {
2452
+ shellErrors.push(error.message);
2453
+ },
2454
+ },
2455
+ ).pipe(fizzWritable);
2456
+ });
2457
+ assertConsoleErrorDev([
2458
+ 'The render was aborted by the server without a reason.',
2459
+ 'The render was aborted by the server without a reason.',
2460
+ ]);
2461
+
2462
+ expect(shellErrors).toEqual([]);
2463
+
2464
+ const container = document.createElement('div');
2465
+ await readInto(container, fizzReadable);
2466
+ expect(getMeaningfulChildren(container)).toEqual(
2467
+ <div>
2468
+ <p>loading 1...</p>
2469
+ <p>loading 2...</p>
2470
+ <div>hello world</div>
2471
+ </div>,
2472
+ );
2473
+ });
2474
});
packages/react-server/src/ReactFlightServer.js
+100
-14
@@ -381,10 +381,11 @@ const PENDING = 0;
381
const COMPLETED = 1;
382
const ABORTED = 3;
383
const ERRORED = 4;
384
+const RENDERING = 5;
385
386
type Task = {
387
id: number,
387
- status: 0 | 1 | 3 | 4,
388
+ status: 0 | 1 | 3 | 4 | 5,
389
model: ReactClientValue,
390
ping: () => void,
391
toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
@@ -396,7 +397,7 @@ type Task = {
397
interface Reference {}
398
399
export type Request = {
399
- status: 0 | 1 | 2,
400
+ status: 0 | 1 | 2 | 3,
401
flushScheduled: boolean,
402
fatalError: mixed,
403
destination: null | Destination,
@@ -427,6 +428,8 @@ export type Request = {
428
didWarnForKey: null | WeakSet<ReactComponentInfo>,
429
};
430
431
+const AbortSigil = {};
432
+
433
const {
434
TaintRegistryObjects,
435
TaintRegistryValues,
@@ -466,8 +469,9 @@ function defaultPostponeHandler(reason: string) {
469
}
470
471
const OPEN = 0;
469
-const CLOSING = 1;
470
-const CLOSED = 2;
472
+const ABORTING = 1;
473
+const CLOSING = 2;
474
+const CLOSED = 3;
475
476
export function createRequest(
477
model: ReactClientValue,
@@ -556,7 +560,6 @@ function serializeThenable(
560
task.implicitSlot,
561
request.abortableTasks,
562
);
559
-
563
if (__DEV__) {
564
// If this came from Flight, forward any debug info into this new row.
565
const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
@@ -590,6 +593,15 @@ function serializeThenable(
593
return newTask.id;
594
}
595
default: {
596
+ if (request.status === ABORTING) {
597
+ // We can no longer accept any resolved values
598
+ newTask.status = ABORTED;
599
+ const errorId: number = (request.fatalError: any);
600
+ const model = stringify(serializeByValueID(errorId));
601
+ emitModelChunk(request, newTask.id, model);
602
+ request.abortableTasks.delete(newTask);
603
+ return newTask.id;
604
+ }
605
if (typeof thenable.status === 'string') {
606
// Only instrument the thenable if the status if not defined. If
607
// it's defined, but an unknown value, assume it's been instrumented by
@@ -1046,6 +1058,14 @@ function renderFunctionComponent<Props>(
1058
const secondArg = undefined;
1059
result = Component(props, secondArg);
1060
}
1061
+
1062
+ if (request.status === ABORTING) {
1063
+ // If we aborted during rendering we should interrupt the render but
1064
+ // we don't need to provide an error because the renderer will encode
1065
+ // the abort error as the reason.
1066
+ throw AbortSigil;
1067
+ }
1068
+
1069
if (
1070
typeof result === 'object' &&
1071
result !== null &&
@@ -1523,6 +1543,12 @@ function renderElement(
1543
const init = type._init;
1544
wrappedType = init(payload);
1545
}
1546
+ if (request.status === ABORTING) {
1547
+ // lazy initializers are user code and could abort during render
1548
+ // we don't wan to return any value resolved from the lazy initializer
1549
+ // if it aborts so we interrupt rendering here
1550
+ throw AbortSigil;
1551
+ }
1552
return renderElement(
1553
request,
1554
task,
@@ -1942,6 +1968,15 @@ function renderModel(
1968
try {
1969
return renderModelDestructive(request, task, parent, key, value);
1970
} catch (thrownValue) {
1971
+ // If the suspended/errored value was an element or lazy it can be reduced
1972
+ // to a lazy reference, so that it doesn't error the parent.
1973
+ const model = task.model;
1974
+ const wasReactNode =
1975
+ typeof model === 'object' &&
1976
+ model !== null &&
1977
+ ((model: any).$$typeof === REACT_ELEMENT_TYPE ||
1978
+ (model: any).$$typeof === REACT_LAZY_TYPE);
1979
+
1980
const x =
1981
thrownValue === SuspenseException
1982
? // This is a special type of exception used for Suspense. For historical
@@ -1951,17 +1986,18 @@ function renderModel(
1986
// later, once we deprecate the old API in favor of `use`.
1987
getSuspendedThenable()
1988
: thrownValue;
1954
- // If the suspended/errored value was an element or lazy it can be reduced
1955
- // to a lazy reference, so that it doesn't error the parent.
1956
- const model = task.model;
1957
- const wasReactNode =
1958
- typeof model === 'object' &&
1959
- model !== null &&
1960
- ((model: any).$$typeof === REACT_ELEMENT_TYPE ||
1961
- (model: any).$$typeof === REACT_LAZY_TYPE);
1989
+
1990
if (typeof x === 'object' && x !== null) {
1991
// $FlowFixMe[method-unbinding]
1992
if (typeof x.then === 'function') {
1993
+ if (request.status === ABORTING) {
1994
+ task.status = ABORTED;
1995
+ const errorId: number = (request.fatalError: any);
1996
+ if (wasReactNode) {
1997
+ return serializeLazyID(errorId);
1998
+ }
1999
+ return serializeByValueID(errorId);
2000
+ }
2001
// Something suspended, we'll need to create a new task and resolve it later.
2002
const newTask = createTask(
2003
request,
@@ -2004,6 +2040,15 @@ function renderModel(
2040
}
2041
}
2042
2043
+ if (thrownValue === AbortSigil) {
2044
+ task.status = ABORTED;
2045
+ const errorId: number = (request.fatalError: any);
2046
+ if (wasReactNode) {
2047
+ return serializeLazyID(errorId);
2048
+ }
2049
+ return serializeByValueID(errorId);
2050
+ }
2051
+
2052
// Restore the context. We assume that this will be restored by the inner
2053
// functions in case nothing throws so we don't use "finally" here.
2054
task.keyPath = prevKeyPath;
@@ -2147,6 +2192,12 @@ function renderModelDestructive(
2192
const init = lazy._init;
2193
resolvedModel = init(payload);
2194
}
2195
+ if (request.status === ABORTING) {
2196
+ // lazy initializers are user code and could abort during render
2197
+ // we don't wan to return any value resolved from the lazy initializer
2198
+ // if it aborts so we interrupt rendering here
2199
+ throw AbortSigil;
2200
+ }
2201
if (__DEV__) {
2202
const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
2203
if (debugInfo) {
@@ -3262,6 +3313,7 @@ function retryTask(request: Request, task: Task): void {
3313
}
3314
3315
const prevDebugID = debugID;
3316
+ task.status = RENDERING;
3317
3318
try {
3319
// Track the root so we know that we have to emit this object even though it
@@ -3328,10 +3380,19 @@ function retryTask(request: Request, task: Task): void {
3380
if (typeof x === 'object' && x !== null) {
3381
// $FlowFixMe[method-unbinding]
3382
if (typeof x.then === 'function') {
3383
+ if (request.status === ABORTING) {
3384
+ request.abortableTasks.delete(task);
3385
+ task.status = ABORTED;
3386
+ const errorId: number = (request.fatalError: any);
3387
+ const model = stringify(serializeByValueID(errorId));
3388
+ emitModelChunk(request, task.id, model);
3389
+ return;
3390
+ }
3391
// Something suspended again, let's pick it back up later.
3392
+ task.status = PENDING;
3393
+ task.thenableState = getThenableStateAfterSuspending();
3394
const ping = task.ping;
3395
x.then(ping, ping);
3334
- task.thenableState = getThenableStateAfterSuspending();
3396
return;
3397
} else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
3398
request.abortableTasks.delete(task);
@@ -3342,6 +3403,16 @@ function retryTask(request: Request, task: Task): void {
3403
return;
3404
}
3405
}
3406
+
3407
+ if (x === AbortSigil) {
3408
+ request.abortableTasks.delete(task);
3409
+ task.status = ABORTED;
3410
+ const errorId: number = (request.fatalError: any);
3411
+ const model = stringify(serializeByValueID(errorId));
3412
+ emitModelChunk(request, task.id, model);
3413
+ return;
3414
+ }
3415
+
3416
request.abortableTasks.delete(task);
3417
task.status = ERRORED;
3418
const digest = logRecoverableError(request, x);
@@ -3399,6 +3470,10 @@ function performWork(request: Request): void {
3470
}
3471
3472
function abortTask(task: Task, request: Request, errorId: number): void {
3473
+ if (task.status === RENDERING) {
3474
+ // This task will be aborted by the render
3475
+ return;
3476
+ }
3477
task.status = ABORTED;
3478
// Instead of emitting an error per task.id, we emit a model that only
3479
// has a single value referencing the error.
@@ -3484,6 +3559,7 @@ function flushCompletedChunks(
3559
if (enableTaint) {
3560
cleanupTaintQueue(request);
3561
}
3562
+ request.status = CLOSED;
3563
close(destination);
3564
request.destination = null;
3565
}
@@ -3547,12 +3623,14 @@ export function stopFlowing(request: Request): void {
3623
// This is called to early terminate a request. It creates an error at all pending tasks.
3624
export function abort(request: Request, reason: mixed): void {
3625
try {
3626
+ request.status = ABORTING;
3627
const abortableTasks = request.abortableTasks;
3628
// We have tasks to abort. We'll emit one error row and then emit a reference
3629
// to that row from every row that's still remaining.
3630
if (abortableTasks.size > 0) {
3631
request.pendingChunks++;
3632
const errorId = request.nextChunkId++;
3633
+ request.fatalError = errorId;
3634
if (
3635
enablePostpone &&
3636
typeof reason === 'object' &&
@@ -3568,6 +3646,10 @@ export function abort(request: Request, reason: mixed): void {
3646
? new Error(
3647
'The render was aborted by the server without a reason.',
3648
)
3649
+ : typeof reason === 'object' &&
3650
+ reason !== null &&
3651
+ typeof reason.then === 'function'
3652
+ ? new Error('The render was aborted by the server with a promise.')
3653
: reason;
3654
const digest = logRecoverableError(request, error);
3655
emitErrorChunk(request, errorId, digest, error);
@@ -3594,6 +3676,10 @@ export function abort(request: Request, reason: mixed): void {
3676
? new Error(
3677
'The render was aborted by the server without a reason.',
3678
)
3679
+ : typeof reason === 'object' &&
3680
+ reason !== null &&
3681
+ typeof reason.then === 'function'
3682
+ ? new Error('The render was aborted by the server with a promise.')
3683
: reason;
3684
}
3685
abortListeners.forEach(callback => callback(error));
scripts/error-codes/codes.json
+2
-1
@@ -514,5 +514,6 @@
514
"526": "Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server.",
515
"527": "Incompatible React versions: The \"react\" and \"react-dom\" packages must have the exact same version. Instead got:\n - react: %s\n - react-dom: %s\nLearn more: https://react.dev/warnings/version-mismatch",
516
"528": "Expected <link> not to update to be updated to a stylesheet with precedence. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different <link> components render in the same slot or share the same key.%s",
517
- "529": "Expected stylesheet with precedence to not be updated to a different kind of <link>. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different <link> components render in the same slot or share the same key.%s"
517
+ "529": "Expected stylesheet with precedence to not be updated to a different kind of <link>. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different <link> components render in the same slot or share the same key.%s",
518
+ "530": "The render was aborted by the server with a promise."
519
}