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. // Corresponds to ReactFiberWakeable and ReactFlightWakeable modules. Generally,
    
  11. // changes to one module should be reflected in the others.
    
  12. 
    
  13. // TODO: Rename this module and the corresponding Fiber one to "Thenable"
    
  14. // instead of "Wakeable". Or some other more appropriate name.
    
  15. 
    
  16. import type {
    
  17.   Thenable,
    
  18.   PendingThenable,
    
  19.   FulfilledThenable,
    
  20.   RejectedThenable,
    
  21. } from 'shared/ReactTypes';
    
  22. 
    
  23. export opaque type ThenableState = Array<Thenable<any>>;
    
  24. 
    
  25. // An error that is thrown (e.g. by `use`) to trigger Suspense. If we
    
  26. // detect this is caught by userspace, we'll log a warning in development.
    
  27. export const SuspenseException: mixed = new Error(
    
  28.   "Suspense Exception: This is not a real error! It's an implementation " +
    
  29.     'detail of `use` to interrupt the current render. You must either ' +
    
  30.     'rethrow it immediately, or move the `use` call outside of the ' +
    
  31.     '`try/catch` block. Capturing without rethrowing will lead to ' +
    
  32.     'unexpected behavior.\n\n' +
    
  33.     'To handle async errors, wrap your component in an error boundary, or ' +
    
  34.     "call the promise's `.catch` method and pass the result to `use`",
    
  35. );
    
  36. 
    
  37. export function createThenableState(): ThenableState {
    
  38.   // The ThenableState is created the first time a component suspends. If it
    
  39.   // suspends again, we'll reuse the same state.
    
  40.   return [];
    
  41. }
    
  42. 
    
  43. function noop(): void {}
    
  44. 
    
  45. export function trackUsedThenable<T>(
    
  46.   thenableState: ThenableState,
    
  47.   thenable: Thenable<T>,
    
  48.   index: number,
    
  49. ): T {
    
  50.   const previous = thenableState[index];
    
  51.   if (previous === undefined) {
    
  52.     thenableState.push(thenable);
    
  53.   } else {
    
  54.     if (previous !== thenable) {
    
  55.       // Reuse the previous thenable, and drop the new one. We can assume
    
  56.       // they represent the same value, because components are idempotent.
    
  57. 
    
  58.       // Avoid an unhandled rejection errors for the Promises that we'll
    
  59.       // intentionally ignore.
    
  60.       thenable.then(noop, noop);
    
  61.       thenable = previous;
    
  62.     }
    
  63.   }
    
  64. 
    
  65.   // We use an expando to track the status and result of a thenable so that we
    
  66.   // can synchronously unwrap the value. Think of this as an extension of the
    
  67.   // Promise API, or a custom interface that is a superset of Thenable.
    
  68.   //
    
  69.   // If the thenable doesn't have a status, set it to "pending" and attach
    
  70.   // a listener that will update its status and result when it resolves.
    
  71.   switch (thenable.status) {
    
  72.     case 'fulfilled': {
    
  73.       const fulfilledValue: T = thenable.value;
    
  74.       return fulfilledValue;
    
  75.     }
    
  76.     case 'rejected': {
    
  77.       const rejectedError = thenable.reason;
    
  78.       throw rejectedError;
    
  79.     }
    
  80.     default: {
    
  81.       if (typeof thenable.status === 'string') {
    
  82.         // Only instrument the thenable if the status if not defined. If
    
  83.         // it's defined, but an unknown value, assume it's been instrumented by
    
  84.         // some custom userspace implementation. We treat it as "pending".
    
  85.       } else {
    
  86.         const pendingThenable: PendingThenable<T> = (thenable: any);
    
  87.         pendingThenable.status = 'pending';
    
  88.         pendingThenable.then(
    
  89.           fulfilledValue => {
    
  90.             if (thenable.status === 'pending') {
    
  91.               const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
    
  92.               fulfilledThenable.status = 'fulfilled';
    
  93.               fulfilledThenable.value = fulfilledValue;
    
  94.             }
    
  95.           },
    
  96.           (error: mixed) => {
    
  97.             if (thenable.status === 'pending') {
    
  98.               const rejectedThenable: RejectedThenable<T> = (thenable: any);
    
  99.               rejectedThenable.status = 'rejected';
    
  100.               rejectedThenable.reason = error;
    
  101.             }
    
  102.           },
    
  103.         );
    
  104. 
    
  105.         // Check one more time in case the thenable resolved synchronously
    
  106.         switch (thenable.status) {
    
  107.           case 'fulfilled': {
    
  108.             const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
    
  109.             return fulfilledThenable.value;
    
  110.           }
    
  111.           case 'rejected': {
    
  112.             const rejectedThenable: RejectedThenable<T> = (thenable: any);
    
  113.             throw rejectedThenable.reason;
    
  114.           }
    
  115.         }
    
  116.       }
    
  117. 
    
  118.       // Suspend.
    
  119.       //
    
  120.       // Throwing here is an implementation detail that allows us to unwind the
    
  121.       // call stack. But we shouldn't allow it to leak into userspace. Throw an
    
  122.       // opaque placeholder value instead of the actual thenable. If it doesn't
    
  123.       // get captured by the work loop, log a warning, because that means
    
  124.       // something in userspace must have caught it.
    
  125.       suspendedThenable = thenable;
    
  126.       throw SuspenseException;
    
  127.     }
    
  128.   }
    
  129. }
    
  130. 
    
  131. // This is used to track the actual thenable that suspended so it can be
    
  132. // passed to the rest of the Suspense implementation — which, for historical
    
  133. // reasons, expects to receive a thenable.
    
  134. let suspendedThenable: Thenable<any> | null = null;
    
  135. export function getSuspendedThenable(): Thenable<mixed> {
    
  136.   // This is called right after `use` suspends by throwing an exception. `use`
    
  137.   // throws an opaque value instead of the thenable itself so that it can't be
    
  138.   // caught in userspace. Then the work loop accesses the actual thenable using
    
  139.   // this function.
    
  140.   if (suspendedThenable === null) {
    
  141.     throw new Error(
    
  142.       'Expected a suspended thenable. This is a bug in React. Please file ' +
    
  143.         'an issue.',
    
  144.     );
    
  145.   }
    
  146.   const thenable = suspendedThenable;
    
  147.   suspendedThenable = null;
    
  148.   return thenable;
    
  149. }