1. let React;
    
  2. let ReactNoop;
    
  3. let Cache;
    
  4. let getCacheSignal;
    
  5. let Scheduler;
    
  6. let assertLog;
    
  7. let act;
    
  8. let Suspense;
    
  9. let Activity;
    
  10. let useCacheRefresh;
    
  11. let startTransition;
    
  12. let useState;
    
  13. let cache;
    
  14. 
    
  15. let getTextCache;
    
  16. let textCaches;
    
  17. let seededCache;
    
  18. 
    
  19. describe('ReactCache', () => {
    
  20.   beforeEach(() => {
    
  21.     jest.resetModules();
    
  22. 
    
  23.     React = require('react');
    
  24.     ReactNoop = require('react-noop-renderer');
    
  25.     Cache = React.unstable_Cache;
    
  26.     Scheduler = require('scheduler');
    
  27.     act = require('internal-test-utils').act;
    
  28.     Suspense = React.Suspense;
    
  29.     cache = React.cache;
    
  30.     Activity = React.unstable_Activity;
    
  31.     getCacheSignal = React.unstable_getCacheSignal;
    
  32.     useCacheRefresh = React.unstable_useCacheRefresh;
    
  33.     startTransition = React.startTransition;
    
  34.     useState = React.useState;
    
  35. 
    
  36.     const InternalTestUtils = require('internal-test-utils');
    
  37.     assertLog = InternalTestUtils.assertLog;
    
  38. 
    
  39.     textCaches = [];
    
  40.     seededCache = null;
    
  41. 
    
  42.     if (gate(flags => flags.enableCache)) {
    
  43.       getTextCache = cache(() => {
    
  44.         if (seededCache !== null) {
    
  45.           // Trick to seed a cache before it exists.
    
  46.           // TODO: Need a built-in API to seed data before the initial render (i.e.
    
  47.           // not a refresh because nothing has mounted yet).
    
  48.           const textCache = seededCache;
    
  49.           seededCache = null;
    
  50.           return textCache;
    
  51.         }
    
  52. 
    
  53.         const data = new Map();
    
  54.         const version = textCaches.length + 1;
    
  55.         const textCache = {
    
  56.           version,
    
  57.           data,
    
  58.           resolve(text) {
    
  59.             const record = data.get(text);
    
  60.             if (record === undefined) {
    
  61.               const newRecord = {
    
  62.                 status: 'resolved',
    
  63.                 value: text,
    
  64.                 cleanupScheduled: false,
    
  65.               };
    
  66.               data.set(text, newRecord);
    
  67.             } else if (record.status === 'pending') {
    
  68.               record.value.resolve();
    
  69.             }
    
  70.           },
    
  71.           reject(text, error) {
    
  72.             const record = data.get(text);
    
  73.             if (record === undefined) {
    
  74.               const newRecord = {
    
  75.                 status: 'rejected',
    
  76.                 value: error,
    
  77.                 cleanupScheduled: false,
    
  78.               };
    
  79.               data.set(text, newRecord);
    
  80.             } else if (record.status === 'pending') {
    
  81.               record.value.reject();
    
  82.             }
    
  83.           },
    
  84.         };
    
  85.         textCaches.push(textCache);
    
  86.         return textCache;
    
  87.       });
    
  88.     }
    
  89.   });
    
  90. 
    
  91.   function readText(text) {
    
  92.     const signal = getCacheSignal ? getCacheSignal() : null;
    
  93.     const textCache = getTextCache();
    
  94.     const record = textCache.data.get(text);
    
  95.     if (record !== undefined) {
    
  96.       if (!record.cleanupScheduled) {
    
  97.         // This record was seeded prior to the abort signal being available:
    
  98.         // schedule a cleanup function for it.
    
  99.         // TODO: Add ability to cleanup entries seeded w useCacheRefresh()
    
  100.         record.cleanupScheduled = true;
    
  101.         if (getCacheSignal) {
    
  102.           signal.addEventListener('abort', () => {
    
  103.             Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`);
    
  104.           });
    
  105.         }
    
  106.       }
    
  107.       switch (record.status) {
    
  108.         case 'pending':
    
  109.           throw record.value;
    
  110.         case 'rejected':
    
  111.           throw record.value;
    
  112.         case 'resolved':
    
  113.           return textCache.version;
    
  114.       }
    
  115.     } else {
    
  116.       Scheduler.log(`Cache miss! [${text}]`);
    
  117. 
    
  118.       let resolve;
    
  119.       let reject;
    
  120.       const thenable = new Promise((res, rej) => {
    
  121.         resolve = res;
    
  122.         reject = rej;
    
  123.       }).then(
    
  124.         value => {
    
  125.           if (newRecord.status === 'pending') {
    
  126.             newRecord.status = 'resolved';
    
  127.             newRecord.value = value;
    
  128.           }
    
  129.         },
    
  130.         error => {
    
  131.           if (newRecord.status === 'pending') {
    
  132.             newRecord.status = 'rejected';
    
  133.             newRecord.value = error;
    
  134.           }
    
  135.         },
    
  136.       );
    
  137.       thenable.resolve = resolve;
    
  138.       thenable.reject = reject;
    
  139. 
    
  140.       const newRecord = {
    
  141.         status: 'pending',
    
  142.         value: thenable,
    
  143.         cleanupScheduled: true,
    
  144.       };
    
  145.       textCache.data.set(text, newRecord);
    
  146. 
    
  147.       if (getCacheSignal) {
    
  148.         signal.addEventListener('abort', () => {
    
  149.           Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`);
    
  150.         });
    
  151.       }
    
  152.       throw thenable;
    
  153.     }
    
  154.   }
    
  155. 
    
  156.   function Text({text}) {
    
  157.     Scheduler.log(text);
    
  158.     return text;
    
  159.   }
    
  160. 
    
  161.   function AsyncText({text, showVersion}) {
    
  162.     const version = readText(text);
    
  163.     const fullText = showVersion ? `${text} [v${version}]` : text;
    
  164.     Scheduler.log(fullText);
    
  165.     return fullText;
    
  166.   }
    
  167. 
    
  168.   function seedNextTextCache(text) {
    
  169.     if (seededCache === null) {
    
  170.       seededCache = getTextCache();
    
  171.     }
    
  172.     seededCache.resolve(text);
    
  173.   }
    
  174. 
    
  175.   function resolveMostRecentTextCache(text) {
    
  176.     if (textCaches.length === 0) {
    
  177.       throw Error('Cache does not exist.');
    
  178.     } else {
    
  179.       // Resolve the most recently created cache. An older cache can by
    
  180.       // resolved with `textCaches[index].resolve(text)`.
    
  181.       textCaches[textCaches.length - 1].resolve(text);
    
  182.     }
    
  183.   }
    
  184. 
    
  185.   // @gate enableCacheElement && enableCache
    
  186.   test('render Cache component', async () => {
    
  187.     const root = ReactNoop.createRoot();
    
  188.     await act(() => {
    
  189.       root.render(<Cache>Hi</Cache>);
    
  190.     });
    
  191.     expect(root).toMatchRenderedOutput('Hi');
    
  192.   });
    
  193. 
    
  194.   // @gate enableCacheElement && enableCache
    
  195.   test('mount new data', async () => {
    
  196.     const root = ReactNoop.createRoot();
    
  197.     await act(() => {
    
  198.       root.render(
    
  199.         <Cache>
    
  200.           <Suspense fallback={<Text text="Loading..." />}>
    
  201.             <AsyncText text="A" />
    
  202.           </Suspense>
    
  203.         </Cache>,
    
  204.       );
    
  205.     });
    
  206.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  207.     expect(root).toMatchRenderedOutput('Loading...');
    
  208. 
    
  209.     await act(() => {
    
  210.       resolveMostRecentTextCache('A');
    
  211.     });
    
  212.     assertLog(['A']);
    
  213.     expect(root).toMatchRenderedOutput('A');
    
  214. 
    
  215.     await act(() => {
    
  216.       root.render('Bye');
    
  217.     });
    
  218.     // no cleanup: cache is still retained at the root
    
  219.     assertLog([]);
    
  220.     expect(root).toMatchRenderedOutput('Bye');
    
  221.   });
    
  222. 
    
  223.   // @gate enableCache
    
  224.   test('root acts as implicit cache boundary', async () => {
    
  225.     const root = ReactNoop.createRoot();
    
  226.     await act(() => {
    
  227.       root.render(
    
  228.         <Suspense fallback={<Text text="Loading..." />}>
    
  229.           <AsyncText text="A" />
    
  230.         </Suspense>,
    
  231.       );
    
  232.     });
    
  233.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  234.     expect(root).toMatchRenderedOutput('Loading...');
    
  235. 
    
  236.     await act(() => {
    
  237.       resolveMostRecentTextCache('A');
    
  238.     });
    
  239.     assertLog(['A']);
    
  240.     expect(root).toMatchRenderedOutput('A');
    
  241. 
    
  242.     await act(() => {
    
  243.       root.render('Bye');
    
  244.     });
    
  245.     // no cleanup: cache is still retained at the root
    
  246.     assertLog([]);
    
  247.     expect(root).toMatchRenderedOutput('Bye');
    
  248.   });
    
  249. 
    
  250.   // @gate enableCacheElement && enableCache
    
  251.   test('multiple new Cache boundaries in the same mount share the same, fresh root cache', async () => {
    
  252.     function App() {
    
  253.       return (
    
  254.         <>
    
  255.           <Cache>
    
  256.             <Suspense fallback={<Text text="Loading..." />}>
    
  257.               <AsyncText text="A" />
    
  258.             </Suspense>
    
  259.           </Cache>
    
  260.           <Cache>
    
  261.             <Suspense fallback={<Text text="Loading..." />}>
    
  262.               <AsyncText text="A" />
    
  263.             </Suspense>
    
  264.           </Cache>
    
  265.         </>
    
  266.       );
    
  267.     }
    
  268. 
    
  269.     const root = ReactNoop.createRoot();
    
  270.     await act(() => {
    
  271.       root.render(<App showMore={false} />);
    
  272.     });
    
  273. 
    
  274.     // Even though there are two new <Cache /> trees, they should share the same
    
  275.     // data cache. So there should be only a single cache miss for A.
    
  276.     assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
    
  277.     expect(root).toMatchRenderedOutput('Loading...Loading...');
    
  278. 
    
  279.     await act(() => {
    
  280.       resolveMostRecentTextCache('A');
    
  281.     });
    
  282.     assertLog(['A', 'A']);
    
  283.     expect(root).toMatchRenderedOutput('AA');
    
  284. 
    
  285.     await act(() => {
    
  286.       root.render('Bye');
    
  287.     });
    
  288.     // no cleanup: cache is still retained at the root
    
  289.     assertLog([]);
    
  290.     expect(root).toMatchRenderedOutput('Bye');
    
  291.   });
    
  292. 
    
  293.   // @gate enableCacheElement && enableCache
    
  294.   test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
    
  295.     function App({showMore}) {
    
  296.       return showMore ? (
    
  297.         <>
    
  298.           <Cache>
    
  299.             <Suspense fallback={<Text text="Loading..." />}>
    
  300.               <AsyncText text="A" />
    
  301.             </Suspense>
    
  302.           </Cache>
    
  303.           <Cache>
    
  304.             <Suspense fallback={<Text text="Loading..." />}>
    
  305.               <AsyncText text="A" />
    
  306.             </Suspense>
    
  307.           </Cache>
    
  308.         </>
    
  309.       ) : (
    
  310.         '(empty)'
    
  311.       );
    
  312.     }
    
  313. 
    
  314.     const root = ReactNoop.createRoot();
    
  315.     await act(() => {
    
  316.       root.render(<App showMore={false} />);
    
  317.     });
    
  318.     assertLog([]);
    
  319.     expect(root).toMatchRenderedOutput('(empty)');
    
  320. 
    
  321.     await act(() => {
    
  322.       root.render(<App showMore={true} />);
    
  323.     });
    
  324.     // Even though there are two new <Cache /> trees, they should share the same
    
  325.     // data cache. So there should be only a single cache miss for A.
    
  326.     assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
    
  327.     expect(root).toMatchRenderedOutput('Loading...Loading...');
    
  328. 
    
  329.     await act(() => {
    
  330.       resolveMostRecentTextCache('A');
    
  331.     });
    
  332.     assertLog(['A', 'A']);
    
  333.     expect(root).toMatchRenderedOutput('AA');
    
  334. 
    
  335.     await act(() => {
    
  336.       root.render('Bye');
    
  337.     });
    
  338.     // cleanup occurs for the cache shared by the inner cache boundaries (which
    
  339.     // are not shared w the root because they were added in an update)
    
  340.     // note that no cache is created for the root since the cache is never accessed
    
  341.     assertLog(['Cache cleanup: A [v1]']);
    
  342.     expect(root).toMatchRenderedOutput('Bye');
    
  343.   });
    
  344. 
    
  345.   // @gate enableCacheElement && enableCache
    
  346.   test(
    
  347.     'nested cache boundaries share the same cache as the root during ' +
    
  348.       'the initial render',
    
  349.     async () => {
    
  350.       function App() {
    
  351.         return (
    
  352.           <Suspense fallback={<Text text="Loading..." />}>
    
  353.             <AsyncText text="A" />
    
  354.             <Cache>
    
  355.               <AsyncText text="A" />
    
  356.             </Cache>
    
  357.           </Suspense>
    
  358.         );
    
  359.       }
    
  360. 
    
  361.       const root = ReactNoop.createRoot();
    
  362.       await act(() => {
    
  363.         root.render(<App />);
    
  364.       });
    
  365.       // Even though there is a nested <Cache /> boundary, it should share the same
    
  366.       // data cache as the root. So there should be only a single cache miss for A.
    
  367.       assertLog(['Cache miss! [A]', 'Loading...']);
    
  368.       expect(root).toMatchRenderedOutput('Loading...');
    
  369. 
    
  370.       await act(() => {
    
  371.         resolveMostRecentTextCache('A');
    
  372.       });
    
  373.       assertLog(['A', 'A']);
    
  374.       expect(root).toMatchRenderedOutput('AA');
    
  375. 
    
  376.       await act(() => {
    
  377.         root.render('Bye');
    
  378.       });
    
  379.       // no cleanup: cache is still retained at the root
    
  380.       assertLog([]);
    
  381.       expect(root).toMatchRenderedOutput('Bye');
    
  382.     },
    
  383.   );
    
  384. 
    
  385.   // @gate enableCacheElement && enableCache
    
  386.   test('new content inside an existing Cache boundary should re-use already cached data', async () => {
    
  387.     function App({showMore}) {
    
  388.       return (
    
  389.         <Cache>
    
  390.           <Suspense fallback={<Text text="Loading..." />}>
    
  391.             <AsyncText showVersion={true} text="A" />
    
  392.           </Suspense>
    
  393.           {showMore ? (
    
  394.             <Suspense fallback={<Text text="Loading..." />}>
    
  395.               <AsyncText showVersion={true} text="A" />
    
  396.             </Suspense>
    
  397.           ) : null}
    
  398.         </Cache>
    
  399.       );
    
  400.     }
    
  401. 
    
  402.     const root = ReactNoop.createRoot();
    
  403.     await act(() => {
    
  404.       seedNextTextCache('A');
    
  405.       root.render(<App showMore={false} />);
    
  406.     });
    
  407.     assertLog(['A [v1]']);
    
  408.     expect(root).toMatchRenderedOutput('A [v1]');
    
  409. 
    
  410.     // Add a new cache boundary
    
  411.     await act(() => {
    
  412.       root.render(<App showMore={true} />);
    
  413.     });
    
  414.     assertLog([
    
  415.       'A [v1]',
    
  416.       // New tree should use already cached data
    
  417.       'A [v1]',
    
  418.     ]);
    
  419.     expect(root).toMatchRenderedOutput('A [v1]A [v1]');
    
  420. 
    
  421.     await act(() => {
    
  422.       root.render('Bye');
    
  423.     });
    
  424.     // no cleanup: cache is still retained at the root
    
  425.     assertLog([]);
    
  426.     expect(root).toMatchRenderedOutput('Bye');
    
  427.   });
    
  428. 
    
  429.   // @gate enableCacheElement && enableCache
    
  430.   test('a new Cache boundary uses fresh cache', async () => {
    
  431.     // The only difference from the previous test is that the "Show More"
    
  432.     // content is wrapped in a nested <Cache /> boundary
    
  433.     function App({showMore}) {
    
  434.       return (
    
  435.         <Cache>
    
  436.           <Suspense fallback={<Text text="Loading..." />}>
    
  437.             <AsyncText showVersion={true} text="A" />
    
  438.           </Suspense>
    
  439.           {showMore ? (
    
  440.             <Cache>
    
  441.               <Suspense fallback={<Text text="Loading..." />}>
    
  442.                 <AsyncText showVersion={true} text="A" />
    
  443.               </Suspense>
    
  444.             </Cache>
    
  445.           ) : null}
    
  446.         </Cache>
    
  447.       );
    
  448.     }
    
  449. 
    
  450.     const root = ReactNoop.createRoot();
    
  451.     await act(() => {
    
  452.       seedNextTextCache('A');
    
  453.       root.render(<App showMore={false} />);
    
  454.     });
    
  455.     assertLog(['A [v1]']);
    
  456.     expect(root).toMatchRenderedOutput('A [v1]');
    
  457. 
    
  458.     // Add a new cache boundary
    
  459.     await act(() => {
    
  460.       root.render(<App showMore={true} />);
    
  461.     });
    
  462.     assertLog([
    
  463.       'A [v1]',
    
  464.       // New tree should load fresh data.
    
  465.       'Cache miss! [A]',
    
  466.       'Loading...',
    
  467.     ]);
    
  468.     expect(root).toMatchRenderedOutput('A [v1]Loading...');
    
  469.     await act(() => {
    
  470.       resolveMostRecentTextCache('A');
    
  471.     });
    
  472.     assertLog(['A [v2]']);
    
  473.     expect(root).toMatchRenderedOutput('A [v1]A [v2]');
    
  474. 
    
  475.     // Replace all the children: this should retain the root Cache instance,
    
  476.     // but cleanup the separate cache instance created for the fresh cache
    
  477.     // boundary
    
  478.     await act(() => {
    
  479.       root.render('Bye!');
    
  480.     });
    
  481.     // Cleanup occurs for the *second* cache instance: the first is still
    
  482.     // referenced by the root
    
  483.     assertLog(['Cache cleanup: A [v2]']);
    
  484.     expect(root).toMatchRenderedOutput('Bye!');
    
  485.   });
    
  486. 
    
  487.   // @gate enableCacheElement && enableCache
    
  488.   test('inner/outer cache boundaries uses the same cache instance on initial render', async () => {
    
  489.     const root = ReactNoop.createRoot();
    
  490. 
    
  491.     function App() {
    
  492.       return (
    
  493.         <Cache>
    
  494.           <Suspense fallback={<Text text="Loading shell..." />}>
    
  495.             {/* The shell reads A */}
    
  496.             <Shell>
    
  497.               {/* The inner content reads both A and B */}
    
  498.               <Suspense fallback={<Text text="Loading content..." />}>
    
  499.                 <Cache>
    
  500.                   <Content />
    
  501.                 </Cache>
    
  502.               </Suspense>
    
  503.             </Shell>
    
  504.           </Suspense>
    
  505.         </Cache>
    
  506.       );
    
  507.     }
    
  508. 
    
  509.     function Shell({children}) {
    
  510.       readText('A');
    
  511.       return (
    
  512.         <>
    
  513.           <div>
    
  514.             <Text text="Shell" />
    
  515.           </div>
    
  516.           <div>{children}</div>
    
  517.         </>
    
  518.       );
    
  519.     }
    
  520. 
    
  521.     function Content() {
    
  522.       readText('A');
    
  523.       readText('B');
    
  524.       return <Text text="Content" />;
    
  525.     }
    
  526. 
    
  527.     await act(() => {
    
  528.       root.render(<App />);
    
  529.     });
    
  530.     assertLog(['Cache miss! [A]', 'Loading shell...']);
    
  531.     expect(root).toMatchRenderedOutput('Loading shell...');
    
  532. 
    
  533.     await act(() => {
    
  534.       resolveMostRecentTextCache('A');
    
  535.     });
    
  536.     assertLog([
    
  537.       'Shell',
    
  538.       // There's a cache miss for B, because it hasn't been read yet. But not
    
  539.       // A, because it was cached when we rendered the shell.
    
  540.       'Cache miss! [B]',
    
  541.       'Loading content...',
    
  542.     ]);
    
  543.     expect(root).toMatchRenderedOutput(
    
  544.       <>
    
  545.         <div>Shell</div>
    
  546.         <div>Loading content...</div>
    
  547.       </>,
    
  548.     );
    
  549. 
    
  550.     await act(() => {
    
  551.       resolveMostRecentTextCache('B');
    
  552.     });
    
  553.     assertLog(['Content']);
    
  554.     expect(root).toMatchRenderedOutput(
    
  555.       <>
    
  556.         <div>Shell</div>
    
  557.         <div>Content</div>
    
  558.       </>,
    
  559.     );
    
  560. 
    
  561.     await act(() => {
    
  562.       root.render('Bye');
    
  563.     });
    
  564.     // no cleanup: cache is still retained at the root
    
  565.     assertLog([]);
    
  566.     expect(root).toMatchRenderedOutput('Bye');
    
  567.   });
    
  568. 
    
  569.   // @gate enableCacheElement && enableCache
    
  570.   test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => {
    
  571.     const root = ReactNoop.createRoot();
    
  572. 
    
  573.     function App({showMore}) {
    
  574.       return showMore ? (
    
  575.         <Cache>
    
  576.           <Suspense fallback={<Text text="Loading shell..." />}>
    
  577.             {/* The shell reads A */}
    
  578.             <Shell>
    
  579.               {/* The inner content reads both A and B */}
    
  580.               <Suspense fallback={<Text text="Loading content..." />}>
    
  581.                 <Cache>
    
  582.                   <Content />
    
  583.                 </Cache>
    
  584.               </Suspense>
    
  585.             </Shell>
    
  586.           </Suspense>
    
  587.         </Cache>
    
  588.       ) : (
    
  589.         '(empty)'
    
  590.       );
    
  591.     }
    
  592. 
    
  593.     function Shell({children}) {
    
  594.       readText('A');
    
  595.       return (
    
  596.         <>
    
  597.           <div>
    
  598.             <Text text="Shell" />
    
  599.           </div>
    
  600.           <div>{children}</div>
    
  601.         </>
    
  602.       );
    
  603.     }
    
  604. 
    
  605.     function Content() {
    
  606.       readText('A');
    
  607.       readText('B');
    
  608.       return <Text text="Content" />;
    
  609.     }
    
  610. 
    
  611.     await act(() => {
    
  612.       root.render(<App showMore={false} />);
    
  613.     });
    
  614.     assertLog([]);
    
  615.     expect(root).toMatchRenderedOutput('(empty)');
    
  616. 
    
  617.     await act(() => {
    
  618.       root.render(<App showMore={true} />);
    
  619.     });
    
  620.     assertLog(['Cache miss! [A]', 'Loading shell...']);
    
  621.     expect(root).toMatchRenderedOutput('Loading shell...');
    
  622. 
    
  623.     await act(() => {
    
  624.       resolveMostRecentTextCache('A');
    
  625.     });
    
  626.     assertLog([
    
  627.       'Shell',
    
  628.       // There's a cache miss for B, because it hasn't been read yet. But not
    
  629.       // A, because it was cached when we rendered the shell.
    
  630.       'Cache miss! [B]',
    
  631.       'Loading content...',
    
  632.     ]);
    
  633.     expect(root).toMatchRenderedOutput(
    
  634.       <>
    
  635.         <div>Shell</div>
    
  636.         <div>Loading content...</div>
    
  637.       </>,
    
  638.     );
    
  639. 
    
  640.     await act(() => {
    
  641.       resolveMostRecentTextCache('B');
    
  642.     });
    
  643.     assertLog(['Content']);
    
  644.     expect(root).toMatchRenderedOutput(
    
  645.       <>
    
  646.         <div>Shell</div>
    
  647.         <div>Content</div>
    
  648.       </>,
    
  649.     );
    
  650. 
    
  651.     await act(() => {
    
  652.       root.render('Bye');
    
  653.     });
    
  654.     assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: B [v1]']);
    
  655.     expect(root).toMatchRenderedOutput('Bye');
    
  656.   });
    
  657. 
    
  658.   // @gate enableCache
    
  659.   test('refresh a cache boundary', async () => {
    
  660.     let refresh;
    
  661.     function App() {
    
  662.       refresh = useCacheRefresh();
    
  663.       return <AsyncText showVersion={true} text="A" />;
    
  664.     }
    
  665. 
    
  666.     // Mount initial data
    
  667.     const root = ReactNoop.createRoot();
    
  668.     await act(() => {
    
  669.       root.render(
    
  670.         <Suspense fallback={<Text text="Loading..." />}>
    
  671.           <App />
    
  672.         </Suspense>,
    
  673.       );
    
  674.     });
    
  675.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  676.     expect(root).toMatchRenderedOutput('Loading...');
    
  677. 
    
  678.     await act(() => {
    
  679.       resolveMostRecentTextCache('A');
    
  680.     });
    
  681.     assertLog(['A [v1]']);
    
  682.     expect(root).toMatchRenderedOutput('A [v1]');
    
  683. 
    
  684.     // Refresh for new data.
    
  685.     await act(() => {
    
  686.       startTransition(() => refresh());
    
  687.     });
    
  688.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  689.     expect(root).toMatchRenderedOutput('A [v1]');
    
  690. 
    
  691.     await act(() => {
    
  692.       resolveMostRecentTextCache('A');
    
  693.     });
    
  694.     // Note that the version has updated
    
  695.     if (getCacheSignal) {
    
  696.       assertLog(['A [v2]', 'Cache cleanup: A [v1]']);
    
  697.     } else {
    
  698.       assertLog(['A [v2]']);
    
  699.     }
    
  700.     expect(root).toMatchRenderedOutput('A [v2]');
    
  701. 
    
  702.     await act(() => {
    
  703.       root.render('Bye');
    
  704.     });
    
  705.     expect(root).toMatchRenderedOutput('Bye');
    
  706.   });
    
  707. 
    
  708.   // @gate enableCacheElement && enableCache
    
  709.   test('refresh the root cache', async () => {
    
  710.     let refresh;
    
  711.     function App() {
    
  712.       refresh = useCacheRefresh();
    
  713.       return <AsyncText showVersion={true} text="A" />;
    
  714.     }
    
  715. 
    
  716.     // Mount initial data
    
  717.     const root = ReactNoop.createRoot();
    
  718.     await act(() => {
    
  719.       root.render(
    
  720.         <Suspense fallback={<Text text="Loading..." />}>
    
  721.           <App />
    
  722.         </Suspense>,
    
  723.       );
    
  724.     });
    
  725.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  726.     expect(root).toMatchRenderedOutput('Loading...');
    
  727. 
    
  728.     await act(() => {
    
  729.       resolveMostRecentTextCache('A');
    
  730.     });
    
  731.     assertLog(['A [v1]']);
    
  732.     expect(root).toMatchRenderedOutput('A [v1]');
    
  733. 
    
  734.     // Refresh for new data.
    
  735.     await act(() => {
    
  736.       startTransition(() => refresh());
    
  737.     });
    
  738.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  739.     expect(root).toMatchRenderedOutput('A [v1]');
    
  740. 
    
  741.     await act(() => {
    
  742.       resolveMostRecentTextCache('A');
    
  743.     });
    
  744.     // Note that the version has updated, and the previous cache is cleared
    
  745.     assertLog(['A [v2]', 'Cache cleanup: A [v1]']);
    
  746.     expect(root).toMatchRenderedOutput('A [v2]');
    
  747. 
    
  748.     await act(() => {
    
  749.       root.render('Bye');
    
  750.     });
    
  751.     // the original root cache already cleaned up when the refresh completed
    
  752.     assertLog([]);
    
  753.     expect(root).toMatchRenderedOutput('Bye');
    
  754.   });
    
  755. 
    
  756.   // @gate enableCacheElement && enableCache
    
  757.   test('refresh the root cache without a transition', async () => {
    
  758.     let refresh;
    
  759.     function App() {
    
  760.       refresh = useCacheRefresh();
    
  761.       return <AsyncText showVersion={true} text="A" />;
    
  762.     }
    
  763. 
    
  764.     // Mount initial data
    
  765.     const root = ReactNoop.createRoot();
    
  766.     await act(() => {
    
  767.       root.render(
    
  768.         <Suspense fallback={<Text text="Loading..." />}>
    
  769.           <App />
    
  770.         </Suspense>,
    
  771.       );
    
  772.     });
    
  773.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  774.     expect(root).toMatchRenderedOutput('Loading...');
    
  775. 
    
  776.     await act(() => {
    
  777.       resolveMostRecentTextCache('A');
    
  778.     });
    
  779.     assertLog(['A [v1]']);
    
  780.     expect(root).toMatchRenderedOutput('A [v1]');
    
  781. 
    
  782.     // Refresh for new data.
    
  783.     await act(() => {
    
  784.       refresh();
    
  785.     });
    
  786.     assertLog([
    
  787.       'Cache miss! [A]',
    
  788.       'Loading...',
    
  789.       // The v1 cache can be cleaned up since everything that references it has
    
  790.       // been replaced by a fallback. When the boundary switches back to visible
    
  791.       // it will use the v2 cache.
    
  792.       'Cache cleanup: A [v1]',
    
  793.     ]);
    
  794.     expect(root).toMatchRenderedOutput('Loading...');
    
  795. 
    
  796.     await act(() => {
    
  797.       resolveMostRecentTextCache('A');
    
  798.     });
    
  799.     // Note that the version has updated, and the previous cache is cleared
    
  800.     assertLog(['A [v2]']);
    
  801.     expect(root).toMatchRenderedOutput('A [v2]');
    
  802. 
    
  803.     await act(() => {
    
  804.       root.render('Bye');
    
  805.     });
    
  806.     // the original root cache already cleaned up when the refresh completed
    
  807.     assertLog([]);
    
  808.     expect(root).toMatchRenderedOutput('Bye');
    
  809.   });
    
  810. 
    
  811.   // @gate enableCacheElement && enableCache
    
  812.   test('refresh a cache with seed data', async () => {
    
  813.     let refreshWithSeed;
    
  814.     function App() {
    
  815.       const refresh = useCacheRefresh();
    
  816.       const [seed, setSeed] = useState({fn: null});
    
  817.       if (seed.fn) {
    
  818.         seed.fn();
    
  819.         seed.fn = null;
    
  820.       }
    
  821.       refreshWithSeed = fn => {
    
  822.         setSeed({fn});
    
  823.         refresh();
    
  824.       };
    
  825.       return <AsyncText showVersion={true} text="A" />;
    
  826.     }
    
  827. 
    
  828.     // Mount initial data
    
  829.     const root = ReactNoop.createRoot();
    
  830.     await act(() => {
    
  831.       root.render(
    
  832.         <Cache>
    
  833.           <Suspense fallback={<Text text="Loading..." />}>
    
  834.             <App />
    
  835.           </Suspense>
    
  836.         </Cache>,
    
  837.       );
    
  838.     });
    
  839.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  840.     expect(root).toMatchRenderedOutput('Loading...');
    
  841. 
    
  842.     await act(() => {
    
  843.       resolveMostRecentTextCache('A');
    
  844.     });
    
  845.     assertLog(['A [v1]']);
    
  846.     expect(root).toMatchRenderedOutput('A [v1]');
    
  847. 
    
  848.     // Refresh for new data.
    
  849.     await act(() => {
    
  850.       // Refresh the cache with seeded data, like you would receive from a
    
  851.       // server mutation.
    
  852.       // TODO: Seeding multiple typed textCaches. Should work by calling `refresh`
    
  853.       // multiple times with different key/value pairs
    
  854.       startTransition(() =>
    
  855.         refreshWithSeed(() => {
    
  856.           const textCache = getTextCache();
    
  857.           textCache.resolve('A');
    
  858.         }),
    
  859.       );
    
  860.     });
    
  861.     // The root should re-render without a cache miss.
    
  862.     // The cache is not cleared up yet, since it's still reference by the root
    
  863.     assertLog(['A [v2]']);
    
  864.     expect(root).toMatchRenderedOutput('A [v2]');
    
  865. 
    
  866.     await act(() => {
    
  867.       root.render('Bye');
    
  868.     });
    
  869.     // the refreshed cache boundary is unmounted and cleans up
    
  870.     assertLog(['Cache cleanup: A [v2]']);
    
  871.     expect(root).toMatchRenderedOutput('Bye');
    
  872.   });
    
  873. 
    
  874.   // @gate enableCacheElement && enableCache
    
  875.   test('refreshing a parent cache also refreshes its children', async () => {
    
  876.     let refreshShell;
    
  877.     function RefreshShell() {
    
  878.       refreshShell = useCacheRefresh();
    
  879.       return null;
    
  880.     }
    
  881. 
    
  882.     function App({showMore}) {
    
  883.       return (
    
  884.         <Cache>
    
  885.           <RefreshShell />
    
  886.           <Suspense fallback={<Text text="Loading..." />}>
    
  887.             <AsyncText showVersion={true} text="A" />
    
  888.           </Suspense>
    
  889.           {showMore ? (
    
  890.             <Cache>
    
  891.               <Suspense fallback={<Text text="Loading..." />}>
    
  892.                 <AsyncText showVersion={true} text="A" />
    
  893.               </Suspense>
    
  894.             </Cache>
    
  895.           ) : null}
    
  896.         </Cache>
    
  897.       );
    
  898.     }
    
  899. 
    
  900.     const root = ReactNoop.createRoot();
    
  901.     await act(() => {
    
  902.       seedNextTextCache('A');
    
  903.       root.render(<App showMore={false} />);
    
  904.     });
    
  905.     assertLog(['A [v1]']);
    
  906.     expect(root).toMatchRenderedOutput('A [v1]');
    
  907. 
    
  908.     // Add a new cache boundary
    
  909.     await act(() => {
    
  910.       seedNextTextCache('A');
    
  911.       root.render(<App showMore={true} />);
    
  912.     });
    
  913.     assertLog([
    
  914.       'A [v1]',
    
  915.       // New tree should load fresh data.
    
  916.       'A [v2]',
    
  917.     ]);
    
  918.     expect(root).toMatchRenderedOutput('A [v1]A [v2]');
    
  919. 
    
  920.     // Now refresh the shell. This should also cause the "Show More" contents to
    
  921.     // refresh, since its cache is nested inside the outer one.
    
  922.     await act(() => {
    
  923.       startTransition(() => refreshShell());
    
  924.     });
    
  925.     assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
    
  926.     expect(root).toMatchRenderedOutput('A [v1]A [v2]');
    
  927. 
    
  928.     await act(() => {
    
  929.       resolveMostRecentTextCache('A');
    
  930.     });
    
  931.     assertLog([
    
  932.       'A [v3]',
    
  933.       'A [v3]',
    
  934.       // once the refresh completes the inner showMore boundary frees its previous
    
  935.       // cache instance, since it is now using the refreshed parent instance.
    
  936.       'Cache cleanup: A [v2]',
    
  937.     ]);
    
  938.     expect(root).toMatchRenderedOutput('A [v3]A [v3]');
    
  939. 
    
  940.     await act(() => {
    
  941.       root.render('Bye!');
    
  942.     });
    
  943.     // Unmounting children releases the refreshed cache instance only; the root
    
  944.     // still retains the original cache instance used for the first render
    
  945.     assertLog(['Cache cleanup: A [v3]']);
    
  946.     expect(root).toMatchRenderedOutput('Bye!');
    
  947.   });
    
  948. 
    
  949.   // @gate enableCacheElement && enableCache
    
  950.   test(
    
  951.     'refreshing a cache boundary does not refresh the other boundaries ' +
    
  952.       'that mounted at the same time (i.e. the ones that share the same cache)',
    
  953.     async () => {
    
  954.       let refreshFirstBoundary;
    
  955.       function RefreshFirstBoundary() {
    
  956.         refreshFirstBoundary = useCacheRefresh();
    
  957.         return null;
    
  958.       }
    
  959. 
    
  960.       function App({showMore}) {
    
  961.         return showMore ? (
    
  962.           <>
    
  963.             <Cache>
    
  964.               <Suspense fallback={<Text text="Loading..." />}>
    
  965.                 <RefreshFirstBoundary />
    
  966.                 <AsyncText showVersion={true} text="A" />
    
  967.               </Suspense>
    
  968.             </Cache>
    
  969.             <Cache>
    
  970.               <Suspense fallback={<Text text="Loading..." />}>
    
  971.                 <AsyncText showVersion={true} text="A" />
    
  972.               </Suspense>
    
  973.             </Cache>
    
  974.           </>
    
  975.         ) : null;
    
  976.       }
    
  977. 
    
  978.       // First mount the initial shell without the nested boundaries. This is
    
  979.       // necessary for this test because we want the two inner boundaries to be
    
  980.       // treated like sibling providers that happen to share an underlying
    
  981.       // cache, as opposed to consumers of the root-level cache.
    
  982.       const root = ReactNoop.createRoot();
    
  983.       await act(() => {
    
  984.         root.render(<App showMore={false} />);
    
  985.       });
    
  986. 
    
  987.       // Now reveal the boundaries. In a real app  this would be a navigation.
    
  988.       await act(() => {
    
  989.         root.render(<App showMore={true} />);
    
  990.       });
    
  991. 
    
  992.       // Even though there are two new <Cache /> trees, they should share the same
    
  993.       // data cache. So there should be only a single cache miss for A.
    
  994.       assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
    
  995.       expect(root).toMatchRenderedOutput('Loading...Loading...');
    
  996. 
    
  997.       await act(() => {
    
  998.         resolveMostRecentTextCache('A');
    
  999.       });
    
  1000.       assertLog(['A [v1]', 'A [v1]']);
    
  1001.       expect(root).toMatchRenderedOutput('A [v1]A [v1]');
    
  1002. 
    
  1003.       // Refresh the first boundary. It should not refresh the second boundary,
    
  1004.       // even though they previously shared the same underlying cache.
    
  1005.       await act(async () => {
    
  1006.         await refreshFirstBoundary();
    
  1007.       });
    
  1008.       assertLog(['Cache miss! [A]', 'Loading...']);
    
  1009. 
    
  1010.       await act(() => {
    
  1011.         resolveMostRecentTextCache('A');
    
  1012.       });
    
  1013.       assertLog(['A [v2]']);
    
  1014.       expect(root).toMatchRenderedOutput('A [v2]A [v1]');
    
  1015. 
    
  1016.       // Unmount children: this should clear *both* cache instances:
    
  1017.       // the root doesn't have a cache instance (since it wasn't accessed
    
  1018.       // during the initial render, and all subsequent cache accesses were within
    
  1019.       // a fresh boundary). Therefore this causes cleanup for both the fresh cache
    
  1020.       // instance in the refreshed first boundary and cleanup for the non-refreshed
    
  1021.       // sibling boundary.
    
  1022.       await act(() => {
    
  1023.         root.render('Bye!');
    
  1024.       });
    
  1025.       assertLog(['Cache cleanup: A [v2]', 'Cache cleanup: A [v1]']);
    
  1026.       expect(root).toMatchRenderedOutput('Bye!');
    
  1027.     },
    
  1028.   );
    
  1029. 
    
  1030.   // @gate enableCacheElement && enableCache
    
  1031.   test(
    
  1032.     'mount a new Cache boundary in a sibling while simultaneously ' +
    
  1033.       'resolving a Suspense boundary',
    
  1034.     async () => {
    
  1035.       function App({showMore}) {
    
  1036.         return (
    
  1037.           <>
    
  1038.             {showMore ? (
    
  1039.               <Suspense fallback={<Text text="Loading..." />}>
    
  1040.                 <Cache>
    
  1041.                   <AsyncText showVersion={true} text="A" />
    
  1042.                 </Cache>
    
  1043.               </Suspense>
    
  1044.             ) : null}
    
  1045.             <Suspense fallback={<Text text="Loading..." />}>
    
  1046.               <Cache>
    
  1047.                 {' '}
    
  1048.                 <AsyncText showVersion={true} text="A" />{' '}
    
  1049.                 <AsyncText showVersion={true} text="B" />
    
  1050.               </Cache>
    
  1051.             </Suspense>
    
  1052.           </>
    
  1053.         );
    
  1054.       }
    
  1055. 
    
  1056.       const root = ReactNoop.createRoot();
    
  1057.       await act(() => {
    
  1058.         root.render(<App showMore={false} />);
    
  1059.       });
    
  1060.       assertLog(['Cache miss! [A]', 'Loading...']);
    
  1061.       expect(root).toMatchRenderedOutput('Loading...');
    
  1062. 
    
  1063.       await act(() => {
    
  1064.         // This will resolve the content in the first cache
    
  1065.         resolveMostRecentTextCache('A');
    
  1066.         resolveMostRecentTextCache('B');
    
  1067.         // And mount the second tree, which includes new content
    
  1068.         root.render(<App showMore={true} />);
    
  1069.       });
    
  1070.       assertLog([
    
  1071.         // The new tree should use a fresh cache
    
  1072.         'Cache miss! [A]',
    
  1073.         'Loading...',
    
  1074.         // The other tree uses the cached responses. This demonstrates that the
    
  1075.         // requests are not dropped.
    
  1076.         'A [v1]',
    
  1077.         'B [v1]',
    
  1078.       ]);
    
  1079.       expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]');
    
  1080. 
    
  1081.       // Now resolve the second tree
    
  1082.       await act(() => {
    
  1083.         resolveMostRecentTextCache('A');
    
  1084.       });
    
  1085.       assertLog(['A [v2]']);
    
  1086.       expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
    
  1087. 
    
  1088.       await act(() => {
    
  1089.         root.render('Bye!');
    
  1090.       });
    
  1091.       // Unmounting children releases both cache boundaries, but the original
    
  1092.       // cache instance (used by second boundary) is still referenced by the root.
    
  1093.       // only the second cache instance is freed.
    
  1094.       assertLog(['Cache cleanup: A [v2]']);
    
  1095.       expect(root).toMatchRenderedOutput('Bye!');
    
  1096.     },
    
  1097.   );
    
  1098. 
    
  1099.   // @gate enableCacheElement && enableCache
    
  1100.   test('cache pool is cleared once transitions that depend on it commit their shell', async () => {
    
  1101.     function Child({text}) {
    
  1102.       return (
    
  1103.         <Cache>
    
  1104.           <AsyncText showVersion={true} text={text} />
    
  1105.         </Cache>
    
  1106.       );
    
  1107.     }
    
  1108. 
    
  1109.     const root = ReactNoop.createRoot();
    
  1110.     await act(() => {
    
  1111.       root.render(
    
  1112.         <Suspense fallback={<Text text="Loading..." />}>(empty)</Suspense>,
    
  1113.       );
    
  1114.     });
    
  1115.     assertLog([]);
    
  1116.     expect(root).toMatchRenderedOutput('(empty)');
    
  1117. 
    
  1118.     await act(() => {
    
  1119.       startTransition(() => {
    
  1120.         root.render(
    
  1121.           <Suspense fallback={<Text text="Loading..." />}>
    
  1122.             <Child text="A" />
    
  1123.           </Suspense>,
    
  1124.         );
    
  1125.       });
    
  1126.     });
    
  1127.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  1128.     expect(root).toMatchRenderedOutput('(empty)');
    
  1129. 
    
  1130.     await act(() => {
    
  1131.       startTransition(() => {
    
  1132.         root.render(
    
  1133.           <Suspense fallback={<Text text="Loading..." />}>
    
  1134.             <Child text="A" />
    
  1135.             <Child text="A" />
    
  1136.           </Suspense>,
    
  1137.         );
    
  1138.       });
    
  1139.     });
    
  1140.     assertLog([
    
  1141.       // No cache miss, because it uses the pooled cache
    
  1142.       'Loading...',
    
  1143.     ]);
    
  1144.     expect(root).toMatchRenderedOutput('(empty)');
    
  1145. 
    
  1146.     // Resolve the request
    
  1147.     await act(() => {
    
  1148.       resolveMostRecentTextCache('A');
    
  1149.     });
    
  1150.     assertLog(['A [v1]', 'A [v1]']);
    
  1151.     expect(root).toMatchRenderedOutput('A [v1]A [v1]');
    
  1152. 
    
  1153.     // Now do another transition
    
  1154.     await act(() => {
    
  1155.       startTransition(() => {
    
  1156.         root.render(
    
  1157.           <Suspense fallback={<Text text="Loading..." />}>
    
  1158.             <Child text="A" />
    
  1159.             <Child text="A" />
    
  1160.             <Child text="A" />
    
  1161.           </Suspense>,
    
  1162.         );
    
  1163.       });
    
  1164.     });
    
  1165.     assertLog([
    
  1166.       // First two children use the old cache because they already finished
    
  1167.       'A [v1]',
    
  1168.       'A [v1]',
    
  1169.       // The new child uses a fresh cache
    
  1170.       'Cache miss! [A]',
    
  1171.       'Loading...',
    
  1172.     ]);
    
  1173.     expect(root).toMatchRenderedOutput('A [v1]A [v1]');
    
  1174. 
    
  1175.     await act(() => {
    
  1176.       resolveMostRecentTextCache('A');
    
  1177.     });
    
  1178.     assertLog(['A [v1]', 'A [v1]', 'A [v2]']);
    
  1179.     expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]');
    
  1180. 
    
  1181.     // Unmount children: the first text cache instance is created only after the root
    
  1182.     // commits, so both fresh cache instances are released by their cache boundaries,
    
  1183.     // cleaning up v1 (used for the first two children which render together) and
    
  1184.     // v2 (used for the third boundary added later).
    
  1185.     await act(() => {
    
  1186.       root.render('Bye!');
    
  1187.     });
    
  1188.     assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: A [v2]']);
    
  1189.     expect(root).toMatchRenderedOutput('Bye!');
    
  1190.   });
    
  1191. 
    
  1192.   // @gate enableCacheElement && enableCache
    
  1193.   test('cache pool is not cleared by arbitrary commits', async () => {
    
  1194.     function App() {
    
  1195.       return (
    
  1196.         <>
    
  1197.           <ShowMore />
    
  1198.           <Unrelated />
    
  1199.         </>
    
  1200.       );
    
  1201.     }
    
  1202. 
    
  1203.     let showMore;
    
  1204.     function ShowMore() {
    
  1205.       const [shouldShow, _showMore] = useState(false);
    
  1206.       showMore = () => _showMore(true);
    
  1207.       return (
    
  1208.         <>
    
  1209.           <Suspense fallback={<Text text="Loading..." />}>
    
  1210.             {shouldShow ? (
    
  1211.               <Cache>
    
  1212.                 <AsyncText showVersion={true} text="A" />
    
  1213.               </Cache>
    
  1214.             ) : null}
    
  1215.           </Suspense>
    
  1216.         </>
    
  1217.       );
    
  1218.     }
    
  1219. 
    
  1220.     let updateUnrelated;
    
  1221.     function Unrelated() {
    
  1222.       const [count, _updateUnrelated] = useState(0);
    
  1223.       updateUnrelated = _updateUnrelated;
    
  1224.       return <Text text={String(count)} />;
    
  1225.     }
    
  1226. 
    
  1227.     const root = ReactNoop.createRoot();
    
  1228.     await act(() => {
    
  1229.       root.render(<App />);
    
  1230.     });
    
  1231.     assertLog(['0']);
    
  1232.     expect(root).toMatchRenderedOutput('0');
    
  1233. 
    
  1234.     await act(() => {
    
  1235.       startTransition(() => {
    
  1236.         showMore();
    
  1237.       });
    
  1238.     });
    
  1239.     assertLog(['Cache miss! [A]', 'Loading...']);
    
  1240.     expect(root).toMatchRenderedOutput('0');
    
  1241. 
    
  1242.     await act(() => {
    
  1243.       updateUnrelated(1);
    
  1244.     });
    
  1245.     assertLog([
    
  1246.       '1',
    
  1247. 
    
  1248.       // Happens to re-render the fallback. Doesn't need to, but not relevant
    
  1249.       // to this test.
    
  1250.       'Loading...',
    
  1251.     ]);
    
  1252.     expect(root).toMatchRenderedOutput('1');
    
  1253. 
    
  1254.     await act(() => {
    
  1255.       resolveMostRecentTextCache('A');
    
  1256.     });
    
  1257.     assertLog(['A [v1]']);
    
  1258.     expect(root).toMatchRenderedOutput('A [v1]1');
    
  1259. 
    
  1260.     // Unmount children: the first text cache instance is created only after initial
    
  1261.     // render after calling showMore(). This instance is cleaned up when that boundary
    
  1262.     // is unmounted. Bc root cache instance is never accessed, the inner cache
    
  1263.     // boundary ends up at v1.
    
  1264.     await act(() => {
    
  1265.       root.render('Bye!');
    
  1266.     });
    
  1267.     assertLog(['Cache cleanup: A [v1]']);
    
  1268.     expect(root).toMatchRenderedOutput('Bye!');
    
  1269.   });
    
  1270. 
    
  1271.   // @gate enableCacheElement && enableCache
    
  1272.   test('cache boundary uses a fresh cache when its key changes', async () => {
    
  1273.     const root = ReactNoop.createRoot();
    
  1274.     seedNextTextCache('A');
    
  1275.     await act(() => {
    
  1276.       root.render(
    
  1277.         <Suspense fallback="Loading...">
    
  1278.           <Cache key="A">
    
  1279.             <AsyncText showVersion={true} text="A" />
    
  1280.           </Cache>
    
  1281.         </Suspense>,
    
  1282.       );
    
  1283.     });
    
  1284.     assertLog(['A [v1]']);
    
  1285.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1286. 
    
  1287.     seedNextTextCache('B');
    
  1288.     await act(() => {
    
  1289.       root.render(
    
  1290.         <Suspense fallback="Loading...">
    
  1291.           <Cache key="B">
    
  1292.             <AsyncText showVersion={true} text="B" />
    
  1293.           </Cache>
    
  1294.         </Suspense>,
    
  1295.       );
    
  1296.     });
    
  1297.     assertLog(['B [v2]']);
    
  1298.     expect(root).toMatchRenderedOutput('B [v2]');
    
  1299. 
    
  1300.     // Unmount children: the fresh cache instance for B cleans up since the cache boundary
    
  1301.     // is the only owner, while the original cache instance (for A) is still retained by
    
  1302.     // the root.
    
  1303.     await act(() => {
    
  1304.       root.render('Bye!');
    
  1305.     });
    
  1306.     assertLog(['Cache cleanup: B [v2]']);
    
  1307.     expect(root).toMatchRenderedOutput('Bye!');
    
  1308.   });
    
  1309. 
    
  1310.   // @gate enableCacheElement && enableCache
    
  1311.   test('overlapping transitions after an initial mount use the same fresh cache', async () => {
    
  1312.     const root = ReactNoop.createRoot();
    
  1313.     await act(() => {
    
  1314.       root.render(
    
  1315.         <Suspense fallback="Loading...">
    
  1316.           <Cache key="A">
    
  1317.             <AsyncText showVersion={true} text="A" />
    
  1318.           </Cache>
    
  1319.         </Suspense>,
    
  1320.       );
    
  1321.     });
    
  1322.     assertLog(['Cache miss! [A]']);
    
  1323.     expect(root).toMatchRenderedOutput('Loading...');
    
  1324. 
    
  1325.     await act(() => {
    
  1326.       resolveMostRecentTextCache('A');
    
  1327.     });
    
  1328.     assertLog(['A [v1]']);
    
  1329.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1330. 
    
  1331.     // After a mount, subsequent transitions use a fresh cache
    
  1332.     await act(() => {
    
  1333.       startTransition(() => {
    
  1334.         root.render(
    
  1335.           <Suspense fallback="Loading...">
    
  1336.             <Cache key="B">
    
  1337.               <AsyncText showVersion={true} text="B" />
    
  1338.             </Cache>
    
  1339.           </Suspense>,
    
  1340.         );
    
  1341.       });
    
  1342.     });
    
  1343.     assertLog(['Cache miss! [B]']);
    
  1344.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1345. 
    
  1346.     // Update to a different text and with a different key for the cache
    
  1347.     // boundary: this should still use the fresh cache instance created
    
  1348.     // for the earlier transition
    
  1349.     await act(() => {
    
  1350.       startTransition(() => {
    
  1351.         root.render(
    
  1352.           <Suspense fallback="Loading...">
    
  1353.             <Cache key="C">
    
  1354.               <AsyncText showVersion={true} text="C" />
    
  1355.             </Cache>
    
  1356.           </Suspense>,
    
  1357.         );
    
  1358.       });
    
  1359.     });
    
  1360.     assertLog(['Cache miss! [C]']);
    
  1361.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1362. 
    
  1363.     await act(() => {
    
  1364.       resolveMostRecentTextCache('C');
    
  1365.     });
    
  1366.     assertLog(['C [v2]']);
    
  1367.     expect(root).toMatchRenderedOutput('C [v2]');
    
  1368. 
    
  1369.     // Unmount children: the fresh cache used for the updates is freed, while the
    
  1370.     // original cache (with A) is still retained at the root.
    
  1371.     await act(() => {
    
  1372.       root.render('Bye!');
    
  1373.     });
    
  1374.     assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']);
    
  1375.     expect(root).toMatchRenderedOutput('Bye!');
    
  1376.   });
    
  1377. 
    
  1378.   // @gate enableCacheElement && enableCache
    
  1379.   test('overlapping updates after an initial mount use the same fresh cache', async () => {
    
  1380.     const root = ReactNoop.createRoot();
    
  1381.     await act(() => {
    
  1382.       root.render(
    
  1383.         <Suspense fallback="Loading...">
    
  1384.           <Cache key="A">
    
  1385.             <AsyncText showVersion={true} text="A" />
    
  1386.           </Cache>
    
  1387.         </Suspense>,
    
  1388.       );
    
  1389.     });
    
  1390.     assertLog(['Cache miss! [A]']);
    
  1391.     expect(root).toMatchRenderedOutput('Loading...');
    
  1392. 
    
  1393.     await act(() => {
    
  1394.       resolveMostRecentTextCache('A');
    
  1395.     });
    
  1396.     assertLog(['A [v1]']);
    
  1397.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1398. 
    
  1399.     // After a mount, subsequent updates use a fresh cache
    
  1400.     await act(() => {
    
  1401.       root.render(
    
  1402.         <Suspense fallback="Loading...">
    
  1403.           <Cache key="B">
    
  1404.             <AsyncText showVersion={true} text="B" />
    
  1405.           </Cache>
    
  1406.         </Suspense>,
    
  1407.       );
    
  1408.     });
    
  1409.     assertLog(['Cache miss! [B]']);
    
  1410.     expect(root).toMatchRenderedOutput('Loading...');
    
  1411. 
    
  1412.     // A second update uses the same fresh cache: even though this is a new
    
  1413.     // Cache boundary, the render uses the fresh cache from the pending update.
    
  1414.     await act(() => {
    
  1415.       root.render(
    
  1416.         <Suspense fallback="Loading...">
    
  1417.           <Cache key="C">
    
  1418.             <AsyncText showVersion={true} text="C" />
    
  1419.           </Cache>
    
  1420.         </Suspense>,
    
  1421.       );
    
  1422.     });
    
  1423.     assertLog(['Cache miss! [C]']);
    
  1424.     expect(root).toMatchRenderedOutput('Loading...');
    
  1425. 
    
  1426.     await act(() => {
    
  1427.       resolveMostRecentTextCache('C');
    
  1428.     });
    
  1429.     assertLog(['C [v2]']);
    
  1430.     expect(root).toMatchRenderedOutput('C [v2]');
    
  1431. 
    
  1432.     // Unmount children: the fresh cache used for the updates is freed, while the
    
  1433.     // original cache (with A) is still retained at the root.
    
  1434.     await act(() => {
    
  1435.       root.render('Bye!');
    
  1436.     });
    
  1437.     assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']);
    
  1438.     expect(root).toMatchRenderedOutput('Bye!');
    
  1439.   });
    
  1440. 
    
  1441.   // @gate enableCacheElement && enableCache
    
  1442.   test('cleans up cache only used in an aborted transition', async () => {
    
  1443.     const root = ReactNoop.createRoot();
    
  1444.     seedNextTextCache('A');
    
  1445.     await act(() => {
    
  1446.       root.render(
    
  1447.         <Suspense fallback="Loading...">
    
  1448.           <Cache key="A">
    
  1449.             <AsyncText showVersion={true} text="A" />
    
  1450.           </Cache>
    
  1451.         </Suspense>,
    
  1452.       );
    
  1453.     });
    
  1454.     assertLog(['A [v1]']);
    
  1455.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1456. 
    
  1457.     // Start a transition from A -> B..., which should create a fresh cache
    
  1458.     // for the new cache boundary (bc of the different key)
    
  1459.     await act(() => {
    
  1460.       startTransition(() => {
    
  1461.         root.render(
    
  1462.           <Suspense fallback="Loading...">
    
  1463.             <Cache key="B">
    
  1464.               <AsyncText showVersion={true} text="B" />
    
  1465.             </Cache>
    
  1466.           </Suspense>,
    
  1467.         );
    
  1468.       });
    
  1469.     });
    
  1470.     assertLog(['Cache miss! [B]']);
    
  1471.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1472. 
    
  1473.     // ...but cancel by transitioning "back" to A (which we never really left)
    
  1474.     await act(() => {
    
  1475.       startTransition(() => {
    
  1476.         root.render(
    
  1477.           <Suspense fallback="Loading...">
    
  1478.             <Cache key="A">
    
  1479.               <AsyncText showVersion={true} text="A" />
    
  1480.             </Cache>
    
  1481.           </Suspense>,
    
  1482.         );
    
  1483.       });
    
  1484.     });
    
  1485.     assertLog(['A [v1]', 'Cache cleanup: B [v2]']);
    
  1486.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1487. 
    
  1488.     // Unmount children: ...
    
  1489.     await act(() => {
    
  1490.       root.render('Bye!');
    
  1491.     });
    
  1492.     assertLog([]);
    
  1493.     expect(root).toMatchRenderedOutput('Bye!');
    
  1494.   });
    
  1495. 
    
  1496.   // @gate enableCacheElement && enableCache
    
  1497.   test.skip('if a root cache refresh never commits its fresh cache is released', async () => {
    
  1498.     const root = ReactNoop.createRoot();
    
  1499.     let refresh;
    
  1500.     function Example({text}) {
    
  1501.       refresh = useCacheRefresh();
    
  1502.       return <AsyncText showVersion={true} text={text} />;
    
  1503.     }
    
  1504.     seedNextTextCache('A');
    
  1505.     await act(() => {
    
  1506.       root.render(
    
  1507.         <Suspense fallback="Loading...">
    
  1508.           <Example text="A" />
    
  1509.         </Suspense>,
    
  1510.       );
    
  1511.     });
    
  1512.     assertLog(['A [v1]']);
    
  1513.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1514. 
    
  1515.     await act(() => {
    
  1516.       startTransition(() => {
    
  1517.         refresh();
    
  1518.       });
    
  1519.     });
    
  1520.     assertLog(['Cache miss! [A]']);
    
  1521.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1522. 
    
  1523.     await act(() => {
    
  1524.       root.render('Bye!');
    
  1525.     });
    
  1526.     assertLog([
    
  1527.       // TODO: the v1 cache should *not* be cleaned up, it is still retained by the root
    
  1528.       // The following line is presently yielded but should not be:
    
  1529.       // 'Cache cleanup: A [v1]',
    
  1530. 
    
  1531.       // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
    
  1532.       // The following line is presently not yielded but should be:
    
  1533.       'Cache cleanup: A [v2]',
    
  1534.     ]);
    
  1535.     expect(root).toMatchRenderedOutput('Bye!');
    
  1536.   });
    
  1537. 
    
  1538.   // @gate enableCacheElement && enableCache
    
  1539.   test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => {
    
  1540.     const root = ReactNoop.createRoot();
    
  1541.     let refresh;
    
  1542.     function Example({text}) {
    
  1543.       refresh = useCacheRefresh();
    
  1544.       return <AsyncText showVersion={true} text={text} />;
    
  1545.     }
    
  1546.     seedNextTextCache('A');
    
  1547.     await act(() => {
    
  1548.       root.render(
    
  1549.         <Suspense fallback="Loading...">
    
  1550.           <Cache>
    
  1551.             <Example text="A" />
    
  1552.           </Cache>
    
  1553.         </Suspense>,
    
  1554.       );
    
  1555.     });
    
  1556.     assertLog(['A [v1]']);
    
  1557.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1558. 
    
  1559.     await act(() => {
    
  1560.       startTransition(() => {
    
  1561.         refresh();
    
  1562.       });
    
  1563.     });
    
  1564.     assertLog(['Cache miss! [A]']);
    
  1565.     expect(root).toMatchRenderedOutput('A [v1]');
    
  1566. 
    
  1567.     // Unmount the boundary before the refresh can complete
    
  1568.     await act(() => {
    
  1569.       root.render('Bye!');
    
  1570.     });
    
  1571.     assertLog([
    
  1572.       // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
    
  1573.       // The following line is presently not yielded but should be:
    
  1574.       'Cache cleanup: A [v2]',
    
  1575.     ]);
    
  1576.     expect(root).toMatchRenderedOutput('Bye!');
    
  1577.   });
    
  1578. 
    
  1579.   // @gate enableActivity
    
  1580.   // @gate enableCache
    
  1581.   test('prerender a new cache boundary inside an Activity tree', async () => {
    
  1582.     function App({prerenderMore}) {
    
  1583.       return (
    
  1584.         <Activity mode="hidden">
    
  1585.           <div>
    
  1586.             {prerenderMore ? (
    
  1587.               <Cache>
    
  1588.                 <AsyncText text="More" />
    
  1589.               </Cache>
    
  1590.             ) : null}
    
  1591.           </div>
    
  1592.         </Activity>
    
  1593.       );
    
  1594.     }
    
  1595. 
    
  1596.     const root = ReactNoop.createRoot();
    
  1597.     await act(() => {
    
  1598.       root.render(<App prerenderMore={false} />);
    
  1599.     });
    
  1600.     assertLog([]);
    
  1601.     expect(root).toMatchRenderedOutput(<div hidden={true} />);
    
  1602. 
    
  1603.     seedNextTextCache('More');
    
  1604.     await act(() => {
    
  1605.       root.render(<App prerenderMore={true} />);
    
  1606.     });
    
  1607.     assertLog(['More']);
    
  1608.     expect(root).toMatchRenderedOutput(<div hidden={true}>More</div>);
    
  1609.   });
    
  1610. 
    
  1611.   // @gate enableCache
    
  1612.   it('cache objects and primitive arguments and a mix of them', async () => {
    
  1613.     const root = ReactNoop.createRoot();
    
  1614.     const types = cache((a, b) => ({a: typeof a, b: typeof b}));
    
  1615.     function Print({a, b}) {
    
  1616.       return types(a, b).a + ' ' + types(a, b).b + ' ';
    
  1617.     }
    
  1618.     function Same({a, b}) {
    
  1619.       const x = types(a, b);
    
  1620.       const y = types(a, b);
    
  1621.       return (x === y).toString() + ' ';
    
  1622.     }
    
  1623.     function FlippedOrder({a, b}) {
    
  1624.       return (types(a, b) === types(b, a)).toString() + ' ';
    
  1625.     }
    
  1626.     function FewerArgs({a, b}) {
    
  1627.       return (types(a, b) === types(a)).toString() + ' ';
    
  1628.     }
    
  1629.     function MoreArgs({a, b}) {
    
  1630.       return (types(a) === types(a, b)).toString() + ' ';
    
  1631.     }
    
  1632.     await act(() => {
    
  1633.       root.render(
    
  1634.         <>
    
  1635.           <Print a="e" b="f" />
    
  1636.           <Same a="a" b="b" />
    
  1637.           <FlippedOrder a="c" b="d" />
    
  1638.           <FewerArgs a="e" b="f" />
    
  1639.           <MoreArgs a="g" b="h" />
    
  1640.         </>,
    
  1641.       );
    
  1642.     });
    
  1643.     expect(root).toMatchRenderedOutput('string string true false false false ');
    
  1644.     await act(() => {
    
  1645.       root.render(
    
  1646.         <>
    
  1647.           <Print a="e" b={null} />
    
  1648.           <Same a="a" b={null} />
    
  1649.           <FlippedOrder a="c" b={null} />
    
  1650.           <FewerArgs a="e" b={null} />
    
  1651.           <MoreArgs a="g" b={null} />
    
  1652.         </>,
    
  1653.       );
    
  1654.     });
    
  1655.     expect(root).toMatchRenderedOutput('string object true false false false ');
    
  1656.     const obj = {};
    
  1657.     await act(() => {
    
  1658.       root.render(
    
  1659.         <>
    
  1660.           <Print a="e" b={obj} />
    
  1661.           <Same a="a" b={obj} />
    
  1662.           <FlippedOrder a="c" b={obj} />
    
  1663.           <FewerArgs a="e" b={obj} />
    
  1664.           <MoreArgs a="g" b={obj} />
    
  1665.         </>,
    
  1666.       );
    
  1667.     });
    
  1668.     expect(root).toMatchRenderedOutput('string object true false false false ');
    
  1669.     const sameObj = {};
    
  1670.     await act(() => {
    
  1671.       root.render(
    
  1672.         <>
    
  1673.           <Print a={sameObj} b={sameObj} />
    
  1674.           <Same a={sameObj} b={sameObj} />
    
  1675.           <FlippedOrder a={sameObj} b={sameObj} />
    
  1676.           <FewerArgs a={sameObj} b={sameObj} />
    
  1677.           <MoreArgs a={sameObj} b={sameObj} />
    
  1678.         </>,
    
  1679.       );
    
  1680.     });
    
  1681.     expect(root).toMatchRenderedOutput('object object true true false false ');
    
  1682.     const objA = {};
    
  1683.     const objB = {};
    
  1684.     await act(() => {
    
  1685.       root.render(
    
  1686.         <>
    
  1687.           <Print a={objA} b={objB} />
    
  1688.           <Same a={objA} b={objB} />
    
  1689.           <FlippedOrder a={objA} b={objB} />
    
  1690.           <FewerArgs a={objA} b={objB} />
    
  1691.           <MoreArgs a={objA} b={objB} />
    
  1692.         </>,
    
  1693.       );
    
  1694.     });
    
  1695.     expect(root).toMatchRenderedOutput('object object true false false false ');
    
  1696.     const sameSymbol = Symbol();
    
  1697.     await act(() => {
    
  1698.       root.render(
    
  1699.         <>
    
  1700.           <Print a={sameSymbol} b={sameSymbol} />
    
  1701.           <Same a={sameSymbol} b={sameSymbol} />
    
  1702.           <FlippedOrder a={sameSymbol} b={sameSymbol} />
    
  1703.           <FewerArgs a={sameSymbol} b={sameSymbol} />
    
  1704.           <MoreArgs a={sameSymbol} b={sameSymbol} />
    
  1705.         </>,
    
  1706.       );
    
  1707.     });
    
  1708.     expect(root).toMatchRenderedOutput('symbol symbol true true false false ');
    
  1709.     const notANumber = +'nan';
    
  1710.     await act(() => {
    
  1711.       root.render(
    
  1712.         <>
    
  1713.           <Print a={1} b={notANumber} />
    
  1714.           <Same a={1} b={notANumber} />
    
  1715.           <FlippedOrder a={1} b={notANumber} />
    
  1716.           <FewerArgs a={1} b={notANumber} />
    
  1717.           <MoreArgs a={1} b={notANumber} />
    
  1718.         </>,
    
  1719.       );
    
  1720.     });
    
  1721.     expect(root).toMatchRenderedOutput('number number true false false false ');
    
  1722.   });
    
  1723. 
    
  1724.   // @gate enableCache
    
  1725.   it('cached functions that throw should cache the error', async () => {
    
  1726.     const root = ReactNoop.createRoot();
    
  1727.     const throws = cache(v => {
    
  1728.       throw new Error(v);
    
  1729.     });
    
  1730.     let x;
    
  1731.     let y;
    
  1732.     let z;
    
  1733.     function Test() {
    
  1734.       try {
    
  1735.         throws(1);
    
  1736.       } catch (e) {
    
  1737.         x = e;
    
  1738.       }
    
  1739.       try {
    
  1740.         throws(1);
    
  1741.       } catch (e) {
    
  1742.         y = e;
    
  1743.       }
    
  1744.       try {
    
  1745.         throws(2);
    
  1746.       } catch (e) {
    
  1747.         z = e;
    
  1748.       }
    
  1749. 
    
  1750.       return 'Blank';
    
  1751.     }
    
  1752.     await act(() => {
    
  1753.       root.render(<Test />);
    
  1754.     });
    
  1755.     expect(x).toBe(y);
    
  1756.     expect(z).not.toBe(x);
    
  1757.   });
    
  1758. });