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.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {ReactContext, Thenable} from 'shared/ReactTypes';
    
  11. 
    
  12. import * as React from 'react';
    
  13. 
    
  14. import {createLRU} from './LRU';
    
  15. 
    
  16. interface Suspender {
    
  17.   then(resolve: () => mixed, reject: () => mixed): mixed;
    
  18. }
    
  19. 
    
  20. type PendingResult = {
    
  21.   status: 0,
    
  22.   value: Suspender,
    
  23. };
    
  24. 
    
  25. type ResolvedResult<V> = {
    
  26.   status: 1,
    
  27.   value: V,
    
  28. };
    
  29. 
    
  30. type RejectedResult = {
    
  31.   status: 2,
    
  32.   value: mixed,
    
  33. };
    
  34. 
    
  35. type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
    
  36. 
    
  37. type Resource<I, V> = {
    
  38.   read(I): V,
    
  39.   preload(I): void,
    
  40.   ...
    
  41. };
    
  42. 
    
  43. const Pending = 0;
    
  44. const Resolved = 1;
    
  45. const Rejected = 2;
    
  46. 
    
  47. const ReactCurrentDispatcher =
    
  48.   React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
    
  49.     .ReactCurrentDispatcher;
    
  50. 
    
  51. function readContext(Context: ReactContext<mixed>) {
    
  52.   const dispatcher = ReactCurrentDispatcher.current;
    
  53.   if (dispatcher === null) {
    
  54.     // This wasn't being minified but we're going to retire this package anyway.
    
  55.     // eslint-disable-next-line react-internal/prod-error-codes
    
  56.     throw new Error(
    
  57.       'react-cache: read and preload may only be called from within a ' +
    
  58.         "component's render. They are not supported in event handlers or " +
    
  59.         'lifecycle methods.',
    
  60.     );
    
  61.   }
    
  62.   return dispatcher.readContext(Context);
    
  63. }
    
  64. 
    
  65. // $FlowFixMe[missing-local-annot]
    
  66. function identityHashFn(input) {
    
  67.   if (__DEV__) {
    
  68.     if (
    
  69.       typeof input !== 'string' &&
    
  70.       typeof input !== 'number' &&
    
  71.       typeof input !== 'boolean' &&
    
  72.       input !== undefined &&
    
  73.       input !== null
    
  74.     ) {
    
  75.       console.error(
    
  76.         'Invalid key type. Expected a string, number, symbol, or boolean, ' +
    
  77.           'but instead received: %s' +
    
  78.           '\n\nTo use non-primitive values as keys, you must pass a hash ' +
    
  79.           'function as the second argument to createResource().',
    
  80.         input,
    
  81.       );
    
  82.     }
    
  83.   }
    
  84.   return input;
    
  85. }
    
  86. 
    
  87. const CACHE_LIMIT = 500;
    
  88. const lru = createLRU<$FlowFixMe>(CACHE_LIMIT);
    
  89. 
    
  90. const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
    
  91. 
    
  92. const CacheContext = React.createContext<mixed>(null);
    
  93. 
    
  94. function accessResult<I, K, V>(
    
  95.   resource: any,
    
  96.   fetch: I => Thenable<V>,
    
  97.   input: I,
    
  98.   key: K,
    
  99. ): Result<V> {
    
  100.   let entriesForResource = entries.get(resource);
    
  101.   if (entriesForResource === undefined) {
    
  102.     entriesForResource = new Map();
    
  103.     entries.set(resource, entriesForResource);
    
  104.   }
    
  105.   const entry = entriesForResource.get(key);
    
  106.   if (entry === undefined) {
    
  107.     const thenable = fetch(input);
    
  108.     thenable.then(
    
  109.       value => {
    
  110.         if (newResult.status === Pending) {
    
  111.           const resolvedResult: ResolvedResult<V> = (newResult: any);
    
  112.           resolvedResult.status = Resolved;
    
  113.           resolvedResult.value = value;
    
  114.         }
    
  115.       },
    
  116.       error => {
    
  117.         if (newResult.status === Pending) {
    
  118.           const rejectedResult: RejectedResult = (newResult: any);
    
  119.           rejectedResult.status = Rejected;
    
  120.           rejectedResult.value = error;
    
  121.         }
    
  122.       },
    
  123.     );
    
  124.     const newResult: PendingResult = {
    
  125.       status: Pending,
    
  126.       value: thenable,
    
  127.     };
    
  128.     const newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key));
    
  129.     entriesForResource.set(key, newEntry);
    
  130.     return newResult;
    
  131.   } else {
    
  132.     return (lru.access(entry): any);
    
  133.   }
    
  134. }
    
  135. 
    
  136. function deleteEntry(resource: any, key: mixed) {
    
  137.   const entriesForResource = entries.get(resource);
    
  138.   if (entriesForResource !== undefined) {
    
  139.     entriesForResource.delete(key);
    
  140.     if (entriesForResource.size === 0) {
    
  141.       entries.delete(resource);
    
  142.     }
    
  143.   }
    
  144. }
    
  145. 
    
  146. export function unstable_createResource<I, K: string | number, V>(
    
  147.   fetch: I => Thenable<V>,
    
  148.   maybeHashInput?: I => K,
    
  149. ): Resource<I, V> {
    
  150.   const hashInput: I => K =
    
  151.     maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
    
  152. 
    
  153.   const resource = {
    
  154.     read(input: I): V {
    
  155.       // react-cache currently doesn't rely on context, but it may in the
    
  156.       // future, so we read anyway to prevent access outside of render.
    
  157.       readContext(CacheContext);
    
  158.       const key = hashInput(input);
    
  159.       const result: Result<V> = accessResult(resource, fetch, input, key);
    
  160.       switch (result.status) {
    
  161.         case Pending: {
    
  162.           const suspender = result.value;
    
  163.           throw suspender;
    
  164.         }
    
  165.         case Resolved: {
    
  166.           const value = result.value;
    
  167.           return value;
    
  168.         }
    
  169.         case Rejected: {
    
  170.           const error = result.value;
    
  171.           throw error;
    
  172.         }
    
  173.         default:
    
  174.           // Should be unreachable
    
  175.           return (undefined: any);
    
  176.       }
    
  177.     },
    
  178. 
    
  179.     preload(input: I): void {
    
  180.       // react-cache currently doesn't rely on context, but it may in the
    
  181.       // future, so we read anyway to prevent access outside of render.
    
  182.       readContext(CacheContext);
    
  183.       const key = hashInput(input);
    
  184.       accessResult(resource, fetch, input, key);
    
  185.     },
    
  186.   };
    
  187.   return resource;
    
  188. }
    
  189. 
    
  190. export function unstable_setGlobalCacheLimit(limit: number) {
    
  191.   lru.setLimit(limit);
    
  192. }