1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. let ReactCache;
    
  13. let createResource;
    
  14. let React;
    
  15. let ReactFeatureFlags;
    
  16. let ReactTestRenderer;
    
  17. let Scheduler;
    
  18. let Suspense;
    
  19. let TextResource;
    
  20. let textResourceShouldFail;
    
  21. let waitForAll;
    
  22. let assertLog;
    
  23. let waitForThrow;
    
  24. let act;
    
  25. 
    
  26. describe('ReactCache', () => {
    
  27.   beforeEach(() => {
    
  28.     jest.resetModules();
    
  29. 
    
  30.     ReactFeatureFlags = require('shared/ReactFeatureFlags');
    
  31. 
    
  32.     ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
    
  33.     React = require('react');
    
  34.     Suspense = React.Suspense;
    
  35.     ReactCache = require('react-cache');
    
  36.     createResource = ReactCache.unstable_createResource;
    
  37.     ReactTestRenderer = require('react-test-renderer');
    
  38.     Scheduler = require('scheduler');
    
  39. 
    
  40.     const InternalTestUtils = require('internal-test-utils');
    
  41.     waitForAll = InternalTestUtils.waitForAll;
    
  42.     assertLog = InternalTestUtils.assertLog;
    
  43.     waitForThrow = InternalTestUtils.waitForThrow;
    
  44.     act = InternalTestUtils.act;
    
  45. 
    
  46.     TextResource = createResource(
    
  47.       ([text, ms = 0]) => {
    
  48.         let listeners = null;
    
  49.         let status = 'pending';
    
  50.         let value = null;
    
  51.         return {
    
  52.           then(resolve, reject) {
    
  53.             switch (status) {
    
  54.               case 'pending': {
    
  55.                 if (listeners === null) {
    
  56.                   listeners = [{resolve, reject}];
    
  57.                   setTimeout(() => {
    
  58.                     if (textResourceShouldFail) {
    
  59.                       Scheduler.log(`Promise rejected [${text}]`);
    
  60.                       status = 'rejected';
    
  61.                       value = new Error('Failed to load: ' + text);
    
  62.                       listeners.forEach(listener => listener.reject(value));
    
  63.                     } else {
    
  64.                       Scheduler.log(`Promise resolved [${text}]`);
    
  65.                       status = 'resolved';
    
  66.                       value = text;
    
  67.                       listeners.forEach(listener => listener.resolve(value));
    
  68.                     }
    
  69.                   }, ms);
    
  70.                 } else {
    
  71.                   listeners.push({resolve, reject});
    
  72.                 }
    
  73.                 break;
    
  74.               }
    
  75.               case 'resolved': {
    
  76.                 resolve(value);
    
  77.                 break;
    
  78.               }
    
  79.               case 'rejected': {
    
  80.                 reject(value);
    
  81.                 break;
    
  82.               }
    
  83.             }
    
  84.           },
    
  85.         };
    
  86.       },
    
  87.       ([text, ms]) => text,
    
  88.     );
    
  89. 
    
  90.     textResourceShouldFail = false;
    
  91.   });
    
  92. 
    
  93.   function Text(props) {
    
  94.     Scheduler.log(props.text);
    
  95.     return props.text;
    
  96.   }
    
  97. 
    
  98.   function AsyncText(props) {
    
  99.     const text = props.text;
    
  100.     try {
    
  101.       TextResource.read([props.text, props.ms]);
    
  102.       Scheduler.log(text);
    
  103.       return text;
    
  104.     } catch (promise) {
    
  105.       if (typeof promise.then === 'function') {
    
  106.         Scheduler.log(`Suspend! [${text}]`);
    
  107.       } else {
    
  108.         Scheduler.log(`Error! [${text}]`);
    
  109.       }
    
  110.       throw promise;
    
  111.     }
    
  112.   }
    
  113. 
    
  114.   it('throws a promise if the requested value is not in the cache', async () => {
    
  115.     function App() {
    
  116.       return (
    
  117.         <Suspense fallback={<Text text="Loading..." />}>
    
  118.           <AsyncText ms={100} text="Hi" />
    
  119.         </Suspense>
    
  120.       );
    
  121.     }
    
  122. 
    
  123.     ReactTestRenderer.create(<App />, {
    
  124.       unstable_isConcurrent: true,
    
  125.     });
    
  126. 
    
  127.     await waitForAll(['Suspend! [Hi]', 'Loading...']);
    
  128. 
    
  129.     jest.advanceTimersByTime(100);
    
  130.     assertLog(['Promise resolved [Hi]']);
    
  131.     await waitForAll(['Hi']);
    
  132.   });
    
  133. 
    
  134.   it('throws an error on the subsequent read if the promise is rejected', async () => {
    
  135.     function App() {
    
  136.       return (
    
  137.         <Suspense fallback={<Text text="Loading..." />}>
    
  138.           <AsyncText ms={100} text="Hi" />
    
  139.         </Suspense>
    
  140.       );
    
  141.     }
    
  142. 
    
  143.     const root = ReactTestRenderer.create(<App />, {
    
  144.       unstable_isConcurrent: true,
    
  145.     });
    
  146. 
    
  147.     await waitForAll(['Suspend! [Hi]', 'Loading...']);
    
  148. 
    
  149.     textResourceShouldFail = true;
    
  150.     let error;
    
  151.     try {
    
  152.       await act(() => jest.advanceTimersByTime(100));
    
  153.     } catch (e) {
    
  154.       error = e;
    
  155.     }
    
  156.     expect(error.message).toMatch('Failed to load: Hi');
    
  157.     assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
    
  158. 
    
  159.     // Should throw again on a subsequent read
    
  160.     root.update(<App />);
    
  161.     await waitForThrow('Failed to load: Hi');
    
  162.     assertLog(['Error! [Hi]', 'Error! [Hi]']);
    
  163.   });
    
  164. 
    
  165.   it('warns if non-primitive key is passed to a resource without a hash function', async () => {
    
  166.     const BadTextResource = createResource(([text, ms = 0]) => {
    
  167.       return new Promise((resolve, reject) =>
    
  168.         setTimeout(() => {
    
  169.           resolve(text);
    
  170.         }, ms),
    
  171.       );
    
  172.     });
    
  173. 
    
  174.     function App() {
    
  175.       Scheduler.log('App');
    
  176.       return BadTextResource.read(['Hi', 100]);
    
  177.     }
    
  178. 
    
  179.     ReactTestRenderer.create(
    
  180.       <Suspense fallback={<Text text="Loading..." />}>
    
  181.         <App />
    
  182.       </Suspense>,
    
  183.       {
    
  184.         unstable_isConcurrent: true,
    
  185.       },
    
  186.     );
    
  187. 
    
  188.     if (__DEV__) {
    
  189.       await expect(async () => {
    
  190.         await waitForAll(['App', 'Loading...']);
    
  191.       }).toErrorDev([
    
  192.         'Invalid key type. Expected a string, number, symbol, or ' +
    
  193.           'boolean, but instead received: Hi,100\n\n' +
    
  194.           'To use non-primitive values as keys, you must pass a hash ' +
    
  195.           'function as the second argument to createResource().',
    
  196.       ]);
    
  197.     } else {
    
  198.       await waitForAll(['App', 'Loading...']);
    
  199.     }
    
  200.   });
    
  201. 
    
  202.   it('evicts least recently used values', async () => {
    
  203.     ReactCache.unstable_setGlobalCacheLimit(3);
    
  204. 
    
  205.     // Render 1, 2, and 3
    
  206.     const root = ReactTestRenderer.create(
    
  207.       <Suspense fallback={<Text text="Loading..." />}>
    
  208.         <AsyncText ms={100} text={1} />
    
  209.         <AsyncText ms={100} text={2} />
    
  210.         <AsyncText ms={100} text={3} />
    
  211.       </Suspense>,
    
  212.       {
    
  213.         unstable_isConcurrent: true,
    
  214.       },
    
  215.     );
    
  216.     await waitForAll(['Suspend! [1]', 'Loading...']);
    
  217.     jest.advanceTimersByTime(100);
    
  218.     assertLog(['Promise resolved [1]']);
    
  219.     await waitForAll([1, 'Suspend! [2]']);
    
  220. 
    
  221.     jest.advanceTimersByTime(100);
    
  222.     assertLog(['Promise resolved [2]']);
    
  223.     await waitForAll([1, 2, 'Suspend! [3]']);
    
  224. 
    
  225.     await act(() => jest.advanceTimersByTime(100));
    
  226.     assertLog(['Promise resolved [3]', 1, 2, 3]);
    
  227. 
    
  228.     expect(root).toMatchRenderedOutput('123');
    
  229. 
    
  230.     // Render 1, 4, 5
    
  231.     root.update(
    
  232.       <Suspense fallback={<Text text="Loading..." />}>
    
  233.         <AsyncText ms={100} text={1} />
    
  234.         <AsyncText ms={100} text={4} />
    
  235.         <AsyncText ms={100} text={5} />
    
  236.       </Suspense>,
    
  237.     );
    
  238. 
    
  239.     await waitForAll([1, 'Suspend! [4]', 'Loading...']);
    
  240. 
    
  241.     await act(() => jest.advanceTimersByTime(100));
    
  242.     assertLog([
    
  243.       'Promise resolved [4]',
    
  244.       1,
    
  245.       4,
    
  246.       'Suspend! [5]',
    
  247.       'Promise resolved [5]',
    
  248.       1,
    
  249.       4,
    
  250.       5,
    
  251.     ]);
    
  252. 
    
  253.     expect(root).toMatchRenderedOutput('145');
    
  254. 
    
  255.     // We've now rendered values 1, 2, 3, 4, 5, over our limit of 3. The least
    
  256.     // recently used values are 2 and 3. They should have been evicted.
    
  257. 
    
  258.     root.update(
    
  259.       <Suspense fallback={<Text text="Loading..." />}>
    
  260.         <AsyncText ms={100} text={1} />
    
  261.         <AsyncText ms={100} text={2} />
    
  262.         <AsyncText ms={100} text={3} />
    
  263.       </Suspense>,
    
  264.     );
    
  265. 
    
  266.     await waitForAll([
    
  267.       // 1 is still cached
    
  268.       1,
    
  269.       // 2 and 3 suspend because they were evicted from the cache
    
  270.       'Suspend! [2]',
    
  271.       'Loading...',
    
  272.     ]);
    
  273. 
    
  274.     await act(() => jest.advanceTimersByTime(100));
    
  275.     assertLog([
    
  276.       'Promise resolved [2]',
    
  277.       1,
    
  278.       2,
    
  279.       'Suspend! [3]',
    
  280.       'Promise resolved [3]',
    
  281.       1,
    
  282.       2,
    
  283.       3,
    
  284.     ]);
    
  285.     expect(root).toMatchRenderedOutput('123');
    
  286.   });
    
  287. 
    
  288.   it('preloads during the render phase', async () => {
    
  289.     function App() {
    
  290.       TextResource.preload(['B', 1000]);
    
  291.       TextResource.read(['A', 1000]);
    
  292.       TextResource.read(['B', 1000]);
    
  293.       return <Text text="Result" />;
    
  294.     }
    
  295. 
    
  296.     const root = ReactTestRenderer.create(
    
  297.       <Suspense fallback={<Text text="Loading..." />}>
    
  298.         <App />
    
  299.       </Suspense>,
    
  300.       {
    
  301.         unstable_isConcurrent: true,
    
  302.       },
    
  303.     );
    
  304. 
    
  305.     await waitForAll(['Loading...']);
    
  306. 
    
  307.     await act(() => jest.advanceTimersByTime(1000));
    
  308.     assertLog(['Promise resolved [B]', 'Promise resolved [A]', 'Result']);
    
  309.     expect(root).toMatchRenderedOutput('Result');
    
  310.   });
    
  311. 
    
  312.   it('if a thenable resolves multiple times, does not update the first cached value', async () => {
    
  313.     let resolveThenable;
    
  314.     const BadTextResource = createResource(
    
  315.       ([text, ms = 0]) => {
    
  316.         let listeners = null;
    
  317.         const value = null;
    
  318.         return {
    
  319.           then(resolve, reject) {
    
  320.             if (value !== null) {
    
  321.               resolve(value);
    
  322.             } else {
    
  323.               if (listeners === null) {
    
  324.                 listeners = [resolve];
    
  325.                 resolveThenable = v => {
    
  326.                   listeners.forEach(listener => listener(v));
    
  327.                 };
    
  328.               } else {
    
  329.                 listeners.push(resolve);
    
  330.               }
    
  331.             }
    
  332.           },
    
  333.         };
    
  334.       },
    
  335.       ([text, ms]) => text,
    
  336.     );
    
  337. 
    
  338.     function BadAsyncText(props) {
    
  339.       const text = props.text;
    
  340.       try {
    
  341.         const actualText = BadTextResource.read([props.text, props.ms]);
    
  342.         Scheduler.log(actualText);
    
  343.         return actualText;
    
  344.       } catch (promise) {
    
  345.         if (typeof promise.then === 'function') {
    
  346.           Scheduler.log(`Suspend! [${text}]`);
    
  347.         } else {
    
  348.           Scheduler.log(`Error! [${text}]`);
    
  349.         }
    
  350.         throw promise;
    
  351.       }
    
  352.     }
    
  353. 
    
  354.     const root = ReactTestRenderer.create(
    
  355.       <Suspense fallback={<Text text="Loading..." />}>
    
  356.         <BadAsyncText text="Hi" />
    
  357.       </Suspense>,
    
  358.       {
    
  359.         unstable_isConcurrent: true,
    
  360.       },
    
  361.     );
    
  362. 
    
  363.     await waitForAll(['Suspend! [Hi]', 'Loading...']);
    
  364. 
    
  365.     resolveThenable('Hi');
    
  366.     // This thenable improperly resolves twice. We should not update the
    
  367.     // cached value.
    
  368.     resolveThenable('Hi muahahaha I am different');
    
  369. 
    
  370.     root.update(
    
  371.       <Suspense fallback={<Text text="Loading..." />}>
    
  372.         <BadAsyncText text="Hi" />
    
  373.       </Suspense>,
    
  374.       {
    
  375.         unstable_isConcurrent: true,
    
  376.       },
    
  377.     );
    
  378. 
    
  379.     assertLog([]);
    
  380.     await waitForAll(['Hi']);
    
  381.     expect(root).toMatchRenderedOutput('Hi');
    
  382.   });
    
  383. 
    
  384.   it('throws if read is called outside render', () => {
    
  385.     expect(() => TextResource.read(['A', 1000])).toThrow(
    
  386.       "read and preload may only be called from within a component's render",
    
  387.     );
    
  388.   });
    
  389. 
    
  390.   it('throws if preload is called outside render', () => {
    
  391.     expect(() => TextResource.preload(['A', 1000])).toThrow(
    
  392.       "read and preload may only be called from within a component's render",
    
  393.     );
    
  394.   });
    
  395. });