diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts index eba2fdf5bbdd..2457e0d7b99e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts @@ -97,7 +97,7 @@ export function validateNoSetStateInEffects( case 'CallExpression': { const callee = instr.value.kind === 'MethodCall' - ? instr.value.receiver + ? instr.value.property : instr.value.callee; if (isUseEffectEventType(callee.identifier)) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-namespace.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-namespace.expect.md new file mode 100644 index 000000000000..b7f823e46d4a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-namespace.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint" +import * as React from 'react'; + +function Component() { + const [state, setState] = React.useState(0); + React.useEffect(() => { + setState(s => s + 1); + }); + return state; +} + +``` + +## Code + +```javascript +// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint" +import * as React from "react"; + +function Component() { + const [state, setState] = React.useState(0); + React.useEffect(() => { + setState((s) => s + 1); + }); + return state; +} + +``` + +## Logs + +``` +{"kind":"CompileError","detail":{"options":{"category":"EffectSetState","reason":"Calling setState synchronously within an effect can trigger cascading renders","description":"Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:\n* Update external systems with the latest state from React.\n* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\nCalling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect)","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":7,"column":4,"index":200},"end":{"line":7,"column":12,"index":208},"filename":"invalid-setState-in-useEffect-namespace.ts","identifierName":"setState"},"message":"Avoid calling setState() directly within an effect"}]}},"fnLoc":null} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":100},"end":{"line":10,"column":1,"index":245},"filename":"invalid-setState-in-useEffect-namespace.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":1,"prunedMemoValues":1} +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-namespace.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-namespace.js new file mode 100644 index 000000000000..0748c1206f59 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-namespace.js @@ -0,0 +1,10 @@ +// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint" +import * as React from 'react'; + +function Component() { + const [state, setState] = React.useState(0); + React.useEffect(() => { + setState(s => s + 1); + }); + return state; +} diff --git a/compiler/packages/snap/src/minimize.ts b/compiler/packages/snap/src/minimize.ts index 0cce5ce1bdee..c734c9306d98 100644 --- a/compiler/packages/snap/src/minimize.ts +++ b/compiler/packages/snap/src/minimize.ts @@ -18,7 +18,7 @@ type CompileSuccess = {kind: 'success'}; type CompileParseError = {kind: 'parse_error'; message: string}; type CompileErrors = { kind: 'errors'; - errors: Array<{category: string; reason: string}>; + errors: Array<{category: string; reason: string; description: string | null}>; }; type CompileResult = CompileSuccess | CompileParseError | CompileErrors; @@ -70,7 +70,11 @@ function compileAndGetError( return {kind: 'success'}; } catch (e: unknown) { const error = e as Error & { - details?: Array<{category: string; reason: string}>; + details?: Array<{ + category: string; + reason: string; + description: string | null; + }>; }; // Check if this is a CompilerError with details if (error.details && error.details.length > 0) { @@ -79,6 +83,7 @@ function compileAndGetError( errors: error.details.map(detail => ({ category: detail.category, reason: detail.reason, + description: detail.description, })), }; } @@ -89,6 +94,7 @@ function compileAndGetError( { category: error.name ?? 'Error', reason: error.message, + description: null, }, ], }; @@ -108,7 +114,8 @@ function errorsMatch(a: CompileErrors, b: CompileResult): boolean { for (let i = 0; i < a.errors.length; i++) { if ( a.errors[i].category !== b.errors[i].category || - a.errors[i].reason !== b.errors[i].reason + a.errors[i].reason !== b.errors[i].reason || + a.errors[i].description !== b.errors[i].description ) { return false; } @@ -217,6 +224,45 @@ function* removeCallArguments(ast: t.File): Generator { } } +/** + * Generator that yields ASTs with function parameters removed one at a time + */ +function* removeFunctionParameters(ast: t.File): Generator { + // Collect all functions with parameters + const funcSites: Array<{funcIndex: number; paramCount: number}> = []; + let funcIndex = 0; + t.traverseFast(ast, node => { + if (t.isFunction(node) && node.params.length > 0) { + funcSites.push({funcIndex, paramCount: node.params.length}); + funcIndex++; + } + }); + + // For each function, try removing each parameter (from end to start) + for (const {funcIndex: targetFuncIdx, paramCount} of funcSites) { + for (let paramIdx = paramCount - 1; paramIdx >= 0; paramIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isFunction(node) && node.params.length > 0) { + if (idx === targetFuncIdx && paramIdx < node.params.length) { + node.params.splice(paramIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + /** * Generator that simplifies call expressions by replacing them with their arguments. * For single argument: foo(x) -> x @@ -1566,6 +1612,84 @@ function* removeObjectProperties(ast: t.File): Generator { } } +/** + * Generator that removes elements from array destructuring patterns one at a time + */ +function* removeArrayPatternElements(ast: t.File): Generator { + // Collect all array patterns with elements + const patternSites: Array<{patternIndex: number; elementCount: number}> = []; + let patternIndex = 0; + t.traverseFast(ast, node => { + if (t.isArrayPattern(node) && node.elements.length > 0) { + patternSites.push({patternIndex, elementCount: node.elements.length}); + patternIndex++; + } + }); + + // For each pattern, try removing each element (from end to start) + for (const {patternIndex: targetPatternIdx, elementCount} of patternSites) { + for (let elemIdx = elementCount - 1; elemIdx >= 0; elemIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isArrayPattern(node) && node.elements.length > 0) { + if (idx === targetPatternIdx && elemIdx < node.elements.length) { + node.elements.splice(elemIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + +/** + * Generator that removes properties from object destructuring patterns one at a time + */ +function* removeObjectPatternProperties(ast: t.File): Generator { + // Collect all object patterns with properties + const patternSites: Array<{patternIndex: number; propCount: number}> = []; + let patternIndex = 0; + t.traverseFast(ast, node => { + if (t.isObjectPattern(node) && node.properties.length > 0) { + patternSites.push({patternIndex, propCount: node.properties.length}); + patternIndex++; + } + }); + + // For each pattern, try removing each property (from end to start) + for (const {patternIndex: targetPatternIdx, propCount} of patternSites) { + for (let propIdx = propCount - 1; propIdx >= 0; propIdx--) { + const cloned = cloneAst(ast); + let idx = 0; + let modified = false; + + t.traverseFast(cloned, node => { + if (modified) return; + if (t.isObjectPattern(node) && node.properties.length > 0) { + if (idx === targetPatternIdx && propIdx < node.properties.length) { + node.properties.splice(propIdx, 1); + modified = true; + } + idx++; + } + }); + + if (modified) { + yield cloned; + } + } + } +} + /** * Generator that simplifies assignment expressions (a = b) -> a or b */ @@ -1852,8 +1976,14 @@ function* simplifyIdentifiersRenameRef(ast: t.File): Generator { const simplificationStrategies = [ {name: 'removeStatements', generator: removeStatements}, {name: 'removeCallArguments', generator: removeCallArguments}, + {name: 'removeFunctionParameters', generator: removeFunctionParameters}, {name: 'removeArrayElements', generator: removeArrayElements}, {name: 'removeObjectProperties', generator: removeObjectProperties}, + {name: 'removeArrayPatternElements', generator: removeArrayPatternElements}, + { + name: 'removeObjectPatternProperties', + generator: removeObjectPatternProperties, + }, {name: 'removeJSXAttributes', generator: removeJSXAttributes}, {name: 'removeJSXChildren', generator: removeJSXChildren}, {name: 'removeJSXFragmentChildren', generator: removeJSXFragmentChildren}, diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index b479ce48521c..29e956d314a2 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -7913,6 +7913,25 @@ const testsFlow = { } `, }, + // Flow type aliases in type assertions should not be flagged as missing dependencies + { + code: normalizeIndent` + function MyComponent() { + type ColumnKey = 'id' | 'name'; + type Item = {id: string, name: string}; + + const columns = useMemo( + () => [ + { + type: 'text', + key: 'id', + } as TextColumn, + ], + [], + ); + } + `, + }, ], invalid: [ { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 05321ffb46f6..6b790680608d 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -21,7 +21,7 @@ import type { VariableDeclarator, } from 'estree'; -import { getAdditionalEffectHooksFromSettings } from '../shared/Utils'; +import {getAdditionalEffectHooksFromSettings} from '../shared/Utils'; type DeclaredDependency = { key: string; @@ -80,7 +80,6 @@ const rule = { const rawOptions = context.options && context.options[0]; const settings = context.settings || {}; - // Parse the `additionalHooks` regex. // Use rule-level additionalHooks if provided, otherwise fall back to settings const additionalHooks = @@ -565,8 +564,12 @@ const rule = { continue; } // Ignore Flow type parameters - // @ts-expect-error We don't have flow types - if (def.type === 'TypeParameter') { + if ( + // @ts-expect-error We don't have flow types + def.type === 'TypeParameter' || + // @ts-expect-error Flow-specific AST node type + dependencyNode.parent?.type === 'GenericTypeAnnotation' + ) { continue; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 43db744007d4..08882a04766c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -47,6 +47,7 @@ import type {ViewTransitionState} from './ReactFiberViewTransitionComponent'; import { alwaysThrottleRetries, enableCreateEventHandleAPI, + enableEffectEventMutationPhase, enableHiddenSubtreeInsertionEffectCleanup, enableProfilerTimer, enableProfilerCommitHooks, @@ -499,7 +500,7 @@ function commitBeforeMutationEffectsOnFiber( case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { - if ((flags & Update) !== NoFlags) { + if (!enableEffectEventMutationPhase && (flags & Update) !== NoFlags) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const eventPayloads = updateQueue !== null ? updateQueue.events : null; @@ -2042,6 +2043,24 @@ function commitMutationEffectsOnFiber( case ForwardRef: case MemoComponent: case SimpleMemoComponent: { + // Mutate event effect callbacks on the way down, before mutation effects. + // This ensures that parent event effects are mutated before child effects. + // This isn't a supported use case, so we can re-consider it, + // but this was the behavior we originally shipped. + if (enableEffectEventMutationPhase) { + if (flags & Update) { + const updateQueue: FunctionComponentUpdateQueue | null = + (finishedWork.updateQueue: any); + const eventPayloads = + updateQueue !== null ? updateQueue.events : null; + if (eventPayloads !== null) { + for (let ii = 0; ii < eventPayloads.length; ii++) { + const {ref, nextImpl} = eventPayloads[ii]; + ref.impl = nextImpl; + } + } + } + } recursivelyTraverseMutationEffects(root, finishedWork, lanes); commitReconciliationEffects(finishedWork, lanes); diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index 2d8d3f7fd16c..9f85897fb05c 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -7,7 +7,10 @@ * @flow */ -import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags'; +import { + enableCreateEventHandleAPI, + enableEffectEventMutationPhase, +} from 'shared/ReactFeatureFlags'; export type Flags = number; @@ -99,10 +102,11 @@ export const BeforeMutationMask: number = // TODO: Only need to visit Deletions during BeforeMutation phase if an // element is focused. Update | ChildDeletion | Visibility - : // TODO: The useEffectEvent hook uses the snapshot phase for clean up but it - // really should use the mutation phase for this or at least schedule an - // explicit Snapshot phase flag for this. - Update); + : // useEffectEvent uses the snapshot phase, + // but we're moving it to the mutation phase. + enableEffectEventMutationPhase + ? 0 + : Update); // For View Transition support we use the snapshot phase to scan the tree for potentially // affected ViewTransition components. diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index b54047cb4aa7..987f0338ad1a 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -29,6 +29,7 @@ import { disableLegacyMode, enableDefaultTransitionIndicator, enableGestureTransition, + enableParallelTransitions, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook'; import {clz32} from './clz32'; @@ -208,6 +209,9 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { case TransitionLane8: case TransitionLane9: case TransitionLane10: + if (enableParallelTransitions) { + return getHighestPriorityLane(lanes); + } return lanes & TransitionUpdateLanes; case TransitionLane11: case TransitionLane12: diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index b03f5eff159c..d055b271ad77 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -58,6 +58,7 @@ import { enableViewTransition, enableGestureTransition, enableDefaultTransitionIndicator, + enableParallelTransitions, } from 'shared/ReactFeatureFlags'; import {resetOwnerStackLimit} from 'shared/ReactOwnerStackReset'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -1777,6 +1778,11 @@ function markRootSuspended( spawnedLane: Lane, didAttemptEntireTree: boolean, ) { + if (enableParallelTransitions) { + // When suspending, we should always mark the entangled lanes as suspended. + suspendedLanes = getEntangledLanes(root, suspendedLanes); + } + // When suspending, we should always exclude lanes that were pinged or (more // rarely, since we try to avoid it) updated during the render phase. suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes); diff --git a/packages/react-reconciler/src/__tests__/Activity-test.js b/packages/react-reconciler/src/__tests__/Activity-test.js index a27e62e585fb..9abebad1dd09 100644 --- a/packages/react-reconciler/src/__tests__/Activity-test.js +++ b/packages/react-reconciler/src/__tests__/Activity-test.js @@ -1527,6 +1527,87 @@ describe('Activity', () => { expect(root).toMatchRenderedOutput(); }); + // @gate enableActivity + it('getSnapshotBeforeUpdate does not run in hidden trees', async () => { + let setState; + + class Child extends React.Component { + getSnapshotBeforeUpdate(prevProps) { + const snapshot = `snapshot-${prevProps.value}-to-${this.props.value}`; + Scheduler.log(`getSnapshotBeforeUpdate: ${snapshot}`); + return snapshot; + } + componentDidUpdate(prevProps, prevState, snapshot) { + Scheduler.log(`componentDidUpdate: ${snapshot}`); + } + componentDidMount() { + Scheduler.log('componentDidMount'); + } + componentWillUnmount() { + Scheduler.log('componentWillUnmount'); + } + render() { + Scheduler.log(`render: ${this.props.value}`); + return ; + } + } + + function Wrapper({show}) { + const [value, _setState] = useState(1); + setState = _setState; + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + + // Initial render + await act(() => { + root.render(); + }); + assertLog(['render: 1', 'componentDidMount']); + + // Hide the Activity + await act(() => { + root.render(); + }); + assertLog([ + 'componentWillUnmount', + 'render: 1', + // Bugfix: snapshots for hidden trees should not need to be read. + ...(gate('enableViewTransition') + ? [] + : ['getSnapshotBeforeUpdate: snapshot-1-to-1']), + ]); + + // Trigger an update while hidden by calling setState + await act(() => { + setState(2); + }); + assertLog([ + 'render: 2', + ...(gate('enableViewTransition') + ? [] + : ['getSnapshotBeforeUpdate: snapshot-1-to-2']), + ]); + + // This is treated as a new mount so the snapshot also shouldn't be read. + await act(() => { + root.render(); + }); + assertLog([ + 'render: 2', + ...(gate('enableViewTransition') + ? [] + : ['getSnapshotBeforeUpdate: snapshot-2-to-2']), + 'componentDidMount', + ]); + }); + + // @gate enableActivity it('warns if you pass a hidden prop', async () => { function App() { return ( diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js index e4fe796fd353..3a348307f4cd 100644 --- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js @@ -420,9 +420,13 @@ describe('ReactDeferredValue', () => { // The initial value suspended, so we attempt the final value, which // also suspends. 'Suspend! [Final]', - // pre-warming - 'Suspend! [Loading...]', - 'Suspend! [Final]', + ...(gate('enableParallelTransitions') + ? [] + : [ + // Existing bug: Unnecessary pre-warm. + 'Suspend! [Loading...]', + 'Suspend! [Final]', + ]), ]); expect(root).toMatchRenderedOutput(null); @@ -439,6 +443,171 @@ describe('ReactDeferredValue', () => { }, ); + it( + 'if a suspended render spawns a deferred task that suspends on a sibling, ' + + 'we can finish the original task if the original sibling loads first', + async () => { + function App() { + const deferredText = useDeferredValue(`Final`, `Loading...`); + return ( + <> + {' '} + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog([ + 'Suspend! [Loading...]', + // The initial value suspended, so we attempt the final value, which + // also suspends. + 'Suspend! [Final]', + 'Suspend! [Sibling: Final]', + ...(gate('enableParallelTransitions') + ? [ + // With parallel transitions, + // we do not continue pre-warming. + ] + : [ + 'Suspend! [Loading...]', + 'Suspend! [Sibling: Loading...]', + 'Suspend! [Final]', + 'Suspend! [Sibling: Final]', + ]), + ]); + expect(root).toMatchRenderedOutput(null); + + // The final value loads, so we can skip the initial value entirely. + await act(() => { + resolveText('Final'); + }); + assertLog(['Final', 'Suspend! [Sibling: Final]']); + expect(root).toMatchRenderedOutput(null); + + // The initial value resolves first, so we render that. + await act(() => resolveText('Loading...')); + assertLog([ + 'Loading...', + 'Suspend! [Sibling: Loading...]', + 'Final', + 'Suspend! [Sibling: Final]', + ...(gate('enableParallelTransitions') + ? [ + // With parallel transitions, + // we do not continue pre-warming. + ] + : [ + 'Loading...', + 'Suspend! [Sibling: Loading...]', + 'Final', + 'Suspend! [Sibling: Final]', + ]), + ]); + expect(root).toMatchRenderedOutput(null); + + // The Final sibling loads, we're unblocked and commit. + await act(() => { + resolveText('Sibling: Final'); + }); + assertLog(['Final', 'Sibling: Final']); + expect(root).toMatchRenderedOutput('Final Sibling: Final'); + + // We already rendered the Final value, so nothing happens + await act(() => { + resolveText('Sibling: Loading...'); + }); + assertLog([]); + expect(root).toMatchRenderedOutput('Final Sibling: Final'); + }, + ); + + it( + 'if a suspended render spawns a deferred task that suspends on a sibling,' + + ' we can switch to the deferred task without finishing the original one', + async () => { + function App() { + const deferredText = useDeferredValue(`Final`, `Loading...`); + return ( + <> + {' '} + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog([ + 'Suspend! [Loading...]', + // The initial value suspended, so we attempt the final value, which + // also suspends. + 'Suspend! [Final]', + 'Suspend! [Sibling: Final]', + ...(gate('enableParallelTransitions') + ? [ + // With parallel transitions, + // we do not continue pre-warming. + ] + : [ + 'Suspend! [Loading...]', + 'Suspend! [Sibling: Loading...]', + 'Suspend! [Final]', + 'Suspend! [Sibling: Final]', + ]), + ]); + expect(root).toMatchRenderedOutput(null); + + // The final value loads, so we can skip the initial value entirely. + await act(() => { + resolveText('Final'); + }); + assertLog(['Final', 'Suspend! [Sibling: Final]']); + expect(root).toMatchRenderedOutput(null); + + // The initial value resolves first, so we render that. + await act(() => resolveText('Loading...')); + assertLog([ + 'Loading...', + 'Suspend! [Sibling: Loading...]', + 'Final', + 'Suspend! [Sibling: Final]', + ...(gate('enableParallelTransitions') + ? [ + // With parallel transitions, + // we do not continue pre-warming. + ] + : [ + 'Loading...', + 'Suspend! [Sibling: Loading...]', + 'Final', + 'Suspend! [Sibling: Final]', + ]), + ]); + expect(root).toMatchRenderedOutput(null); + + // The initial sibling loads, we're unblocked and commit. + await act(() => { + resolveText('Sibling: Loading...'); + }); + assertLog([ + 'Loading...', + 'Sibling: Loading...', + 'Final', + 'Suspend! [Sibling: Final]', + ]); + expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...'); + + // Now unblock the final sibling. + await act(() => { + resolveText('Sibling: Final'); + }); + assertLog(['Final', 'Sibling: Final']); + expect(root).toMatchRenderedOutput('Final Sibling: Final'); + }, + ); + it( 'if a suspended render spawns a deferred task, we can switch to the ' + 'deferred task without finishing the original one (no Suspense boundary, ' + @@ -462,9 +631,12 @@ describe('ReactDeferredValue', () => { // The initial value suspended, so we attempt the final value, which // also suspends. 'Suspend! [Final]', - // pre-warming - 'Suspend! [Loading...]', - 'Suspend! [Final]', + ...(gate('enableParallelTransitions') + ? [ + // With parallel transitions, + // we do not continue pre-warming. + ] + : ['Suspend! [Loading...]', 'Suspend! [Final]']), ]); expect(root).toMatchRenderedOutput(null); @@ -539,9 +711,12 @@ describe('ReactDeferredValue', () => { // The initial value suspended, so we attempt the final value, which // also suspends. 'Suspend! [Final]', - // pre-warming - 'Suspend! [Loading...]', - 'Suspend! [Final]', + ...(gate('enableParallelTransitions') + ? [ + // With parallel transitions, + // we do not continue pre-warming. + ] + : ['Suspend! [Loading...]', 'Suspend! [Final]']), ]); expect(root).toMatchRenderedOutput(null); diff --git a/packages/react-reconciler/src/__tests__/ReactTransition-test.js b/packages/react-reconciler/src/__tests__/ReactTransition-test.js index d3ad2e613857..7a962018067a 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransition-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransition-test.js @@ -209,6 +209,319 @@ describe('ReactTransition', () => { expect(root).toMatchRenderedOutput('Async'); }); + // @gate enableLegacyCache + it('when multiple transitions update different queues, they entangle', async () => { + let setA; + let startTransitionA; + let setB; + let startTransitionB; + function A() { + const [a, _setA] = useState(0); + const [isPending, _startTransitionA] = useTransition(); + setA = _setA; + startTransitionA = _startTransitionA; + + return ( + + {isPending && ( + + + + )} + + + ); + } + + function B() { + const [b, _setB] = useState(0); + const [isPending, _startTransitionB] = useTransition(); + setB = _setB; + startTransitionB = _startTransitionB; + + return ( + + {isPending && ( + + + + )} + + + ); + } + function App() { + return ( + <> + Loading A}> + + + Loading B}> + + + + ); + } + + // Initial render + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + assertLog([ + 'Suspend! [A: 0]', + 'Suspend! [B: 0]', + 'Suspend! [A: 0]', + 'Suspend! [B: 0]', + ]); + expect(root).toMatchRenderedOutput( + <> + Loading A + Loading B + , + ); + + // Resolve + await act(() => { + resolveText('A: 0'); + resolveText('B: 0'); + }); + assertLog(['A: 0', 'B: 0']); + expect(root).toMatchRenderedOutput( + <> + A: 0 + B: 0 + , + ); + + // Start transitioning A + await act(() => { + startTransitionA(() => { + setA(1); + }); + }); + assertLog(['Pending A...', 'A: 0', 'Suspend! [A: 1]']); + expect(root).toMatchRenderedOutput( + <> + + Pending A...A: 0 + + B: 0 + , + ); + + // Start transitioning B + await act(() => { + startTransitionB(() => { + setB(1); + }); + }); + assertLog(['Pending B...', 'B: 0', 'Suspend! [A: 1]', 'Suspend! [B: 1]']); + expect(root).toMatchRenderedOutput( + <> + + Pending A...A: 0 + + + Pending B...B: 0 + + , + ); + + // Resolve B + await act(() => { + resolveText('B: 1'); + }); + assertLog( + gate('enableParallelTransitions') + ? ['B: 1', 'Suspend! [A: 1]'] + : ['Suspend! [A: 1]', 'B: 1'], + ); + expect(root).toMatchRenderedOutput( + gate('enableParallelTransitions') ? ( + <> + + Pending A...A: 0 + + B: 1 + + ) : ( + <> + + Pending A...A: 0 + + + Pending B...B: 0 + + + ), + ); + + // Resolve A + await act(() => { + resolveText('A: 1'); + }); + assertLog(gate('enableParallelTransitions') ? ['A: 1'] : ['A: 1', 'B: 1']); + expect(root).toMatchRenderedOutput( + <> + A: 1 + B: 1 + , + ); + }); + + // @gate enableLegacyCache + it('when multiple transitions update different queues, but suspend the same boundary, they do entangle', async () => { + let setA; + let startTransitionA; + let setB; + let startTransitionB; + function A() { + const [a, _setA] = useState(0); + const [isPending, _startTransitionA] = useTransition(); + setA = _setA; + startTransitionA = _startTransitionA; + + return ( + + {isPending && ( + + + + )} + + + ); + } + + function B() { + const [b, _setB] = useState(0); + const [isPending, _startTransitionB] = useTransition(); + setB = _setB; + startTransitionB = _startTransitionB; + + return ( + + {isPending && ( + + + + )} + + + ); + } + function App() { + return ( + Loading...}> + + + + ); + } + + // Initial render + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + assertLog([ + 'Suspend! [A: 0]', + // pre-warming + 'Suspend! [A: 0]', + 'Suspend! [B: 0]', + ]); + expect(root).toMatchRenderedOutput(Loading...); + + // Resolve + await act(() => { + resolveText('A: 0'); + resolveText('B: 0'); + }); + assertLog(['A: 0', 'B: 0']); + expect(root).toMatchRenderedOutput( + <> + A: 0 + B: 0 + , + ); + + // Start transitioning A + await act(() => { + startTransitionA(() => { + setA(1); + }); + }); + assertLog(['Pending A...', 'A: 0', 'Suspend! [A: 1]']); + expect(root).toMatchRenderedOutput( + <> + + Pending A...A: 0 + + B: 0 + , + ); + + // Start transitioning B + await act(() => { + startTransitionB(() => { + setB(1); + }); + }); + assertLog(['Pending B...', 'B: 0', 'Suspend! [A: 1]', 'Suspend! [B: 1]']); + expect(root).toMatchRenderedOutput( + <> + + Pending A...A: 0 + + + Pending B...B: 0 + + , + ); + + // Resolve B + await act(() => { + resolveText('B: 1'); + }); + assertLog( + gate('enableParallelTransitions') + ? ['B: 1', 'Suspend! [A: 1]'] + : ['Suspend! [A: 1]', 'B: 1'], + ); + expect(root).toMatchRenderedOutput( + gate('enableParallelTransitions') ? ( + <> + + Pending A...A: 0 + + B: 1 + + ) : ( + <> + + Pending A...A: 0 + + + Pending B...B: 0 + + + ), + ); + + // Resolve A + await act(() => { + resolveText('A: 1'); + }); + assertLog(gate('enableParallelTransitions') ? ['A: 1'] : ['A: 1', 'B: 1']); + expect(root).toMatchRenderedOutput( + <> + A: 1 + B: 1 + , + ); + }); + // @gate enableLegacyCache it( 'when multiple transitions update the same queue, only the most recent ' + diff --git a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js index 2b5af2206b9b..b120adf61159 100644 --- a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js +++ b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js @@ -27,6 +27,7 @@ describe('useEffectEvent', () => { let waitForAll; let assertLog; let waitForThrow; + let waitFor; beforeEach(() => { React = require('react'); @@ -46,6 +47,7 @@ describe('useEffectEvent', () => { waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; waitForThrow = InternalTestUtils.waitForThrow; + waitFor = InternalTestUtils.waitFor; }); function Text(props) { @@ -595,6 +597,358 @@ describe('useEffectEvent', () => { assertLog(['Effect value: 2', 'Event value: 2']); }); + it('fires all (interleaved) effects with useEffectEvent in correct order', async () => { + function CounterA({count}) { + const onEvent = useEffectEvent(() => { + return `A ${count}`; + }); + + useInsertionEffect(() => { + // Call the event function to verify it sees the latest value + Scheduler.log(`Parent Insertion Create: ${onEvent()}`); + return () => { + Scheduler.log(`Parent Insertion Create: ${onEvent()}`); + }; + }); + + useLayoutEffect(() => { + Scheduler.log(`Parent Layout Create: ${onEvent()}`); + return () => { + Scheduler.log(`Parent Layout Cleanup: ${onEvent()}`); + }; + }); + + useEffect(() => { + Scheduler.log(`Parent Passive Create: ${onEvent()}`); + return () => { + Scheduler.log(`Parent Passive Destroy ${onEvent()}`); + }; + }); + + // this breaks the rules, but ensures the ordering is correct. + return ; + } + + function CounterB({count, onEventParent}) { + const onEvent = useEffectEvent(() => { + return `${onEventParent()} B ${count}`; + }); + + useInsertionEffect(() => { + Scheduler.log(`Child Insertion Create ${onEvent()}`); + return () => { + Scheduler.log(`Child Insertion Destroy ${onEvent()}`); + }; + }); + + useLayoutEffect(() => { + Scheduler.log(`Child Layout Create ${onEvent()}`); + return () => { + Scheduler.log(`Child Layout Destroy ${onEvent()}`); + }; + }); + + useEffect(() => { + Scheduler.log(`Child Passive Create ${onEvent()}`); + return () => { + Scheduler.log(`Child Passive Destroy ${onEvent()}`); + }; + }); + + return null; + } + + await act(async () => { + ReactNoop.render(); + }); + + assertLog([ + 'Child Insertion Create A 1 B 1', + 'Parent Insertion Create: A 1', + 'Child Layout Create A 1 B 1', + 'Parent Layout Create: A 1', + 'Child Passive Create A 1 B 1', + 'Parent Passive Create: A 1', + ]); + + await act(async () => { + ReactNoop.render(); + }); + + assertLog([ + 'Child Insertion Destroy A 2 B 2', + 'Child Insertion Create A 2 B 2', + 'Child Layout Destroy A 2 B 2', + 'Parent Insertion Create: A 2', + 'Parent Insertion Create: A 2', + 'Parent Layout Cleanup: A 2', + 'Child Layout Create A 2 B 2', + 'Parent Layout Create: A 2', + 'Child Passive Destroy A 2 B 2', + 'Parent Passive Destroy A 2', + 'Child Passive Create A 2 B 2', + 'Parent Passive Create: A 2', + ]); + + // Unmount everything + await act(async () => { + ReactNoop.render(null); + }); + + assertLog([ + 'Parent Insertion Create: A 2', + 'Parent Layout Cleanup: A 2', + 'Child Insertion Destroy A 2 B 2', + 'Child Layout Destroy A 2 B 2', + 'Parent Passive Destroy A 2', + 'Child Passive Destroy A 2 B 2', + ]); + }); + + it('correctly mutates effect event with Activity', async () => { + let setState; + let setChildState; + function CounterA({count, hideChild}) { + const [state, _setState] = useState(1); + setState = _setState; + const onEvent = useEffectEvent(() => { + return `A ${count} ${state}`; + }); + + useInsertionEffect(() => { + // Call the event function to verify it sees the latest value + Scheduler.log(`Parent Insertion Create: ${onEvent()}`); + return () => { + Scheduler.log(`Parent Insertion Create: ${onEvent()}`); + }; + }); + + useLayoutEffect(() => { + Scheduler.log(`Parent Layout Create: ${onEvent()}`); + return () => { + Scheduler.log(`Parent Layout Cleanup: ${onEvent()}`); + }; + }); + + // this breaks the rules, but ensures the ordering is correct. + return ( + + + + ); + } + + function CounterB({count, state, onEventParent}) { + const [childState, _setChildState] = useState(1); + setChildState = _setChildState; + const onEvent = useEffectEvent(() => { + return `${onEventParent()} B ${count} ${state} ${childState}`; + }); + + useInsertionEffect(() => { + Scheduler.log(`Child Insertion Create ${onEvent()}`); + return () => { + Scheduler.log(`Child Insertion Destroy ${onEvent()}`); + }; + }); + + useLayoutEffect(() => { + Scheduler.log(`Child Layout Create ${onEvent()}`); + return () => { + Scheduler.log(`Child Layout Destroy ${onEvent()}`); + }; + }); + + useEffect(() => { + Scheduler.log(`Child Passive Create ${onEvent()}`); + return () => { + Scheduler.log(`Child Passive Destroy ${onEvent()}`); + }; + }); + + return null; + } + + await act(async () => { + ReactNoop.render(); + await waitFor([ + 'Parent Insertion Create: A 1 1', + 'Parent Layout Create: A 1 1', + 'Child Insertion Create A 1 1 B 1 1 1', + ]); + }); + + assertLog([]); + + await act(async () => { + ReactNoop.render(); + + await waitFor([ + 'Parent Insertion Create: A 2 1', + 'Parent Insertion Create: A 2 1', + 'Parent Layout Cleanup: A 2 1', + 'Parent Layout Create: A 2 1', + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? [ + 'Child Insertion Destroy A 2 1 B 1 1 1', + 'Child Insertion Create A 2 1 B 1 1 1', + ] + : [ + 'Child Insertion Destroy A 2 1 B 2 1 1', + 'Child Insertion Create A 2 1 B 2 1 1', + ]), + ]); + }); + + assertLog([]); + + await act(async () => { + setState(2); + + await waitFor([ + 'Parent Insertion Create: A 2 2', + 'Parent Insertion Create: A 2 2', + 'Parent Layout Cleanup: A 2 2', + 'Parent Layout Create: A 2 2', + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? [ + 'Child Insertion Destroy A 2 2 B 1 1 1', + 'Child Insertion Create A 2 2 B 1 1 1', + ] + : [ + 'Child Insertion Destroy A 2 2 B 2 2 1', + 'Child Insertion Create A 2 2 B 2 2 1', + ]), + ]); + }); + + assertLog([]); + + await act(async () => { + setChildState(2); + + await waitFor( + gate('enableViewTransition') && !gate('enableEffectEventMutationPhase') + ? [ + 'Child Insertion Destroy A 2 2 B 1 1 1', + 'Child Insertion Create A 2 2 B 1 1 1', + ] + : [ + 'Child Insertion Destroy A 2 2 B 2 2 2', + 'Child Insertion Create A 2 2 B 2 2 2', + ], + ); + }); + + assertLog([]); + + await act(async () => { + ReactNoop.render(); + + await waitFor([ + 'Parent Insertion Create: A 3 2', + 'Parent Insertion Create: A 3 2', + 'Parent Layout Cleanup: A 3 2', + 'Parent Layout Create: A 3 2', + ]); + }); + + assertLog( + gate('enableViewTransition') && !gate('enableEffectEventMutationPhase') + ? [ + 'Child Insertion Destroy A 3 2 B 1 1 1', + 'Child Insertion Create A 3 2 B 1 1 1', + ] + : [ + 'Child Insertion Destroy A 3 2 B 3 2 2', + 'Child Insertion Create A 3 2 B 3 2 2', + ], + ); + + await act(async () => { + ReactNoop.render(); + + await waitFor([ + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? [ + 'Child Insertion Destroy A 3 2 B 1 1 1', + 'Child Insertion Create A 3 2 B 1 1 1', + ] + : [ + 'Child Insertion Destroy A 3 2 B 3 2 2', + 'Child Insertion Create A 3 2 B 3 2 2', + ]), + 'Parent Insertion Create: A 3 2', + 'Parent Insertion Create: A 3 2', + 'Parent Layout Cleanup: A 3 2', + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? ['Child Layout Create A 3 2 B 1 1 1'] + : ['Child Layout Create A 3 2 B 3 2 2']), + + 'Parent Layout Create: A 3 2', + ]); + }); + + assertLog( + gate('enableViewTransition') && !gate('enableEffectEventMutationPhase') + ? ['Child Passive Create A 3 2 B 1 1 1'] + : ['Child Passive Create A 3 2 B 3 2 2'], + ); + + await act(async () => { + ReactNoop.render(); + + await waitFor([ + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? ['Child Layout Destroy A 3 2 B 1 1 1'] + : ['Child Layout Destroy A 3 2 B 3 2 2']), + 'Parent Insertion Create: A 3 2', + 'Parent Insertion Create: A 3 2', + 'Parent Layout Cleanup: A 3 2', + 'Parent Layout Create: A 3 2', + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? ['Child Passive Destroy A 3 2 B 1 1 1'] + : ['Child Passive Destroy A 3 2 B 3 2 2']), + ]); + }); + + assertLog( + gate('enableViewTransition') && !gate('enableEffectEventMutationPhase') + ? [ + 'Child Insertion Destroy A 3 2 B 1 1 1', + 'Child Insertion Create A 3 2 B 1 1 1', + ] + : [ + 'Child Insertion Destroy A 3 2 B 3 2 2', + 'Child Insertion Create A 3 2 B 3 2 2', + ], + ); + + // Unmount everything + await act(async () => { + ReactNoop.render(null); + }); + + assertLog([ + 'Parent Insertion Create: A 3 2', + 'Parent Layout Cleanup: A 3 2', + ...(gate('enableHiddenSubtreeInsertionEffectCleanup') + ? [ + gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? 'Child Insertion Destroy A 3 2 B 1 1 1' + : 'Child Insertion Destroy A 3 2 B 3 2 2', + ] + : []), + ]); + }); + it("doesn't provide a stable identity", async () => { function Counter({shouldRender, value}) { const onClick = useEffectEvent(() => { @@ -916,4 +1270,66 @@ describe('useEffectEvent', () => { logContextValue(); assertLog(['ContextReader (Effect event): second']); }); + + // @gate enableActivity + it('effect events are fresh inside Activity', async () => { + function Child({value}) { + const getValue = useEffectEvent(() => { + return value; + }); + useInsertionEffect(() => { + Scheduler.log('insertion create: ' + getValue()); + return () => { + Scheduler.log('insertion destroy: ' + getValue()); + }; + }); + useLayoutEffect(() => { + Scheduler.log('layout create: ' + getValue()); + return () => { + Scheduler.log('layout destroy: ' + getValue()); + }; + }); + + Scheduler.log('render: ' + value); + return null; + } + + function App({value, mode}) { + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + + // Mount hidden + await act(async () => root.render()); + assertLog(['render: 1', 'insertion create: 1']); + + // Update, still hidden + await act(async () => root.render()); + + // Bug in enableViewTransition. Insertion and layout see stale closure. + assertLog([ + 'render: 2', + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? ['insertion destroy: 1', 'insertion create: 1'] + : ['insertion destroy: 2', 'insertion create: 2']), + ]); + + // Switch to visible + await act(async () => root.render()); + + // Bug in enableViewTransition. Even when switching to visible, sees stale closure. + assertLog([ + 'render: 2', + ...(gate('enableViewTransition') && + !gate('enableEffectEventMutationPhase') + ? ['insertion destroy: 1', 'insertion create: 1', 'layout create: 1'] + : ['insertion destroy: 2', 'insertion create: 2', 'layout create: 2']), + ]); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 6edfbaa4df3e..80d5f3e5c005 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2345,19 +2345,53 @@ function visitAsyncNode( >, cutOff: number, ): void | null | PromiseNode | IONode { - if (visited.has(node)) { - // It's possible to visit them same node twice when it's part of both an "awaited" path - // and a "previous" path. This also gracefully handles cycles which would be a bug. - return visited.get(node); - } - // Set it as visited early in case we see ourselves before returning. - visited.set(node, null); - const result = visitAsyncNodeImpl(request, task, node, visited, cutOff); - if (result !== null) { - // If we ended up with a value, let's use that value for future visits. - visited.set(node, result); + // Collect the previous chain iteratively instead of recursively to avoid + // stack overflow on deep chains. We process from deepest to shallowest so + // each node has its previousIONode available. + const chain: Array = []; + let current: AsyncSequence | null = node; + + while (current !== null) { + if (visited.has(current)) { + break; + } + chain.push(current); + current = current.previous; } - return result; + + let previousIONode: void | null | PromiseNode | IONode = + current !== null ? visited.get(current) : null; + + // Process from deepest to shallowest (reverse order). + for (let i = chain.length - 1; i >= 0; i--) { + const n = chain[i]; + // Set it as visited early in case we see the node again before returning. + visited.set(n, null); + + const result = visitAsyncNodeImpl( + request, + task, + n, + visited, + cutOff, + previousIONode, + ); + + if (result !== null) { + // If we ended up with a value, let's use that value for future visits. + visited.set(n, result); + } + + if (result === undefined) { + // Undefined is used as a signal that we found a suitable aborted node + // and we don't have to find further aborted nodes. + return undefined; + } + + previousIONode = result; + } + + return previousIONode; } function visitAsyncNodeImpl( @@ -2369,6 +2403,7 @@ function visitAsyncNodeImpl( void | null | PromiseNode | IONode, >, cutOff: number, + previousIONode: void | null | PromiseNode | IONode, ): void | null | PromiseNode | IONode { if (node.end >= 0 && node.end <= request.timeOrigin) { // This was already resolved when we started this render. It must have been either something @@ -2377,23 +2412,6 @@ function visitAsyncNodeImpl( return null; } - let previousIONode: void | null | PromiseNode | IONode = null; - // First visit anything that blocked this sequence to start in the first place. - if (node.previous !== null) { - previousIONode = visitAsyncNode( - request, - task, - node.previous, - visited, - cutOff, - ); - if (previousIONode === undefined) { - // Undefined is used as a signal that we found a suitable aborted node and we don't have to find - // further aborted nodes. - return undefined; - } - } - // `found` represents the return value of the following switch statement. // We can't use multiple `return` statements in the switch statement // since that prevents Closure compiler from inlining `visitAsyncImpl` diff --git a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js index 2570c0d12e2a..336d797efb61 100644 --- a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js +++ b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js @@ -5,6 +5,7 @@ import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate'; +import crypto from 'crypto'; import fs from 'fs/promises'; import path from 'path'; @@ -160,9 +161,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 129, + 130, + 109, 109, - 108, 50, ], ], @@ -184,9 +185,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 129, + 130, + 109, 109, - 108, 50, ], ], @@ -195,25 +196,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 110, + 111, 13, - 109, + 110, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 118, + 119, 26, - 117, + 118, 5, ], ], @@ -232,9 +233,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 129, + 130, + 109, 109, - 108, 50, ], ], @@ -243,17 +244,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 110, + 111, 13, - 109, + 110, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 118, + 119, 26, - 117, + 118, 5, ], ], @@ -278,9 +279,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 129, + 130, + 109, 109, - 108, 50, ], ], @@ -289,25 +290,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 111, + 112, 21, - 109, + 110, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 118, + 119, 20, - 117, + 118, 5, ], ], @@ -326,9 +327,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 129, + 130, + 109, 109, - 108, 50, ], ], @@ -337,17 +338,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 113, + 114, 21, - 109, + 110, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 118, + 119, 20, - 117, + 118, 5, ], ], @@ -367,9 +368,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 120, + 121, 60, - 117, + 118, 5, ], ], @@ -391,9 +392,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 129, + 130, + 109, 109, - 108, 50, ], ], @@ -402,17 +403,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 111, + 112, 21, - 109, + 110, 5, ], ], @@ -431,9 +432,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 120, + 121, 60, - 117, + 118, 5, ], ], @@ -442,9 +443,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 126, + 127, 35, - 123, + 124, 5, ], ], @@ -625,9 +626,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 589, + 590, 40, - 570, + 571, 49, ], [ @@ -657,9 +658,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 589, + 590, 40, - 570, + 571, 49, ], [ @@ -676,25 +677,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 572, + 573, 13, - 571, + 572, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 577, + 578, 36, - 576, + 577, 5, ], ], @@ -713,9 +714,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 589, + 590, 40, - 570, + 571, 49, ], [ @@ -732,17 +733,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 572, + 573, 13, - 571, + 572, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 577, + 578, 36, - 576, + 577, 5, ], ], @@ -762,9 +763,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 579, + 580, 60, - 576, + 577, 5, ], ], @@ -783,9 +784,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 589, + 590, 40, - 570, + 571, 49, ], [ @@ -802,25 +803,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 572, + 573, 13, - 571, + 572, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 578, + 579, 22, - 576, + 577, 5, ], ], @@ -839,9 +840,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 579, + 580, 60, - 576, + 577, 5, ], ], @@ -850,9 +851,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 585, + 586, 40, - 582, + 583, 5, ], ], @@ -927,9 +928,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 896, + 897, 109, - 883, + 884, 80, ], ], @@ -948,9 +949,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 896, + 897, 109, - 883, + 884, 80, ], ], @@ -967,9 +968,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 896, + 897, 109, - 883, + 884, 80, ], ], @@ -1041,9 +1042,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1010, + 1011, 109, - 1001, + 1002, 94, ], ], @@ -1114,9 +1115,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1083, + 1084, 109, - 1059, + 1060, 50, ], ], @@ -1198,9 +1199,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1167, + 1168, 109, - 1150, + 1151, 63, ], ], @@ -1217,17 +1218,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 93, + 94, 40, - 91, + 92, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1163, + 1164, 24, - 1162, + 1163, 5, ], ], @@ -1249,17 +1250,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 93, + 94, 40, - 91, + 92, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1163, + 1164, 24, - 1162, + 1163, 5, ], ], @@ -1268,25 +1269,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1152, + 1153, 13, - 1151, + 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1158, + 1159, 24, - 1157, + 1158, 5, ], ], @@ -1305,17 +1306,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 93, + 94, 40, - 91, + 92, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1163, + 1164, 24, - 1162, + 1163, 5, ], ], @@ -1324,17 +1325,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1152, + 1153, 13, - 1151, + 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1158, + 1159, 24, - 1157, + 1158, 5, ], ], @@ -1359,17 +1360,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 93, + 94, 40, - 91, + 92, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1163, + 1164, 24, - 1162, + 1163, 5, ], ], @@ -1378,25 +1379,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1153, + 1154, 13, - 1151, + 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1158, + 1159, 18, - 1157, + 1158, 5, ], ], @@ -1415,17 +1416,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "fetchThirdParty", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 93, + 94, 40, - 91, + 92, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1163, + 1164, 24, - 1162, + 1163, 5, ], ], @@ -1434,17 +1435,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1153, + 1154, 13, - 1151, + 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1158, + 1159, 18, - 1157, + 1158, 5, ], ], @@ -1544,9 +1545,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1508, + 1509, 40, - 1491, + 1492, 62, ], [ @@ -1576,9 +1577,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1508, + 1509, 40, - 1491, + 1492, 62, ], [ @@ -1595,25 +1596,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1493, + 1494, 13, - 1492, + 1493, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1503, + 1504, 13, - 1502, + 1503, 5, ], ], @@ -1632,9 +1633,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1508, + 1509, 40, - 1491, + 1492, 62, ], [ @@ -1651,17 +1652,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1493, + 1494, 13, - 1492, + 1493, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1503, + 1504, 13, - 1502, + 1503, 5, ], ], @@ -1681,9 +1682,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1504, + 1505, 60, - 1502, + 1503, 5, ], ], @@ -1705,9 +1706,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1508, + 1509, 40, - 1491, + 1492, 62, ], [ @@ -1724,25 +1725,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1493, + 1494, 13, - 1492, + 1493, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1503, + 1504, 13, - 1502, + 1503, 5, ], ], @@ -1761,9 +1762,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1504, + 1505, 60, - 1502, + 1503, 5, ], ], @@ -1772,9 +1773,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Child", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1498, + 1499, 28, - 1497, + 1498, 5, ], ], @@ -1857,9 +1858,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1821, + 1822, 40, - 1805, + 1806, 57, ], [ @@ -1889,9 +1890,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1821, + 1822, 40, - 1805, + 1806, 57, ], [ @@ -1908,25 +1909,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1807, + 1808, 13, - 1806, + 1807, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1816, + 1817, 23, - 1815, + 1816, 5, ], ], @@ -1945,9 +1946,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1821, + 1822, 40, - 1805, + 1806, 57, ], [ @@ -1964,17 +1965,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1807, + 1808, 13, - 1806, + 1807, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1816, + 1817, 23, - 1815, + 1816, 5, ], ], @@ -1994,9 +1995,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1817, + 1818, 60, - 1815, + 1816, 5, ], ], @@ -2015,9 +2016,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1821, + 1822, 40, - 1805, + 1806, 57, ], [ @@ -2034,25 +2035,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1807, + 1808, 13, - 1806, + 1807, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1816, + 1817, 23, - 1815, + 1816, 5, ], ], @@ -2066,9 +2067,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1817, + 1818, 60, - 1815, + 1816, 5, ], ], @@ -2153,9 +2154,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2117, + 2118, 40, - 2099, + 2100, 80, ], [ @@ -2185,9 +2186,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2117, + 2118, 40, - 2099, + 2100, 80, ], [ @@ -2204,25 +2205,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2107, + 2108, 13, - 2105, + 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2112, + 2113, 13, - 2111, + 2112, 5, ], ], @@ -2241,9 +2242,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2117, + 2118, 40, - 2099, + 2100, 80, ], [ @@ -2260,17 +2261,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2107, + 2108, 13, - 2105, + 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2112, + 2113, 13, - 2111, + 2112, 5, ], ], @@ -2292,9 +2293,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2117, + 2118, 40, - 2099, + 2100, 80, ], [ @@ -2311,33 +2312,33 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2101, + 2102, 13, - 2100, + 2101, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2106, + 2107, 15, - 2105, + 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2112, + 2113, 13, - 2111, + 2112, 5, ], ], @@ -2356,9 +2357,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2117, + 2118, 40, - 2099, + 2100, 80, ], [ @@ -2375,25 +2376,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2101, + 2102, 13, - 2100, + 2101, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2106, + 2107, 15, - 2105, + 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2112, + 2113, 13, - 2111, + 2112, 5, ], ], @@ -2415,9 +2416,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2117, + 2118, 40, - 2099, + 2100, 80, ], [ @@ -2434,17 +2435,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2102, + 2103, 13, - 2100, + 2101, 5, ], ], @@ -2463,9 +2464,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2117, + 2118, 40, - 2099, + 2100, 80, ], [ @@ -2482,9 +2483,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2102, + 2103, 13, - 2100, + 2101, 5, ], ], @@ -2557,9 +2558,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2526, + 2527, 109, - 2515, + 2516, 58, ], ], @@ -2581,9 +2582,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2526, + 2527, 109, - 2515, + 2516, 58, ], ], @@ -2592,25 +2593,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2517, + 2518, 14, - 2516, + 2517, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2523, + 2524, 20, - 2522, + 2523, 5, ], ], @@ -2629,9 +2630,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2526, + 2527, 109, - 2515, + 2516, 58, ], ], @@ -2640,17 +2641,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2517, + 2518, 23, - 2516, + 2517, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2523, + 2524, 20, - 2522, + 2523, 5, ], ], @@ -2729,9 +2730,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2693, + 2694, 40, - 2681, + 2682, 56, ], [ @@ -2761,9 +2762,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2693, + 2694, 40, - 2681, + 2682, 56, ], [ @@ -2780,17 +2781,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 86, + 87, 12, - 85, + 86, 3, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2689, + 2690, 20, - 2688, + 2689, 5, ], ], @@ -2809,9 +2810,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2693, + 2694, 40, - 2681, + 2682, 56, ], [ @@ -2828,9 +2829,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2689, + 2690, 20, - 2688, + 2689, 5, ], ], @@ -2923,9 +2924,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2882, + 2883, 40, - 2861, + 2862, 42, ], [ @@ -2955,9 +2956,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2882, + 2883, 40, - 2861, + 2862, 42, ], [ @@ -2974,17 +2975,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2868, + 2869, 15, - 2867, + 2868, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2877, + 2878, 19, - 2876, + 2877, 5, ], ], @@ -3003,9 +3004,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2882, + 2883, 40, - 2861, + 2862, 42, ], [ @@ -3022,17 +3023,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2868, + 2869, 15, - 2867, + 2868, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2877, + 2878, 19, - 2876, + 2877, 5, ], ], @@ -3054,9 +3055,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2882, + 2883, 40, - 2861, + 2862, 42, ], [ @@ -3073,9 +3074,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2877, + 2878, 25, - 2876, + 2877, 5, ], ], @@ -3094,9 +3095,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2882, + 2883, 40, - 2861, + 2862, 42, ], [ @@ -3113,9 +3114,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2877, + 2878, 25, - 2876, + 2877, 5, ], ], @@ -3191,9 +3192,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3158, + 3159, 19, - 3146, + 3147, 36, ], ], @@ -3215,9 +3216,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3158, + 3159, 19, - 3146, + 3147, 36, ], ], @@ -3226,9 +3227,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3150, + 3151, 7, - 3148, + 3149, 5, ], ], @@ -3247,9 +3248,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3158, + 3159, 19, - 3146, + 3147, 36, ], ], @@ -3258,9 +3259,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3152, + 3153, 7, - 3148, + 3149, 5, ], ], @@ -3385,9 +3386,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3303, + 3304, 20, - 3302, + 3303, 5, ], ], @@ -3402,4 +3403,234 @@ describe('ReactFlightAsyncDebugInfo', () => { `); } }); + + // Regression test: Database clients like Gel/EdgeDB with connection pools can + // create very long chains of async nodes linked via `previous` pointers. + // visitAsyncNode must handle chains of thousands of nodes without stack + // overflow. + // + // The pattern that creates deep chains (Event/messageWaiter): + // 1. Create a Promise resolved by an I/O callback + // 2. Start I/O, await the Promise + // 3. When I/O resolves, code continues in the callback context + // 4. The next iteration's async nodes link via `previous` to the current + // context + it('handles deep linear async chains from connection pool patterns', async () => { + // Replicate Gel's Event class pattern, a Promise resolved externally by I/O + class Event { + constructor() { + this._resolve = null; + this._promise = new Promise(resolve => { + this._resolve = resolve; + }); + } + + wait() { + return this._promise; + } + + set() { + this._resolve(true); + } + } + + // Replicate Gel's _waitForMessage pattern: + // Each iteration creates an Event, schedules I/O to resolve it, and awaits. + // The next iteration runs in the context of the previous I/O callback, + // creating linear chains of previous pointers. + async function buildLinearChain(depth) { + for (let i = 0; i < depth; i++) { + const event = new Event(); + // crypto.randomBytes uses the thread pool and is recognized as I/O + // (type='RANDOMBYTESREQUEST'). It's much faster than setTimeout. + crypto.randomBytes(1, () => event.set()); + await event.wait(); + // After this await resolves, we're in the crypto callback's context + // The next Event will be created in this context, linking prev pointers + } + } + + async function Component() { + // Use 2000 iterations to ensure regression would be caught. Using + // crypto.randomBytes keeps the test fast. + await buildLinearChain(2000); + return 'done'; + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + , + {}, + { + filterStackFrame: (filename, functionName) => { + // Custom filter that treats Event and buildLinearChain as library + // code. This simulates how real DB libraries like Gel/EdgeDB would be + // filtered. + if ( + functionName === 'new Event' || + functionName === 'buildLinearChain' || + functionName === '_loop' // Generated name for the for-loop + ) { + return false; + } + return filterStackFrame(filename, functionName); + }, + }, + ); + + const readable = new Stream.PassThrough(streamOptions); + + const result = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: {}, + moduleLoading: {}, + }); + stream.pipe(readable); + + // This should not throw "Maximum call stack size exceeded" + expect(await result).toBe('done'); + + await finishLoadingStream(readable); + if ( + __DEV__ && + gate( + flags => + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo, + ) + ) { + // With library code filtered out, we should only see the Component's + // debug info, not thousands of entries from the internal + // Event/buildLinearChain operations. + expect(getDebugInfo(result)).toMatchInlineSnapshot(` + [ + { + "time": 0, + }, + { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 3461, + 40, + 3418, + 72, + ], + [ + "new Promise", + "", + 0, + 0, + 0, + 0, + ], + ], + }, + { + "time": 0, + }, + { + "awaited": { + "end": 0, + "env": "Server", + "name": "buildLinearChain", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 3461, + 40, + 3418, + 72, + ], + [ + "new Promise", + "", + 0, + 0, + 0, + 0, + ], + ], + }, + "stack": [ + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 3456, + 13, + 3453, + 5, + ], + ], + "start": 0, + "value": { + "value": undefined, + }, + }, + "env": "Server", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "Object.", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 3461, + 40, + 3418, + 72, + ], + [ + "new Promise", + "", + 0, + 0, + 0, + 0, + ], + ], + }, + "stack": [ + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 3456, + 13, + 3453, + 5, + ], + ], + }, + { + "time": 0, + }, + { + "time": 0, + }, + { + "awaited": { + "byteSize": 0, + "end": 0, + "name": "rsc stream", + "owner": null, + "start": 0, + "value": { + "value": "stream", + }, + }, + }, + ] + `); + } + }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 113370d1eb79..bdeab6aef43e 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -123,6 +123,10 @@ export const enableFizzExternalRuntime = __EXPERIMENTAL__; export const alwaysThrottleRetries: boolean = true; +// Gate whether useEffectEvent uses the mutation phase (true) or before-mutation +// phase (false) for updating event function references. +export const enableEffectEventMutationPhase: boolean = false; + export const passChildrenWhenCloningPersistedNodes: boolean = false; export const enableEagerAlternateStateNodeCleanup: boolean = true; @@ -215,6 +219,9 @@ export const disableInputAttributeSyncing: boolean = false; // Disables children for