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. import type {Lane} from './ReactFiberLane';
    
  17. 
    
  18. import {requestTransitionLane} from './ReactFiberRootScheduler';
    
  19. import {NoLane} from './ReactFiberLane';
    
  20. 
    
  21. // If there are multiple, concurrent async actions, they are entangled. All
    
  22. // transition updates that occur while the async action is still in progress
    
  23. // are treated as part of the action.
    
  24. //
    
  25. // The ideal behavior would be to treat each async function as an independent
    
  26. // action. However, without a mechanism like AsyncContext, we can't tell which
    
  27. // action an update corresponds to. So instead, we entangle them all into one.
    
  28. 
    
  29. // The listeners to notify once the entangled scope completes.
    
  30. let currentEntangledListeners: Array<() => mixed> | null = null;
    
  31. // The number of pending async actions in the entangled scope.
    
  32. let currentEntangledPendingCount: number = 0;
    
  33. // The transition lane shared by all updates in the entangled scope.
    
  34. let currentEntangledLane: Lane = NoLane;
    
  35. 
    
  36. export function requestAsyncActionContext<S>(
    
  37.   actionReturnValue: Thenable<any>,
    
  38.   // If this is provided, this resulting thenable resolves to this value instead
    
  39.   // of the return value of the action. This is a perf trick to avoid composing
    
  40.   // an extra async function.
    
  41.   overrideReturnValue: S | null,
    
  42. ): Thenable<S> {
    
  43.   // This is an async action.
    
  44.   //
    
  45.   // Return a thenable that resolves once the action scope (i.e. the async
    
  46.   // function passed to startTransition) has finished running.
    
  47. 
    
  48.   const thenable: Thenable<S> = (actionReturnValue: any);
    
  49.   let entangledListeners;
    
  50.   if (currentEntangledListeners === null) {
    
  51.     // There's no outer async action scope. Create a new one.
    
  52.     entangledListeners = currentEntangledListeners = [];
    
  53.     currentEntangledPendingCount = 0;
    
  54.     currentEntangledLane = requestTransitionLane();
    
  55.   } else {
    
  56.     entangledListeners = currentEntangledListeners;
    
  57.   }
    
  58. 
    
  59.   currentEntangledPendingCount++;
    
  60. 
    
  61.   // Create a thenable that represents the result of this action, but doesn't
    
  62.   // resolve until the entire entangled scope has finished.
    
  63.   //
    
  64.   // Expressed using promises:
    
  65.   //   const [thisResult] = await Promise.all([thisAction, entangledAction]);
    
  66.   //   return thisResult;
    
  67.   const resultThenable = createResultThenable<S>(entangledListeners);
    
  68. 
    
  69.   let resultStatus = 'pending';
    
  70.   let resultValue;
    
  71.   let rejectedReason;
    
  72.   thenable.then(
    
  73.     (value: S) => {
    
  74.       resultStatus = 'fulfilled';
    
  75.       resultValue = overrideReturnValue !== null ? overrideReturnValue : value;
    
  76.       pingEngtangledActionScope();
    
  77.     },
    
  78.     error => {
    
  79.       resultStatus = 'rejected';
    
  80.       rejectedReason = error;
    
  81.       pingEngtangledActionScope();
    
  82.     },
    
  83.   );
    
  84. 
    
  85.   // Attach a listener to fill in the result.
    
  86.   entangledListeners.push(() => {
    
  87.     switch (resultStatus) {
    
  88.       case 'fulfilled': {
    
  89.         const fulfilledThenable: FulfilledThenable<S> = (resultThenable: any);
    
  90.         fulfilledThenable.status = 'fulfilled';
    
  91.         fulfilledThenable.value = resultValue;
    
  92.         break;
    
  93.       }
    
  94.       case 'rejected': {
    
  95.         const rejectedThenable: RejectedThenable<S> = (resultThenable: any);
    
  96.         rejectedThenable.status = 'rejected';
    
  97.         rejectedThenable.reason = rejectedReason;
    
  98.         break;
    
  99.       }
    
  100.       case 'pending':
    
  101.       default: {
    
  102.         // The listener above should have been called first, so `resultStatus`
    
  103.         // should already be set to the correct value.
    
  104.         throw new Error(
    
  105.           'Thenable should have already resolved. This ' + 'is a bug in React.',
    
  106.         );
    
  107.       }
    
  108.     }
    
  109.   });
    
  110. 
    
  111.   return resultThenable;
    
  112. }
    
  113. 
    
  114. export function requestSyncActionContext<S>(
    
  115.   actionReturnValue: any,
    
  116.   // If this is provided, this resulting thenable resolves to this value instead
    
  117.   // of the return value of the action. This is a perf trick to avoid composing
    
  118.   // an extra async function.
    
  119.   overrideReturnValue: S | null,
    
  120. ): Thenable<S> | S {
    
  121.   const resultValue: S =
    
  122.     overrideReturnValue !== null
    
  123.       ? overrideReturnValue
    
  124.       : (actionReturnValue: any);
    
  125.   // This is not an async action, but it may be part of an outer async action.
    
  126.   if (currentEntangledListeners === null) {
    
  127.     return resultValue;
    
  128.   } else {
    
  129.     // Return a thenable that does not resolve until the entangled actions
    
  130.     // have finished.
    
  131.     const entangledListeners = currentEntangledListeners;
    
  132.     const resultThenable = createResultThenable<S>(entangledListeners);
    
  133.     entangledListeners.push(() => {
    
  134.       const fulfilledThenable: FulfilledThenable<S> = (resultThenable: any);
    
  135.       fulfilledThenable.status = 'fulfilled';
    
  136.       fulfilledThenable.value = resultValue;
    
  137.     });
    
  138.     return resultThenable;
    
  139.   }
    
  140. }
    
  141. 
    
  142. function pingEngtangledActionScope() {
    
  143.   if (
    
  144.     currentEntangledListeners !== null &&
    
  145.     --currentEntangledPendingCount === 0
    
  146.   ) {
    
  147.     // All the actions have finished. Close the entangled async action scope
    
  148.     // and notify all the listeners.
    
  149.     const listeners = currentEntangledListeners;
    
  150.     currentEntangledListeners = null;
    
  151.     currentEntangledLane = NoLane;
    
  152.     for (let i = 0; i < listeners.length; i++) {
    
  153.       const listener = listeners[i];
    
  154.       listener();
    
  155.     }
    
  156.   }
    
  157. }
    
  158. 
    
  159. function createResultThenable<S>(
    
  160.   entangledListeners: Array<() => mixed>,
    
  161. ): Thenable<S> {
    
  162.   // Waits for the entangled async action to complete, then resolves to the
    
  163.   // result of an individual action.
    
  164.   const resultThenable: PendingThenable<S> = {
    
  165.     status: 'pending',
    
  166.     value: null,
    
  167.     reason: null,
    
  168.     then(resolve: S => mixed) {
    
  169.       // This is a bit of a cheat. `resolve` expects a value of type `S` to be
    
  170.       // passed, but because we're instrumenting the `status` field ourselves,
    
  171.       // and we know this thenable will only be used by React, we also know
    
  172.       // the value isn't actually needed. So we add the resolve function
    
  173.       // directly to the entangled listeners.
    
  174.       //
    
  175.       // This is also why we don't need to check if the thenable is still
    
  176.       // pending; the Suspense implementation already performs that check.
    
  177.       const ping: () => mixed = (resolve: any);
    
  178.       entangledListeners.push(ping);
    
  179.     },
    
  180.   };
    
  181.   return resultThenable;
    
  182. }
    
  183. 
    
  184. export function peekEntangledActionLane(): Lane {
    
  185.   return currentEntangledLane;
    
  186. }