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 {
    
  11.   Thenable,
    
  12.   PendingThenable,
    
  13.   FulfilledThenable,
    
  14.   RejectedThenable,
    
  15. } from 'shared/ReactTypes';
    
  16. 
    
  17. import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
    
  18. 
    
  19. import ReactSharedInternals from 'shared/ReactSharedInternals';
    
  20. const {ReactCurrentActQueue} = ReactSharedInternals;
    
  21. 
    
  22. export opaque type ThenableState = Array<Thenable<any>>;
    
  23. 
    
  24. // An error that is thrown (e.g. by `use`) to trigger Suspense. If we
    
  25. // detect this is caught by userspace, we'll log a warning in development.
    
  26. export const SuspenseException: mixed = new Error(
    
  27.   "Suspense Exception: This is not a real error! It's an implementation " +
    
  28.     'detail of `use` to interrupt the current render. You must either ' +
    
  29.     'rethrow it immediately, or move the `use` call outside of the ' +
    
  30.     '`try/catch` block. Capturing without rethrowing will lead to ' +
    
  31.     'unexpected behavior.\n\n' +
    
  32.     'To handle async errors, wrap your component in an error boundary, or ' +
    
  33.     "call the promise's `.catch` method and pass the result to `use`",
    
  34. );
    
  35. 
    
  36. export const SuspenseyCommitException: mixed = new Error(
    
  37.   'Suspense Exception: This is not a real error, and should not leak into ' +
    
  38.     "userspace. If you're seeing this, it's likely a bug in React.",
    
  39. );
    
  40. 
    
  41. // This is a noop thenable that we use to trigger a fallback in throwException.
    
  42. // TODO: It would be better to refactor throwException into multiple functions
    
  43. // so we can trigger a fallback directly without having to check the type. But
    
  44. // for now this will do.
    
  45. export const noopSuspenseyCommitThenable = {
    
  46.   then() {
    
  47.     if (__DEV__) {
    
  48.       console.error(
    
  49.         'Internal React error: A listener was unexpectedly attached to a ' +
    
  50.           '"noop" thenable. This is a bug in React. Please file an issue.',
    
  51.       );
    
  52.     }
    
  53.   },
    
  54. };
    
  55. 
    
  56. export function createThenableState(): ThenableState {
    
  57.   // The ThenableState is created the first time a component suspends. If it
    
  58.   // suspends again, we'll reuse the same state.
    
  59.   return [];
    
  60. }
    
  61. 
    
  62. export function isThenableResolved(thenable: Thenable<mixed>): boolean {
    
  63.   const status = thenable.status;
    
  64.   return status === 'fulfilled' || status === 'rejected';
    
  65. }
    
  66. 
    
  67. function noop(): void {}
    
  68. 
    
  69. export function trackUsedThenable<T>(
    
  70.   thenableState: ThenableState,
    
  71.   thenable: Thenable<T>,
    
  72.   index: number,
    
  73. ): T {
    
  74.   if (__DEV__ && ReactCurrentActQueue.current !== null) {
    
  75.     ReactCurrentActQueue.didUsePromise = true;
    
  76.   }
    
  77. 
    
  78.   const previous = thenableState[index];
    
  79.   if (previous === undefined) {
    
  80.     thenableState.push(thenable);
    
  81.   } else {
    
  82.     if (previous !== thenable) {
    
  83.       // Reuse the previous thenable, and drop the new one. We can assume
    
  84.       // they represent the same value, because components are idempotent.
    
  85. 
    
  86.       // Avoid an unhandled rejection errors for the Promises that we'll
    
  87.       // intentionally ignore.
    
  88.       thenable.then(noop, noop);
    
  89.       thenable = previous;
    
  90.     }
    
  91.   }
    
  92. 
    
  93.   // We use an expando to track the status and result of a thenable so that we
    
  94.   // can synchronously unwrap the value. Think of this as an extension of the
    
  95.   // Promise API, or a custom interface that is a superset of Thenable.
    
  96.   //
    
  97.   // If the thenable doesn't have a status, set it to "pending" and attach
    
  98.   // a listener that will update its status and result when it resolves.
    
  99.   switch (thenable.status) {
    
  100.     case 'fulfilled': {
    
  101.       const fulfilledValue: T = thenable.value;
    
  102.       return fulfilledValue;
    
  103.     }
    
  104.     case 'rejected': {
    
  105.       const rejectedError = thenable.reason;
    
  106.       checkIfUseWrappedInAsyncCatch(rejectedError);
    
  107.       throw rejectedError;
    
  108.     }
    
  109.     default: {
    
  110.       if (typeof thenable.status === 'string') {
    
  111.         // Only instrument the thenable if the status if not defined. If
    
  112.         // it's defined, but an unknown value, assume it's been instrumented by
    
  113.         // some custom userspace implementation. We treat it as "pending".
    
  114.         // Attach a dummy listener, to ensure that any lazy initialization can
    
  115.         // happen. Flight lazily parses JSON when the value is actually awaited.
    
  116.         thenable.then(noop, noop);
    
  117.       } else {
    
  118.         // This is an uncached thenable that we haven't seen before.
    
  119. 
    
  120.         // Detect infinite ping loops caused by uncached promises.
    
  121.         const root = getWorkInProgressRoot();
    
  122.         if (root !== null && root.shellSuspendCounter > 100) {
    
  123.           // This root has suspended repeatedly in the shell without making any
    
  124.           // progress (i.e. committing something). This is highly suggestive of
    
  125.           // an infinite ping loop, often caused by an accidental Async Client
    
  126.           // Component.
    
  127.           //
    
  128.           // During a transition, we can suspend the work loop until the promise
    
  129.           // to resolve, but this is a sync render, so that's not an option. We
    
  130.           // also can't show a fallback, because none was provided. So our last
    
  131.           // resort is to throw an error.
    
  132.           //
    
  133.           // TODO: Remove this error in a future release. Other ways of handling
    
  134.           // this case include forcing a concurrent render, or putting the whole
    
  135.           // root into offscreen mode.
    
  136.           throw new Error(
    
  137.             'async/await is not yet supported in Client Components, only ' +
    
  138.               'Server Components. This error is often caused by accidentally ' +
    
  139.               "adding `'use client'` to a module that was originally written " +
    
  140.               'for the server.',
    
  141.           );
    
  142.         }
    
  143. 
    
  144.         const pendingThenable: PendingThenable<T> = (thenable: any);
    
  145.         pendingThenable.status = 'pending';
    
  146.         pendingThenable.then(
    
  147.           fulfilledValue => {
    
  148.             if (thenable.status === 'pending') {
    
  149.               const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
    
  150.               fulfilledThenable.status = 'fulfilled';
    
  151.               fulfilledThenable.value = fulfilledValue;
    
  152.             }
    
  153.           },
    
  154.           (error: mixed) => {
    
  155.             if (thenable.status === 'pending') {
    
  156.               const rejectedThenable: RejectedThenable<T> = (thenable: any);
    
  157.               rejectedThenable.status = 'rejected';
    
  158.               rejectedThenable.reason = error;
    
  159.             }
    
  160.           },
    
  161.         );
    
  162. 
    
  163.         // Check one more time in case the thenable resolved synchronously.
    
  164.         switch (thenable.status) {
    
  165.           case 'fulfilled': {
    
  166.             const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
    
  167.             return fulfilledThenable.value;
    
  168.           }
    
  169.           case 'rejected': {
    
  170.             const rejectedThenable: RejectedThenable<T> = (thenable: any);
    
  171.             const rejectedError = rejectedThenable.reason;
    
  172.             checkIfUseWrappedInAsyncCatch(rejectedError);
    
  173.             throw rejectedError;
    
  174.           }
    
  175.         }
    
  176.       }
    
  177. 
    
  178.       // Suspend.
    
  179.       //
    
  180.       // Throwing here is an implementation detail that allows us to unwind the
    
  181.       // call stack. But we shouldn't allow it to leak into userspace. Throw an
    
  182.       // opaque placeholder value instead of the actual thenable. If it doesn't
    
  183.       // get captured by the work loop, log a warning, because that means
    
  184.       // something in userspace must have caught it.
    
  185.       suspendedThenable = thenable;
    
  186.       if (__DEV__) {
    
  187.         needsToResetSuspendedThenableDEV = true;
    
  188.       }
    
  189.       throw SuspenseException;
    
  190.     }
    
  191.   }
    
  192. }
    
  193. 
    
  194. export function suspendCommit(): void {
    
  195.   // This extra indirection only exists so it can handle passing
    
  196.   // noopSuspenseyCommitThenable through to throwException.
    
  197.   // TODO: Factor the thenable check out of throwException
    
  198.   suspendedThenable = noopSuspenseyCommitThenable;
    
  199.   throw SuspenseyCommitException;
    
  200. }
    
  201. 
    
  202. // This is used to track the actual thenable that suspended so it can be
    
  203. // passed to the rest of the Suspense implementation — which, for historical
    
  204. // reasons, expects to receive a thenable.
    
  205. let suspendedThenable: Thenable<any> | null = null;
    
  206. let needsToResetSuspendedThenableDEV = false;
    
  207. export function getSuspendedThenable(): Thenable<mixed> {
    
  208.   // This is called right after `use` suspends by throwing an exception. `use`
    
  209.   // throws an opaque value instead of the thenable itself so that it can't be
    
  210.   // caught in userspace. Then the work loop accesses the actual thenable using
    
  211.   // this function.
    
  212.   if (suspendedThenable === null) {
    
  213.     throw new Error(
    
  214.       'Expected a suspended thenable. This is a bug in React. Please file ' +
    
  215.         'an issue.',
    
  216.     );
    
  217.   }
    
  218.   const thenable = suspendedThenable;
    
  219.   suspendedThenable = null;
    
  220.   if (__DEV__) {
    
  221.     needsToResetSuspendedThenableDEV = false;
    
  222.   }
    
  223.   return thenable;
    
  224. }
    
  225. 
    
  226. export function checkIfUseWrappedInTryCatch(): boolean {
    
  227.   if (__DEV__) {
    
  228.     // This was set right before SuspenseException was thrown, and it should
    
  229.     // have been cleared when the exception was handled. If it wasn't,
    
  230.     // it must have been caught by userspace.
    
  231.     if (needsToResetSuspendedThenableDEV) {
    
  232.       needsToResetSuspendedThenableDEV = false;
    
  233.       return true;
    
  234.     }
    
  235.   }
    
  236.   return false;
    
  237. }
    
  238. 
    
  239. export function checkIfUseWrappedInAsyncCatch(rejectedReason: any) {
    
  240.   // This check runs in prod, too, because it prevents a more confusing
    
  241.   // downstream error, where SuspenseException is caught by a promise and
    
  242.   // thrown asynchronously.
    
  243.   // TODO: Another way to prevent SuspenseException from leaking into an async
    
  244.   // execution context is to check the dispatcher every time `use` is called,
    
  245.   // or some equivalent. That might be preferable for other reasons, too, since
    
  246.   // it matches how we prevent similar mistakes for other hooks.
    
  247.   if (rejectedReason === SuspenseException) {
    
  248.     throw new Error(
    
  249.       'Hooks are not supported inside an async component. This ' +
    
  250.         "error is often caused by accidentally adding `'use client'` " +
    
  251.         'to a module that was originally written for the server.',
    
  252.     );
    
  253.   }
    
  254. }