diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index 7a77ab20eb9..1de972658c2 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -857,7 +857,7 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.008, "type": "render-idle", @@ -873,25 +873,17 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.012, "type": "commit", }, - { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.02, - "type": "layout-effects", - }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.023, + "timestamp": 0.021, "type": "passive-effects", }, ], @@ -899,9 +891,9 @@ describe('Timeline profiler', () => { { "batchUID": 1, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render-idle", }, { @@ -909,31 +901,23 @@ describe('Timeline profiler', () => { "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render", }, { "batchUID": 1, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.032, + "timestamp": 0.03, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.04, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.043, + "timestamp": 0.039, "type": "passive-effects", }, ], @@ -949,26 +933,26 @@ describe('Timeline profiler', () => { { "componentName": "App", "duration": 0.002, - "timestamp": 0.024, + "timestamp": 0.022, "type": "passive-effect-mount", "warning": null, }, { "componentName": "App", "duration": 0.001, - "timestamp": 0.029, + "timestamp": 0.027, "type": "render", "warning": null, }, { "componentName": "App", "duration": 0.001, - "timestamp": 0.044, + "timestamp": 0.04, "type": "passive-effect-mount", "warning": null, }, ], - "duration": 0.046, + "duration": 0.042, "flamechart": [], "internalModuleSourceToRanges": Map { undefined => [ @@ -1031,7 +1015,7 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.008, "type": "render-idle", @@ -1047,33 +1031,25 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.012, "type": "commit", }, - { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.02, - "type": "layout-effects", - }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.023, + "timestamp": 0.021, "type": "passive-effects", }, { "batchUID": 1, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render-idle", }, { @@ -1081,31 +1057,23 @@ describe('Timeline profiler', () => { "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render", }, { "batchUID": 1, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.032, + "timestamp": 0.03, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.04, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.043, + "timestamp": 0.039, "type": "passive-effects", }, ], @@ -1149,7 +1117,7 @@ describe('Timeline profiler', () => { { "componentName": "App", "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.025, + "timestamp": 0.023, "type": "schedule-state-update", "warning": null, }, @@ -1254,6 +1222,15 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + promise.displayName = 'Testing displayName'; + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -1273,7 +1250,7 @@ describe('Timeline profiler', () => { const testMarks = [creactCpuProfilerSample()]; const root = ReactDOMClient.createRoot(document.createElement('div')); - utils.act(() => + await utils.actAsync(() => root.render( @@ -1823,6 +1800,14 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -1881,6 +1866,14 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -2192,14 +2185,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, @@ -2234,14 +2219,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 2, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 2, "depth": 0, @@ -2292,8 +2269,8 @@ describe('Timeline profiler', () => { 8 => "InputContinuous", 16 => "DefaultHydration", 32 => "Default", - 64 => "TransitionHydration", - 128 => "Transition", + 64 => undefined, + 128 => "TransitionHydration", 256 => "Transition", 512 => "Transition", 1024 => "Transition", @@ -2349,14 +2326,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, @@ -2389,14 +2358,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 2, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 2, "depth": 0, diff --git a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js index cead054d318..c8384b2fa42 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js @@ -215,7 +215,11 @@ describe('ProfilerStore', () => { it('should not throw while initializing context values for Fibers within a not-yet-mounted subtree', () => { const promise = new Promise(resolve => {}); const SuspendingView = () => { - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } }; const App = () => { diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index fb1fa6c5f22..c0f0804f416 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -682,6 +682,14 @@ describe('ProfilingCache', () => { it('should calculate durations correctly for suspended views', async () => { let data; const getData = () => { + if (React.use) { + if (!data) { + data = new Promise(resolve => { + resolve('abc'); + }); + } + return React.use(data); + } if (data) { return data; } else { diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 93c7048b2bc..e982ac5ecc5 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -44,10 +44,10 @@ describe('Store', () => { const {render, unmount, createContainer} = getVersionedRenderImplementation(); // @reactVersion >= 18.0 - it('should not allow a root node to be collapsed', () => { + it('should not allow a root node to be collapsed', async () => { const Component = () =>
Hi
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -63,16 +63,16 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should properly handle a root with no visible nodes', () => { + it('should properly handle a root with no visible nodes', async () => { const Root = ({children}) => children; - act(() => render({null})); + await act(() => render({null})); expect(store).toMatchInlineSnapshot(` [root] `); - act(() => render(
)); + await act(() => render(
)); expect(store).toMatchInlineSnapshot(`[root]`); }); @@ -82,20 +82,24 @@ describe('Store', () => { // I'mnot yet sure of how to reduce the GitHub reported production case to a test though. // See https://github.com/facebook/react/issues/21445 // @reactVersion >= 18.0 - it('should handle when a component mounts before its owner', () => { + it('should handle when a component mounts before its owner', async () => { const promise = new Promise(resolve => {}); let Dynamic = null; const Owner = () => { Dynamic = ; - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } }; const Parent = () => { return Dynamic; }; const Child = () => null; - act(() => + await act(() => render( <> @@ -114,11 +118,11 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should handle multibyte character strings', () => { + it('should handle multibyte character strings', async () => { const Component = () => null; Component.displayName = '🟩💜🔵'; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] <🟩💜🔵> @@ -126,7 +130,7 @@ describe('Store', () => { }); describe('StrictMode compliance', () => { - it('should mark strict root elements as strict', () => { + it('should mark strict root elements as strict', async () => { const App = () => ; const Component = () => null; @@ -134,7 +138,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container, { unstable_strictMode: true, }); - act(() => { + await act(() => { root.render(); }); @@ -143,13 +147,13 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should mark non strict root elements as not strict', () => { + it('should mark non strict root elements as not strict', async () => { const App = () => ; const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); @@ -157,7 +161,7 @@ describe('Store', () => { expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true); }); - it('should mark StrictMode subtree elements as strict', () => { + it('should mark StrictMode subtree elements as strict', async () => { const App = () => ( @@ -167,7 +171,7 @@ describe('Store', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); @@ -182,7 +186,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support mount and update operations', () => { + it('should support mount and update operations', async () => { const Grandparent = ({count}) => ( @@ -193,7 +197,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -209,7 +213,7 @@ describe('Store', () => { `); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -221,14 +225,14 @@ describe('Store', () => { `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('should support mount and update operations for multiple roots (legacy render)', () => { + it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -236,7 +240,7 @@ describe('Store', () => { const containerA = document.createElement('div'); const containerB = document.createElement('div'); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -252,7 +256,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -268,7 +272,7 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -278,12 +282,12 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots (createRoot)', () => { + it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -294,7 +298,7 @@ describe('Store', () => { const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -310,7 +314,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -326,7 +330,7 @@ describe('Store', () => { `); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -336,12 +340,12 @@ describe('Store', () => { `); - act(() => rootA.unmount()); + await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should filter DOM nodes from the store tree', () => { + it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => (
@@ -357,7 +361,7 @@ describe('Store', () => { ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -369,10 +373,14 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should display Suspense nodes properly in various states', () => { + it('should display Suspense nodes properly in various states', async () => { const Loading = () =>
Loading...
; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
Hello
; @@ -390,7 +398,7 @@ describe('Store', () => { ); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -399,7 +407,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { render(); }); expect(store).toMatchInlineSnapshot(` @@ -412,11 +420,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support nested Suspense nodes', () => { + it('should support nested Suspense nodes', async () => { const Component = () => null; const Loading = () =>
Loading...
; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Wrapper = ({ @@ -451,7 +463,7 @@ describe('Store', () => { ); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { â–¾ `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); const rendererID = getRendererID(); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, @@ -628,7 +640,7 @@ describe('Store', () => { `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, @@ -642,7 +654,7 @@ describe('Store', () => { â–¾ `); - act(() => + await act(() => render( { â–¾ `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, @@ -679,7 +691,7 @@ describe('Store', () => { `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, @@ -700,7 +712,7 @@ describe('Store', () => { `); - act(() => + await act(() => render( { `); }); - it('should display a partially rendered SuspenseList', () => { + it('should display a partially rendered SuspenseList', async () => { const Loading = () =>
Loading...
; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
Hello
; @@ -747,7 +763,7 @@ describe('Store', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); expect(store).toMatchInlineSnapshot(` @@ -759,7 +775,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { root.render(); }); expect(store).toMatchInlineSnapshot(` @@ -774,7 +790,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support collapsing parts of the tree', () => { + it('should support collapsing parts of the tree', async () => { const Grandparent = ({count}) => ( @@ -785,7 +801,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -801,7 +817,7 @@ describe('Store', () => { const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(4); - act(() => store.toggleIsCollapsed(parentOneID, true)); + await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -811,7 +827,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentTwoID, true)); + await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -819,7 +835,7 @@ describe('Store', () => { â–¸ `); - act(() => store.toggleIsCollapsed(parentOneID, false)); + await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -829,13 +845,13 @@ describe('Store', () => { â–¸ `); - act(() => store.toggleIsCollapsed(grandparentID, true)); + await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => store.toggleIsCollapsed(grandparentID, false)); + await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -847,7 +863,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support reordering of children', () => { + it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; @@ -856,7 +872,7 @@ describe('Store', () => { const foo = ; const bar = ; - act(() => render({[foo, bar]})); + await act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -867,7 +883,7 @@ describe('Store', () => { `); - act(() => render({[bar, foo]})); + await act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -878,13 +894,17 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), + ); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -903,12 +923,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support mount and update operations', () => { + it('should support mount and update operations', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => + await act(() => render( @@ -922,7 +942,7 @@ describe('Store', () => { â–¸ `); - act(() => + await act(() => render( @@ -936,14 +956,14 @@ describe('Store', () => { â–¸ `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('should support mount and update operations for multiple roots (legacy render)', () => { + it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -951,7 +971,7 @@ describe('Store', () => { const containerA = document.createElement('div'); const containerB = document.createElement('div'); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -962,7 +982,7 @@ describe('Store', () => { â–¸ `); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -973,18 +993,18 @@ describe('Store', () => { â–¸ `); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots (createRoot)', () => { + it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -995,7 +1015,7 @@ describe('Store', () => { const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -1006,7 +1026,7 @@ describe('Store', () => { â–¸ `); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -1017,18 +1037,18 @@ describe('Store', () => { â–¸ `); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => rootA.unmount()); + await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should filter DOM nodes from the store tree', () => { + it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => (
@@ -1044,13 +1064,15 @@ describe('Store', () => { ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -1058,7 +1080,9 @@ describe('Store', () => { â–¸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -1069,10 +1093,14 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should display Suspense nodes properly in various states', () => { + it('should display Suspense nodes properly in various states', async () => { const Loading = () =>
Loading...
; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
Hello
; @@ -1090,15 +1118,19 @@ describe('Store', () => { ); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); // This test isn't meaningful unless we expand the suspended tree - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(2), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -1107,7 +1139,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { render(); }); expect(store).toMatchInlineSnapshot(` @@ -1120,7 +1152,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support expanding parts of the tree', () => { + it('should support expanding parts of the tree', async () => { const Grandparent = ({count}) => ( @@ -1131,7 +1163,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1139,7 +1171,7 @@ describe('Store', () => { const grandparentID = store.getElementIDAtIndex(0); - act(() => store.toggleIsCollapsed(grandparentID, false)); + await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1150,7 +1182,7 @@ describe('Store', () => { const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(2); - act(() => store.toggleIsCollapsed(parentOneID, false)); + await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1160,7 +1192,7 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(parentTwoID, false)); + await act(() => store.toggleIsCollapsed(parentTwoID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1172,7 +1204,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentOneID, true)); + await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1182,7 +1214,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentTwoID, true)); + await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1190,7 +1222,7 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(grandparentID, true)); + await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1198,7 +1230,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support expanding deep parts of the tree', () => { + it('should support expanding deep parts of the tree', async () => { const Wrapper = ({forwardedRef}) => ( ); @@ -1211,7 +1243,7 @@ describe('Store', () => { const ref = React.createRef(); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1219,7 +1251,7 @@ describe('Store', () => { const deepestedNodeID = agent.getIDForHostInstance(ref.current); - act(() => store.toggleIsCollapsed(deepestedNodeID, false)); + await act(() => store.toggleIsCollapsed(deepestedNodeID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1231,13 +1263,13 @@ describe('Store', () => { const rootID = store.getElementIDAtIndex(0); - act(() => store.toggleIsCollapsed(rootID, true)); + await act(() => store.toggleIsCollapsed(rootID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(rootID, false)); + await act(() => store.toggleIsCollapsed(rootID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1249,14 +1281,14 @@ describe('Store', () => { const id = store.getElementIDAtIndex(1); - act(() => store.toggleIsCollapsed(id, true)); + await act(() => store.toggleIsCollapsed(id, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ ▸ `); - act(() => store.toggleIsCollapsed(id, false)); + await act(() => store.toggleIsCollapsed(id, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1268,7 +1300,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support reordering of children', () => { + it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; @@ -1277,19 +1309,21 @@ describe('Store', () => { const foo = ; const bar = ; - act(() => render({[foo, bar]})); + await act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => render({[bar, foo]})); + await act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1297,7 +1331,7 @@ describe('Store', () => { ▸ `); - act(() => { + await act(() => { store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); }); @@ -1311,7 +1345,9 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), + ); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1319,7 +1355,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should not add new nodes when suspense is toggled', () => { + it('should not add new nodes when suspense is toggled', async () => { const SuspenseTree = () => { return ( Loading outer}> @@ -1332,14 +1368,18 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1350,7 +1390,7 @@ describe('Store', () => { const rendererID = getRendererID(); const suspenseID = store.getElementIDAtIndex(1); - act(() => + await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, @@ -1364,7 +1404,7 @@ describe('Store', () => { `); - act(() => + await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, @@ -1386,7 +1426,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support a single root with a single child', () => { + it('should support a single root with a single child', async () => { const Grandparent = () => ( @@ -1396,7 +1436,7 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => render()); + await act(() => render()); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); @@ -1404,12 +1444,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support multiple roots with one children each', () => { + it('should support multiple roots with one children each', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => { + await act(() => { render(); render(); }); @@ -1420,12 +1460,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support a single root with multiple top level children', () => { + it('should support a single root with multiple top level children', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => + await act(() => render( @@ -1440,12 +1480,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support multiple roots with multiple top level children', () => { + it('should support multiple roots with multiple top level children', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => { + await act(() => { render( @@ -1472,7 +1512,7 @@ describe('Store', () => { // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('detects and updates profiling support based on the attached roots (legacy render)', () => { + it('detects and updates profiling support based on the attached roots (legacy render)', async () => { const Component = () => null; const containerA = document.createElement('div'); @@ -1480,19 +1520,19 @@ describe('Store', () => { expect(store.rootSupportsBasicProfiling).toBe(false); - act(() => legacyRender(, containerA)); + await act(() => legacyRender(, containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => legacyRender(, containerB)); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => legacyRender(, containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18 - it('detects and updates profiling support based on the attached roots (createRoot)', () => { + it('detects and updates profiling support based on the attached roots (createRoot)', async () => { const Component = () => null; const containerA = document.createElement('div'); @@ -1503,26 +1543,26 @@ describe('Store', () => { expect(store.rootSupportsBasicProfiling).toBe(false); - act(() => rootA.render()); + await act(() => rootA.render()); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => rootB.render()); - act(() => rootA.unmount()); + await act(() => rootB.render()); + await act(() => rootA.unmount()); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18.0 - it('should properly serialize non-string key values', () => { + it('should properly serialize non-string key values', async () => { const Child = () => null; // Bypass React element's automatic stringifying of keys intentionally. // This is pretty hacky. const fauxElement = Object.assign({}, , {key: 123}); - act(() => render([fauxElement])); + await act(() => render([fauxElement])); expect(store).toMatchInlineSnapshot(` [root] @@ -1581,12 +1621,12 @@ describe('Store', () => { ); // Render once to start fetching the lazy component - act(() => render()); + await act(() => render()); await Promise.resolve(); // Render again after it resolves - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1638,7 +1678,7 @@ describe('Store', () => { const container = document.createElement('div'); // Render once to start fetching the lazy component - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1649,7 +1689,7 @@ describe('Store', () => { await Promise.resolve(); // Render again after it resolves - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1659,7 +1699,7 @@ describe('Store', () => { `); // Render again to unmount it - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1673,7 +1713,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1684,7 +1724,7 @@ describe('Store', () => { await Promise.resolve(); // Render again after it resolves - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1694,7 +1734,7 @@ describe('Store', () => { `); // Render again to unmount it - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1709,7 +1749,7 @@ describe('Store', () => { const container = document.createElement('div'); // Render once to start fetching the lazy component - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1718,7 +1758,7 @@ describe('Store', () => { `); // Render again to unmount it before it finishes loading - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1733,7 +1773,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1742,7 +1782,7 @@ describe('Store', () => { `); // Render again to unmount it before it finishes loading - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1753,15 +1793,15 @@ describe('Store', () => { describe('inline errors and warnings', () => { // @reactVersion >= 18.0 - it('during render are counted', () => { + it('during render are counted', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1770,8 +1810,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1782,7 +1822,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('during layout get counted', () => { + it('during layout get counted', async () => { function Example() { React.useLayoutEffect(() => { console.error('test-only: layout error'); @@ -1791,8 +1831,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1801,8 +1841,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1818,7 +1858,7 @@ describe('Store', () => { } // @reactVersion >= 18.0 - it('are counted (after no delay)', () => { + it('are counted (after no delay)', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); @@ -1827,8 +1867,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => { + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => { render(); }, false); }); @@ -1839,12 +1879,12 @@ describe('Store', () => { ✕⚠ `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('are flushed early when there is a new commit', () => { + it('are flushed early when there is a new commit', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); @@ -1890,7 +1930,7 @@ describe('Store', () => { `); }); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); }); @@ -1898,7 +1938,7 @@ describe('Store', () => { // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088 // The warning is moved to the Child instead of the Parent. // @reactVersion >= 19.0.1 - it('from react get counted [React >= 19.0.1]', () => { + it('from react get counted [React >= 19.0.1]', async () => { function Example() { return []; } @@ -1923,7 +1963,7 @@ describe('Store', () => { // @reactVersion >= 18.0 // @reactVersion < 19.0 - it('from react get counted [React 18.x]', () => { + it('from react get counted [React 18.x]', async () => { function Example() { return []; } @@ -1947,15 +1987,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for the whole app', () => { + it('can be cleared for the whole app', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -1988,15 +2028,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for particular Fiber (only warnings)', () => { + it('can be cleared for particular Fiber (only warnings)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2033,15 +2073,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for a particular Fiber (only errors)', () => { + it('can be cleared for a particular Fiber (only errors)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2078,7 +2118,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('are updated when fibers are removed from the tree', () => { + it('are updated when fibers are removed from the tree', async () => { function ComponentWithWarning() { console.warn('test-only: render warning'); return null; @@ -2093,8 +2133,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2112,8 +2152,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2129,8 +2169,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2144,8 +2184,8 @@ describe('Store', () => { ⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(`[root]`); expect(store.componentWithErrorCount).toBe(0); diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 5f00af92bf5..c0ab51a1718 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -509,7 +509,11 @@ describe('Store component filters', () => { const Component = ({shouldSuspend}) => { if (shouldSuspend) { - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } } return null; }; diff --git a/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js b/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js index 585499fd81a..759ce795903 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js @@ -522,7 +522,11 @@ describe('StoreStress (Legacy Mode)', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Root = ({children}) => { @@ -1144,7 +1148,11 @@ describe('StoreStress (Legacy Mode)', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const MaybeSuspend = ({children, suspend}) => { diff --git a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js index 8dd4ce42843..4389f78cd26 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js @@ -38,7 +38,7 @@ describe('StoreStressConcurrent', () => { // This is a stress test for the tree mount/update/unmount traversal. // It renders different trees that should produce the same output. // @reactVersion >= 18.0 - it('should handle a stress test with different tree operations (Concurrent Mode)', () => { + it('should handle a stress test with different tree operations (Concurrent Mode)', async () => { let setShowX; const A = () => 'a'; const B = () => 'b'; @@ -151,26 +151,26 @@ describe('StoreStressConcurrent', () => { root = ReactDOMClient.createRoot(container); // Verify mounting 'abcde'. - act(() => root.render({cases[i]})); + await act(() => root.render({cases[i]})); expect(container.textContent).toMatch('abcde'); expect(print(store)).toEqual(snapshotForABCDE); // Verify switching to 'abxde'. - act(() => { + await act(() => { setShowX(true); }); expect(container.textContent).toMatch('abxde'); expect(print(store)).toBe(snapshotForABXDE); // Verify switching back to 'abcde'. - act(() => { + await act(() => { setShowX(false); }); expect(container.textContent).toMatch('abcde'); expect(print(store)).toBe(snapshotForABCDE); // Clean up. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -180,19 +180,19 @@ describe('StoreStressConcurrent', () => { root = ReactDOMClient.createRoot(container); for (let i = 0; i < cases.length; i++) { // Verify mounting 'abcde'. - act(() => root.render({cases[i]})); + await act(() => root.render({cases[i]})); expect(container.textContent).toMatch('abcde'); expect(print(store)).toEqual(snapshotForABCDE); // Verify switching to 'abxde'. - act(() => { + await act(() => { setShowX(true); }); expect(container.textContent).toMatch('abxde'); expect(print(store)).toBe(snapshotForABXDE); // Verify switching back to 'abcde'. - act(() => { + await act(() => { setShowX(false); }); expect(container.textContent).toMatch('abcde'); @@ -204,7 +204,7 @@ describe('StoreStressConcurrent', () => { }); // @reactVersion >= 18.0 - it('should handle stress test with reordering (Concurrent Mode)', () => { + it('should handle stress test with reordering (Concurrent Mode)', async () => { const A = () => 'a'; const B = () => 'b'; const C = () => 'c'; @@ -245,10 +245,10 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); // We snapshot each step once so it doesn't regress. snapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -316,13 +316,13 @@ describe('StoreStressConcurrent', () => { for (let j = 0; j < steps.length; j++) { container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); expect(print(store)).toMatch(snapshots[i]); - act(() => root.render({steps[j]})); + await act(() => root.render({steps[j]})); expect(print(store)).toMatch(snapshots[j]); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); expect(print(store)).toMatch(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -332,7 +332,7 @@ describe('StoreStressConcurrent', () => { for (let j = 0; j < steps.length; j++) { container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render(
{steps[i]}
@@ -340,7 +340,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[i]); - act(() => + await act(() => root.render(
{steps[j]}
@@ -348,7 +348,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[j]); - act(() => + await act(() => root.render(
{steps[i]}
@@ -356,7 +356,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -392,7 +392,11 @@ describe('StoreStressConcurrent', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Root = ({children}) => { @@ -405,7 +409,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -416,7 +420,7 @@ describe('StoreStressConcurrent', () => { ); // We snapshot each step once so it doesn't regress.d snapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -507,7 +511,7 @@ describe('StoreStressConcurrent', () => { // 2. Verify check Suspense can render same steps as initial fallback content. for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -521,7 +525,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toEqual(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -531,7 +535,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -542,7 +546,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -554,7 +558,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -565,7 +569,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -576,7 +580,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -591,7 +595,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -607,7 +611,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -622,7 +626,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -633,7 +637,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -644,7 +648,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -660,7 +664,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -671,7 +675,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -682,7 +686,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -697,7 +701,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -709,7 +713,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -724,7 +728,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -735,7 +739,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -772,7 +776,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[i]); // Trigger actual fallback. - act(() => + await act(() => root.render( @@ -788,7 +792,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[j]); // Force fallback while we're in fallback mode. - act(() => { + await act(() => { bridge.send('overrideSuspense', { id: suspenseID, rendererID: store.getRendererIDForElement(suspenseID), @@ -799,7 +803,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[j]); // Switch to primary mode. - act(() => + await act(() => root.render( @@ -859,7 +863,11 @@ describe('StoreStressConcurrent', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const MaybeSuspend = ({children, suspend}) => { @@ -890,7 +898,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -903,7 +911,7 @@ describe('StoreStressConcurrent', () => { ); // We snapshot each step once so it doesn't regress. snapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -913,7 +921,7 @@ describe('StoreStressConcurrent', () => { const fallbackSnapshots = []; for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -928,7 +936,7 @@ describe('StoreStressConcurrent', () => { ); // We snapshot each step once so it doesn't regress. fallbackSnapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -1046,7 +1054,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1059,7 +1067,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1073,7 +1081,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1086,7 +1094,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1097,7 +1105,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1115,7 +1123,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1134,7 +1142,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1152,7 +1160,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1163,7 +1171,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1176,7 +1184,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1190,7 +1198,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1203,7 +1211,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1214,7 +1222,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1227,7 +1235,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1241,7 +1249,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1254,7 +1262,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1265,7 +1273,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1304,7 +1312,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[i]); // Trigger actual fallback. - act(() => + await act(() => root.render( @@ -1318,7 +1326,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(fallbackSnapshots[j]); // Force fallback while we're in fallback mode. - act(() => { + await act(() => { bridge.send('overrideSuspense', { id: suspenseID, rendererID: store.getRendererIDForElement(suspenseID), @@ -1329,7 +1337,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(fallbackSnapshots[j]); // Switch to primary mode. - act(() => + await act(() => root.render( @@ -1355,7 +1363,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } diff --git a/packages/react-devtools-shared/src/devtools/cache.js b/packages/react-devtools-shared/src/devtools/cache.js index eda30ed56ae..5ed2133b068 100644 --- a/packages/react-devtools-shared/src/devtools/cache.js +++ b/packages/react-devtools-shared/src/devtools/cache.js @@ -7,7 +7,12 @@ * @flow */ -import type {ReactContext, Thenable} from 'shared/ReactTypes'; +import type { + ReactContext, + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import * as React from 'react'; import {createContext} from 'react'; @@ -26,27 +31,6 @@ import {createContext} from 'react'; export type {Thenable}; -interface Suspender { - then(resolve: () => mixed, reject: () => mixed): mixed; -} - -type PendingResult = { - status: 0, - value: Suspender, -}; - -type ResolvedResult = { - status: 1, - value: Value, -}; - -type RejectedResult = { - status: 2, - value: mixed, -}; - -type Result = PendingResult | ResolvedResult | RejectedResult; - export type Resource = { clear(): void, invalidate(Key): void, @@ -55,10 +39,6 @@ export type Resource = { write(Key, Value): void, }; -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - let readContext; if (typeof React.use === 'function') { readContext = function (Context: ReactContext) { @@ -115,33 +95,25 @@ function accessResult( fetch: Input => Thenable, input: Input, key: Key, -): Result { +): Thenable { const entriesForResource = getEntriesForResource(resource); const entry = entriesForResource.get(key); if (entry === undefined) { const thenable = fetch(input); thenable.then( value => { - if (newResult.status === Pending) { - const resolvedResult: ResolvedResult = (newResult: any); - resolvedResult.status = Resolved; - resolvedResult.value = value; - } + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; }, error => { - if (newResult.status === Pending) { - const rejectedResult: RejectedResult = (newResult: any); - rejectedResult.status = Rejected; - rejectedResult.value = error; - } + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; }, ); - const newResult: PendingResult = { - status: Pending, - value: thenable, - }; - entriesForResource.set(key, newResult); - return newResult; + entriesForResource.set(key, thenable); + return thenable; } else { return entry; } @@ -167,23 +139,22 @@ export function createResource( readContext(CacheContext); const key = hashInput(input); - const result: Result = accessResult(resource, fetch, input, key); + const result: Thenable = accessResult(resource, fetch, input, key); + if (typeof React.use === 'function') { + return React.use(result); + } + switch (result.status) { - case Pending: { - const suspender = result.value; - throw suspender; - } - case Resolved: { + case 'fulfilled': { const value = result.value; return value; } - case Rejected: { - const error = result.value; + case 'rejected': { + const error = result.reason; throw error; } default: - // Should be unreachable - return (undefined: any); + throw result; } }, @@ -198,12 +169,13 @@ export function createResource( write(key: Key, value: Value): void { const entriesForResource = getEntriesForResource(resource); - const resolvedResult: ResolvedResult = { - status: Resolved, + const fulfilledThenable: FulfilledThenable = (Promise.resolve( value, - }; + ): any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; - entriesForResource.set(key, resolvedResult); + entriesForResource.set(key, fulfilledThenable); }, }; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js index c7821991220..44950acddf7 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -20,7 +20,7 @@ import {getMetaValueLabel, serializeHooksForCopy} from '../utils'; import Store from '../../store'; import styles from './InspectedElementHooksTree.css'; import {meta} from '../../../hydration'; -import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation'; import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import isArray from 'react-devtools-shared/src/isArray'; diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js index eca76be605b..6da066a1103 100644 --- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js +++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js @@ -7,54 +7,46 @@ * @flow */ -import type {Wakeable} from 'shared/ReactTypes'; +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import type {GitHubIssue} from './githubAPI'; +import * as React from 'react'; + import {unstable_getCacheForType as getCacheForType} from 'react'; import {searchGitHubIssues} from './githubAPI'; const API_TIMEOUT = 3000; - -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - -type PendingRecord = { - status: 0, - value: Wakeable, -}; - -type ResolvedRecord = { - status: 1, - value: T, -}; - -type RejectedRecord = { - status: 2, - value: null, -}; - -type Record = PendingRecord | ResolvedRecord | RejectedRecord; - -function readRecord(record: Record): ResolvedRecord | RejectedRecord { - if (record.status === Resolved) { - // This is just a type refinement. - return record; - } else if (record.status === Rejected) { - // This is just a type refinement. - return record; +function readRecord(record: Thenable): T | null { + if (typeof React.use === 'function') { + try { + return React.use(record); + } catch (x) { + if (x === null) { + return null; + } + throw x; + } + } + if (record.status === 'fulfilled') { + return record.value; + } else if (record.status === 'rejected') { + return null; } else { - throw record.value; + throw record; } } -type GitHubIssueMap = Map>; +type GitHubIssueMap = Map>; function createMap(): GitHubIssueMap { return new Map(); } -function getRecordMap(): Map> { +function getRecordMap(): Map> { return getCacheForType(createMap); } @@ -65,10 +57,15 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null { let record = map.get(errorMessage); if (!record) { - const callbacks = new Set<() => mixed>(); - const wakeable: Wakeable = { - then(callback: () => mixed) { + const callbacks = new Set<(value: any) => mixed>(); + const rejectCallbacks = new Set<(reason: mixed) => mixed>(); + const thenable: Thenable = { + status: 'pending', + value: null, + reason: null, + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) { callbacks.add(callback); + rejectCallbacks.add(reject); }, // Optional property used by Timeline: @@ -76,13 +73,17 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null { }; const wake = () => { // This assumes they won't throw. - callbacks.forEach(callback => callback()); + callbacks.forEach(callback => callback((thenable: any).value)); callbacks.clear(); + rejectCallbacks.clear(); }; - const newRecord: Record = (record = { - status: Pending, - value: wakeable, - }); + const wakeRejections = () => { + // This assumes they won't throw. + rejectCallbacks.forEach(callback => callback((thenable: any).reason)); + rejectCallbacks.clear(); + callbacks.clear(); + }; + record = thenable; let didTimeout = false; @@ -93,41 +94,40 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null { } if (maybeItem) { - const resolvedRecord = - ((newRecord: any): ResolvedRecord); - resolvedRecord.status = Resolved; - resolvedRecord.value = maybeItem; + const fulfilledThenable: FulfilledThenable = + (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = maybeItem; + wake(); } else { - const notFoundRecord = ((newRecord: any): RejectedRecord); - notFoundRecord.status = Rejected; - notFoundRecord.value = null; + const notFoundThenable: RejectedThenable = + (thenable: any); + notFoundThenable.status = 'rejected'; + notFoundThenable.reason = null; + wakeRejections(); } - - wake(); }) .catch(error => { - const thrownRecord = ((newRecord: any): RejectedRecord); - thrownRecord.status = Rejected; - thrownRecord.value = null; - - wake(); + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = null; + wakeRejections(); }); // Only wait a little while for GitHub results before showing a fallback. setTimeout(() => { didTimeout = true; - const timedoutRecord = ((newRecord: any): RejectedRecord); - timedoutRecord.status = Rejected; - timedoutRecord.value = null; - - wake(); + const timedoutThenable: RejectedThenable = (thenable: any); + timedoutThenable.status = 'rejected'; + timedoutThenable.reason = null; + wakeRejections(); }, API_TIMEOUT); map.set(errorMessage, record); } - const response = readRecord(record).value; + const response = readRecord(record); return response; } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js index ed852c85c77..e5c8f9c0fa6 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js @@ -21,10 +21,8 @@ import ButtonIcon from '../ButtonIcon'; import {InspectedElementContext} from '../Components/InspectedElementContext'; import {StoreContext} from '../context'; -import { - getAlreadyLoadedHookNames, - getHookSourceLocationKey, -} from 'react-devtools-shared/src/hookNamesCache'; +import {getAlreadyLoadedHookNames} from 'react-devtools-shared/src/hookNamesCache'; +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation'; import Toggle from '../Toggle'; import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks'; import type {ChangeDescription} from './types'; diff --git a/packages/react-devtools-shared/src/dynamicImportCache.js b/packages/react-devtools-shared/src/dynamicImportCache.js index 8150b7fddf7..3b48a8459d1 100644 --- a/packages/react-devtools-shared/src/dynamicImportCache.js +++ b/packages/react-devtools-shared/src/dynamicImportCache.js @@ -9,30 +9,15 @@ import {__DEBUG__} from 'react-devtools-shared/src/constants'; -import type {Thenable, Wakeable} from 'shared/ReactTypes'; +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; -const TIMEOUT = 30000; - -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - -type PendingRecord = { - status: 0, - value: Wakeable, -}; - -type ResolvedRecord = { - status: 1, - value: T, -}; - -type RejectedRecord = { - status: 2, - value: null, -}; +import * as React from 'react'; -type Record = PendingRecord | ResolvedRecord | RejectedRecord; +const TIMEOUT = 30000; type Module = any; type ModuleLoaderFunction = () => Thenable; @@ -42,16 +27,23 @@ type ModuleLoaderFunction = () => Thenable; // Modules are static anyway. const moduleLoaderFunctionToModuleMap: Map = new Map(); - -function readRecord(record: Record): ResolvedRecord | RejectedRecord { - if (record.status === Resolved) { - // This is just a type refinement. - return record; - } else if (record.status === Rejected) { - // This is just a type refinement. - return record; +function readRecord(record: Thenable): T | null { + if (typeof React.use === 'function') { + try { + return React.use(record); + } catch (x) { + if (x === null) { + return null; + } + throw x; + } + } + if (record.status === 'fulfilled') { + return record.value; + } else if (record.status === 'rejected') { + return null; } else { - throw record.value; + throw record; } } @@ -66,10 +58,15 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module { } if (!record) { - const callbacks = new Set<() => mixed>(); - const wakeable: Wakeable = { - then(callback: () => mixed) { + const callbacks = new Set<(value: any) => mixed>(); + const rejectCallbacks = new Set<(reason: mixed) => mixed>(); + const thenable: Thenable = { + status: 'pending', + value: null, + reason: null, + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) { callbacks.add(callback); + rejectCallbacks.add(reject); }, // Optional property used by Timeline: @@ -85,12 +82,21 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module { // This assumes they won't throw. callbacks.forEach(callback => callback()); callbacks.clear(); + rejectCallbacks.clear(); }; + const wakeRejections = () => { + if (timeoutID) { + clearTimeout(timeoutID); + timeoutID = null; + } - const newRecord: Record = (record = { - status: Pending, - value: wakeable, - }); + // This assumes they won't throw. + rejectCallbacks.forEach(callback => callback((thenable: any).reason)); + rejectCallbacks.clear(); + callbacks.clear(); + }; + + record = thenable; let didTimeout = false; @@ -106,9 +112,9 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module { return; } - const resolvedRecord = ((newRecord: any): ResolvedRecord); - resolvedRecord.status = Resolved; - resolvedRecord.value = module; + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = module; wake(); }, @@ -125,11 +131,11 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module { console.log(error); - const thrownRecord = ((newRecord: any): RejectedRecord); - thrownRecord.status = Rejected; - thrownRecord.value = null; + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; - wake(); + wakeRejections(); }, ); @@ -145,17 +151,17 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module { didTimeout = true; - const timedoutRecord = ((newRecord: any): RejectedRecord); - timedoutRecord.status = Rejected; - timedoutRecord.value = null; + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = null; - wake(); + wakeRejections(); }, TIMEOUT); moduleLoaderFunctionToModuleMap.set(moduleLoaderFunction, record); } // $FlowFixMe[underconstrained-implicit-instantiation] - const response = readRecord(record).value; + const response = readRecord(record); return response; } diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js index e0d4b1075c2..f85bb3c6d8e 100644 --- a/packages/react-devtools-shared/src/hookNamesCache.js +++ b/packages/react-devtools-shared/src/hookNamesCache.js @@ -10,49 +10,40 @@ import {__DEBUG__} from 'react-devtools-shared/src/constants'; import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; -import type {Thenable, Wakeable} from 'shared/ReactTypes'; +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import type { Element, HookNames, - HookSourceLocationKey, } from 'react-devtools-shared/src/frontend/types'; -import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks'; import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext'; + +import * as React from 'react'; + import {withCallbackPerfMeasurements} from './PerformanceLoggingUtils'; import {logEvent} from './Logger'; const TIMEOUT = 30000; - -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - -type PendingRecord = { - status: 0, - value: Wakeable, -}; - -type ResolvedRecord = { - status: 1, - value: T, -}; - -type RejectedRecord = { - status: 2, - value: null, -}; - -type Record = PendingRecord | ResolvedRecord | RejectedRecord; - -function readRecord(record: Record): ResolvedRecord | RejectedRecord { - if (record.status === Resolved) { - // This is just a type refinement. - return record; - } else if (record.status === Rejected) { - // This is just a type refinement. - return record; +function readRecord(record: Thenable): T | null { + if (typeof React.use === 'function') { + try { + return React.use(record); + } catch (x) { + if (record.status === 'rejected') { + return null; + } + throw x; + } + } + if (record.status === 'fulfilled') { + return record.value; + } else if (record.status === 'rejected') { + return null; } else { - throw record.value; + throw record; } } @@ -65,16 +56,16 @@ type LoadHookNamesFunction = ( // Otherwise, refreshing the inspected element cache would also clear this cache. // TODO Rethink this if the React API constraints change. // See https://github.com/reactwg/react-18/discussions/25#discussioncomment-980435 -let map: WeakMap> = new WeakMap(); +let map: WeakMap> = new WeakMap(); export function hasAlreadyLoadedHookNames(element: Element): boolean { const record = map.get(element); - return record != null && record.status === Resolved; + return record != null && record.status === 'fulfilled'; } export function getAlreadyLoadedHookNames(element: Element): HookNames | null { const record = map.get(element); - if (record != null && record.status === Resolved) { + if (record != null && record.status === 'fulfilled') { return record.value; } return null; @@ -95,10 +86,15 @@ export function loadHookNames( } if (!record) { - const callbacks = new Set<() => mixed>(); - const wakeable: Wakeable = { - then(callback: () => mixed) { + const callbacks = new Set<(value: any) => mixed>(); + const rejectCallbacks = new Set<(reason: mixed) => mixed>(); + const thenable: Thenable = { + status: 'pending', + value: null, + reason: null, + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) { callbacks.add(callback); + rejectCallbacks.add(reject); }, // Optional property used by Timeline: @@ -117,7 +113,18 @@ export function loadHookNames( } // This assumes they won't throw. - callbacks.forEach(callback => callback()); + callbacks.forEach(callback => callback((thenable: any).value)); + callbacks.clear(); + rejectCallbacks.clear(); + }; + const wakeRejections = () => { + if (timeoutID) { + clearTimeout(timeoutID); + timeoutID = null; + } + // This assumes they won't throw. + rejectCallbacks.forEach(callback => callback((thenable: any).reason)); + rejectCallbacks.clear(); callbacks.clear(); }; @@ -132,10 +139,7 @@ export function loadHookNames( }); }; - const newRecord: Record = (record = { - status: Pending, - value: wakeable, - }); + record = thenable; withCallbackPerfMeasurements( 'loadHookNames', @@ -151,20 +155,24 @@ export function loadHookNames( } if (hookNames) { - const resolvedRecord = - ((newRecord: any): ResolvedRecord); - resolvedRecord.status = Resolved; - resolvedRecord.value = hookNames; + const fulfilledThenable: FulfilledThenable = + (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = hookNames; + status = 'success'; + resolvedHookNames = hookNames; + done(); + wake(); } else { - const notFoundRecord = ((newRecord: any): RejectedRecord); - notFoundRecord.status = Rejected; - notFoundRecord.value = null; + const notFoundThenable: RejectedThenable = + (thenable: any); + notFoundThenable.status = 'rejected'; + notFoundThenable.reason = null; + status = 'error'; + resolvedHookNames = hookNames; + done(); + wakeRejections(); } - - status = 'success'; - resolvedHookNames = hookNames; - done(); - wake(); }, function onError(error) { if (didTimeout) { @@ -177,13 +185,14 @@ export function loadHookNames( console.error(error); - const thrownRecord = ((newRecord: any): RejectedRecord); - thrownRecord.status = Rejected; - thrownRecord.value = null; + const rejectedThenable: RejectedThenable = + (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = null; status = 'error'; done(); - wake(); + wakeRejections(); }, ); @@ -197,13 +206,13 @@ export function loadHookNames( didTimeout = true; - const timedoutRecord = ((newRecord: any): RejectedRecord); - timedoutRecord.status = Rejected; - timedoutRecord.value = null; + const timedoutThenable: RejectedThenable = (thenable: any); + timedoutThenable.status = 'rejected'; + timedoutThenable.reason = null; status = 'timeout'; done(); - wake(); + wakeRejections(); }, TIMEOUT); }, handleLoadComplete, @@ -211,21 +220,10 @@ export function loadHookNames( map.set(element, record); } - const response = readRecord(record).value; + const response = readRecord(record); return response; } -export function getHookSourceLocationKey({ - fileName, - lineNumber, - columnNumber, -}: HookSource): HookSourceLocationKey { - if (fileName == null || lineNumber == null || columnNumber == null) { - throw Error('Hook source code location not found.'); - } - return `${fileName}:${lineNumber}:${columnNumber}`; -} - export function clearHookNamesCache(): void { map = new WeakMap(); } diff --git a/packages/react-devtools-shared/src/hookSourceLocation.js b/packages/react-devtools-shared/src/hookSourceLocation.js new file mode 100644 index 00000000000..a9a3d931bcf --- /dev/null +++ b/packages/react-devtools-shared/src/hookSourceLocation.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {HookSourceLocationKey} from 'react-devtools-shared/src/frontend/types'; +import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks'; + +export function getHookSourceLocationKey({ + fileName, + lineNumber, + columnNumber, +}: HookSource): HookSourceLocationKey { + if (fileName == null || lineNumber == null || columnNumber == null) { + throw Error('Hook source code location not found.'); + } + return `${fileName}:${lineNumber}:${columnNumber}`; +} diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/index.js b/packages/react-devtools-shared/src/hooks/parseHookNames/index.js index 3691f74e4ff..40725bc21fa 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/index.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/index.js @@ -12,6 +12,8 @@ import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; import type {HookNames} from 'react-devtools-shared/src/frontend/types'; import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext'; +import 'react'; + import {withAsyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils'; import WorkerizedParseSourceAndMetadata from './parseSourceAndMetadata.worker'; import typeof * as ParseSourceAndMetadataModule from './parseSourceAndMetadata'; diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js index 34f770ae005..27073a1a6d2 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js @@ -46,7 +46,7 @@ // and there is no need to convert runtime code to the original source. import {__DEBUG__} from 'react-devtools-shared/src/constants'; -import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation'; import {sourceMapIncludesSource} from '../SourceMapUtils'; import { withAsyncPerfMeasurements, diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js index 15423c12416..fde53857857 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js @@ -15,7 +15,7 @@ import LRU from 'lru-cache'; import {getHookName} from '../astUtils'; import {areSourceMapsAppliedToErrors} from '../ErrorTester'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; -import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation'; import {SourceMapMetadataConsumer} from '../SourceMapMetadataConsumer'; import { withAsyncPerfMeasurements, diff --git a/packages/react-devtools-shared/src/inspectedElementCache.js b/packages/react-devtools-shared/src/inspectedElementCache.js index 014edf22a7f..a7e3206db70 100644 --- a/packages/react-devtools-shared/src/inspectedElementCache.js +++ b/packages/react-devtools-shared/src/inspectedElementCache.js @@ -7,6 +7,8 @@ * @flow */ +import * as React from 'react'; + import { unstable_getCacheForType as getCacheForType, startTransition, @@ -16,7 +18,11 @@ import {inspectElement as inspectElementMutableSource} from 'react-devtools-shar import ElementPollingCancellationError from 'react-devtools-shared/src//errors/ElementPollingCancellationError'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; -import type {Wakeable} from 'shared/ReactTypes'; +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import type { Element, InspectedElement as InspectedElementFrontend, @@ -24,44 +30,27 @@ import type { InspectedElementPath, } from 'react-devtools-shared/src/frontend/types'; -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - -type PendingRecord = { - status: 0, - value: Wakeable, -}; - -type ResolvedRecord = { - status: 1, - value: T, -}; - -type RejectedRecord = { - status: 2, - value: Error | string, -}; - -type Record = PendingRecord | ResolvedRecord | RejectedRecord; - -function readRecord(record: Record): ResolvedRecord { - if (record.status === Resolved) { - // This is just a type refinement. - return record; +function readRecord(record: Thenable): T { + if (typeof React.use === 'function') { + return React.use(record); + } + if (record.status === 'fulfilled') { + return record.value; + } else if (record.status === 'rejected') { + throw record.reason; } else { - throw record.value; + throw record; } } -type InspectedElementMap = WeakMap>; +type InspectedElementMap = WeakMap>; type CacheSeedKey = () => InspectedElementMap; function createMap(): InspectedElementMap { return new WeakMap(); } -function getRecordMap(): WeakMap> { +function getRecordMap(): WeakMap> { return getCacheForType(createMap); } @@ -69,12 +58,15 @@ function createCacheSeed( element: Element, inspectedElement: InspectedElementFrontend, ): [CacheSeedKey, InspectedElementMap] { - const newRecord: Record = { - status: Resolved, + const thenable: FulfilledThenable = { + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) { + callback(thenable.value); + }, + status: 'fulfilled', value: inspectedElement, }; const map = createMap(); - map.set(element, newRecord); + map.set(element, thenable); return [createMap, map]; } @@ -91,10 +83,15 @@ export function inspectElement( const map = getRecordMap(); let record = map.get(element); if (!record) { - const callbacks = new Set<() => mixed>(); - const wakeable: Wakeable = { - then(callback: () => mixed) { + const callbacks = new Set<(value: any) => mixed>(); + const rejectCallbacks = new Set<(reason: mixed) => mixed>(); + const thenable: Thenable = { + status: 'pending', + value: null, + reason: null, + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) { callbacks.add(callback); + rejectCallbacks.add(reject); }, // Optional property used by Timeline: @@ -103,19 +100,24 @@ export function inspectElement( const wake = () => { // This assumes they won't throw. - callbacks.forEach(callback => callback()); + callbacks.forEach(callback => callback((thenable: any).value)); callbacks.clear(); + rejectCallbacks.clear(); }; - const newRecord: Record = (record = { - status: Pending, - value: wakeable, - }); + const wakeRejections = () => { + // This assumes they won't throw. + rejectCallbacks.forEach(callback => callback((thenable: any).reason)); + rejectCallbacks.clear(); + callbacks.clear(); + }; + record = thenable; const rendererID = store.getRendererIDForElement(element.id); if (rendererID == null) { - const rejectedRecord = ((newRecord: any): RejectedRecord); - rejectedRecord.status = Rejected; - rejectedRecord.value = new Error( + const rejectedThenable: RejectedThenable = + (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = new Error( `Could not inspect element with id "${element.id}". No renderer found.`, ); @@ -129,29 +131,29 @@ export function inspectElement( InspectedElementFrontend, InspectedElementResponseType, ]) => { - const resolvedRecord = - ((newRecord: any): ResolvedRecord); - resolvedRecord.status = Resolved; - resolvedRecord.value = inspectedElement; - + const fulfilledThenable: FulfilledThenable = + (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = inspectedElement; wake(); }, error => { console.error(error); - const rejectedRecord = ((newRecord: any): RejectedRecord); - rejectedRecord.status = Rejected; - rejectedRecord.value = error; + const rejectedThenable: RejectedThenable = + (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; - wake(); + wakeRejections(); }, ); map.set(element, record); } - const response = readRecord(record).value; + const response = readRecord(record); return response; } diff --git a/packages/react-devtools-timeline/src/timelineCache.js b/packages/react-devtools-timeline/src/timelineCache.js index b6e7e6076e2..a778ec9444a 100644 --- a/packages/react-devtools-timeline/src/timelineCache.js +++ b/packages/react-devtools-timeline/src/timelineCache.js @@ -7,46 +7,42 @@ * @flow */ -import type {Wakeable} from 'shared/ReactTypes'; +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import type {TimelineData} from './types'; -import {importFile as importFileWorker} from './import-worker'; - -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - -type PendingRecord = { - status: 0, - value: Wakeable, -}; - -type ResolvedRecord = { - status: 1, - value: T, -}; +import * as React from 'react'; -type RejectedRecord = { - status: 2, - value: Error, -}; - -type Record = PendingRecord | ResolvedRecord | RejectedRecord; +import {importFile as importFileWorker} from './import-worker'; // This is intentionally a module-level Map, rather than a React-managed one. // Otherwise, refreshing the inspected element cache would also clear this cache. // Profiler file contents are static anyway. -const fileNameToProfilerDataMap: Map> = new Map(); - -function readRecord(record: Record): ResolvedRecord | RejectedRecord { - if (record.status === Resolved) { - // This is just a type refinement. - return record; - } else if (record.status === Rejected) { - // This is just a type refinement. - return record; +const fileNameToProfilerDataMap: Map< + string, + Thenable, +> = new Map(); + +function readRecord(record: Thenable): T | Error { + if (typeof React.use === 'function') { + try { + return React.use(record); + } catch (x) { + if (record.status === 'rejected') { + return (record.reason: any); + } + throw x; + } + } + if (record.status === 'fulfilled') { + return record.value; + } else if (record.status === 'rejected') { + return (record.reason: any); } else { - throw record.value; + throw record; } } @@ -55,10 +51,15 @@ export function importFile(file: File): TimelineData | Error { let record = fileNameToProfilerDataMap.get(fileName); if (!record) { - const callbacks = new Set<() => mixed>(); - const wakeable: Wakeable = { - then(callback: () => mixed) { + const callbacks = new Set<(value: any) => mixed>(); + const rejectCallbacks = new Set<(reason: mixed) => mixed>(); + const thenable: Thenable = { + status: 'pending', + value: null, + reason: null, + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) { callbacks.add(callback); + rejectCallbacks.add(reject); }, // Optional property used by Timeline: @@ -67,37 +68,42 @@ export function importFile(file: File): TimelineData | Error { const wake = () => { // This assumes they won't throw. - callbacks.forEach(callback => callback()); + callbacks.forEach(callback => callback((thenable: any).value)); + callbacks.clear(); + rejectCallbacks.clear(); + }; + const wakeRejections = () => { + // This assumes they won't throw. + rejectCallbacks.forEach(callback => callback((thenable: any).reason)); + rejectCallbacks.clear(); callbacks.clear(); }; - const newRecord: Record = (record = { - status: Pending, - value: wakeable, - }); + record = thenable; importFileWorker(file).then(data => { switch (data.status) { case 'SUCCESS': - const resolvedRecord = - ((newRecord: any): ResolvedRecord); - resolvedRecord.status = Resolved; - resolvedRecord.value = data.processedData; + const fulfilledThenable: FulfilledThenable = + (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = data.processedData; + wake(); break; case 'INVALID_PROFILE_ERROR': case 'UNEXPECTED_ERROR': - const thrownRecord = ((newRecord: any): RejectedRecord); - thrownRecord.status = Rejected; - thrownRecord.value = data.error; + const rejectedThenable: RejectedThenable = + (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = data.error; + wakeRejections(); break; } - - wake(); }); fileNameToProfilerDataMap.set(fileName, record); } - const response = readRecord(record).value; + const response = readRecord(record); return response; }