@samitouri / QOS-React / commits / c4b433f8cb

[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 }