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 {AnyNativeEvent} from '../events/PluginModuleType';
    
  11. import type {Container, SuspenseInstance} from '../client/ReactFiberConfigDOM';
    
  12. import type {DOMEventName} from '../events/DOMEventNames';
    
  13. import type {EventSystemFlags} from './EventSystemFlags';
    
  14. import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
    
  15. import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
    
  16. 
    
  17. import {
    
  18.   unstable_scheduleCallback as scheduleCallback,
    
  19.   unstable_NormalPriority as NormalPriority,
    
  20. } from 'scheduler';
    
  21. import {
    
  22.   getNearestMountedFiber,
    
  23.   getContainerFromFiber,
    
  24.   getSuspenseInstanceFromFiber,
    
  25. } from 'react-reconciler/src/ReactFiberTreeReflection';
    
  26. import {
    
  27.   findInstanceBlockingEvent,
    
  28.   findInstanceBlockingTarget,
    
  29. } from './ReactDOMEventListener';
    
  30. import {setReplayingEvent, resetReplayingEvent} from './CurrentReplayingEvent';
    
  31. import {
    
  32.   getInstanceFromNode,
    
  33.   getClosestInstanceFromNode,
    
  34.   getFiberCurrentPropsFromNode,
    
  35. } from '../client/ReactDOMComponentTree';
    
  36. import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
    
  37. import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities';
    
  38. import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
    
  39. import {dispatchReplayedFormAction} from './plugins/FormActionEventPlugin';
    
  40. 
    
  41. import {
    
  42.   attemptContinuousHydration,
    
  43.   attemptHydrationAtCurrentPriority,
    
  44. } from 'react-reconciler/src/ReactFiberReconciler';
    
  45. import {
    
  46.   runWithPriority as attemptHydrationAtPriority,
    
  47.   getCurrentUpdatePriority,
    
  48. } from 'react-reconciler/src/ReactEventPriorities';
    
  49. import {enableFormActions} from 'shared/ReactFeatureFlags';
    
  50. 
    
  51. // TODO: Upgrade this definition once we're on a newer version of Flow that
    
  52. // has this definition built-in.
    
  53. type PointerEvent = Event & {
    
  54.   pointerId: number,
    
  55.   relatedTarget: EventTarget | null,
    
  56.   ...
    
  57. };
    
  58. 
    
  59. type QueuedReplayableEvent = {
    
  60.   blockedOn: null | Container | SuspenseInstance,
    
  61.   domEventName: DOMEventName,
    
  62.   eventSystemFlags: EventSystemFlags,
    
  63.   nativeEvent: AnyNativeEvent,
    
  64.   targetContainers: Array<EventTarget>,
    
  65. };
    
  66. 
    
  67. let hasScheduledReplayAttempt = false;
    
  68. 
    
  69. // The last of each continuous event type. We only need to replay the last one
    
  70. // if the last target was dehydrated.
    
  71. let queuedFocus: null | QueuedReplayableEvent = null;
    
  72. let queuedDrag: null | QueuedReplayableEvent = null;
    
  73. let queuedMouse: null | QueuedReplayableEvent = null;
    
  74. // For pointer events there can be one latest event per pointerId.
    
  75. const queuedPointers: Map<number, QueuedReplayableEvent> = new Map();
    
  76. const queuedPointerCaptures: Map<number, QueuedReplayableEvent> = new Map();
    
  77. // We could consider replaying selectionchange and touchmoves too.
    
  78. 
    
  79. type QueuedHydrationTarget = {
    
  80.   blockedOn: null | Container | SuspenseInstance,
    
  81.   target: Node,
    
  82.   priority: EventPriority,
    
  83. };
    
  84. const queuedExplicitHydrationTargets: Array<QueuedHydrationTarget> = [];
    
  85. 
    
  86. const discreteReplayableEvents: Array<DOMEventName> = [
    
  87.   'mousedown',
    
  88.   'mouseup',
    
  89.   'touchcancel',
    
  90.   'touchend',
    
  91.   'touchstart',
    
  92.   'auxclick',
    
  93.   'dblclick',
    
  94.   'pointercancel',
    
  95.   'pointerdown',
    
  96.   'pointerup',
    
  97.   'dragend',
    
  98.   'dragstart',
    
  99.   'drop',
    
  100.   'compositionend',
    
  101.   'compositionstart',
    
  102.   'keydown',
    
  103.   'keypress',
    
  104.   'keyup',
    
  105.   'input',
    
  106.   'textInput', // Intentionally camelCase
    
  107.   'copy',
    
  108.   'cut',
    
  109.   'paste',
    
  110.   'click',
    
  111.   'change',
    
  112.   'contextmenu',
    
  113.   'reset',
    
  114.   // 'submit', // stopPropagation blocks the replay mechanism
    
  115. ];
    
  116. 
    
  117. export function isDiscreteEventThatRequiresHydration(
    
  118.   eventType: DOMEventName,
    
  119. ): boolean {
    
  120.   return discreteReplayableEvents.indexOf(eventType) > -1;
    
  121. }
    
  122. 
    
  123. function createQueuedReplayableEvent(
    
  124.   blockedOn: null | Container | SuspenseInstance,
    
  125.   domEventName: DOMEventName,
    
  126.   eventSystemFlags: EventSystemFlags,
    
  127.   targetContainer: EventTarget,
    
  128.   nativeEvent: AnyNativeEvent,
    
  129. ): QueuedReplayableEvent {
    
  130.   return {
    
  131.     blockedOn,
    
  132.     domEventName,
    
  133.     eventSystemFlags,
    
  134.     nativeEvent,
    
  135.     targetContainers: [targetContainer],
    
  136.   };
    
  137. }
    
  138. 
    
  139. // Resets the replaying for this type of continuous event to no event.
    
  140. export function clearIfContinuousEvent(
    
  141.   domEventName: DOMEventName,
    
  142.   nativeEvent: AnyNativeEvent,
    
  143. ): void {
    
  144.   switch (domEventName) {
    
  145.     case 'focusin':
    
  146.     case 'focusout':
    
  147.       queuedFocus = null;
    
  148.       break;
    
  149.     case 'dragenter':
    
  150.     case 'dragleave':
    
  151.       queuedDrag = null;
    
  152.       break;
    
  153.     case 'mouseover':
    
  154.     case 'mouseout':
    
  155.       queuedMouse = null;
    
  156.       break;
    
  157.     case 'pointerover':
    
  158.     case 'pointerout': {
    
  159.       const pointerId = ((nativeEvent: any): PointerEvent).pointerId;
    
  160.       queuedPointers.delete(pointerId);
    
  161.       break;
    
  162.     }
    
  163.     case 'gotpointercapture':
    
  164.     case 'lostpointercapture': {
    
  165.       const pointerId = ((nativeEvent: any): PointerEvent).pointerId;
    
  166.       queuedPointerCaptures.delete(pointerId);
    
  167.       break;
    
  168.     }
    
  169.   }
    
  170. }
    
  171. 
    
  172. function accumulateOrCreateContinuousQueuedReplayableEvent(
    
  173.   existingQueuedEvent: null | QueuedReplayableEvent,
    
  174.   blockedOn: null | Container | SuspenseInstance,
    
  175.   domEventName: DOMEventName,
    
  176.   eventSystemFlags: EventSystemFlags,
    
  177.   targetContainer: EventTarget,
    
  178.   nativeEvent: AnyNativeEvent,
    
  179. ): QueuedReplayableEvent {
    
  180.   if (
    
  181.     existingQueuedEvent === null ||
    
  182.     existingQueuedEvent.nativeEvent !== nativeEvent
    
  183.   ) {
    
  184.     const queuedEvent = createQueuedReplayableEvent(
    
  185.       blockedOn,
    
  186.       domEventName,
    
  187.       eventSystemFlags,
    
  188.       targetContainer,
    
  189.       nativeEvent,
    
  190.     );
    
  191.     if (blockedOn !== null) {
    
  192.       const fiber = getInstanceFromNode(blockedOn);
    
  193.       if (fiber !== null) {
    
  194.         // Attempt to increase the priority of this target.
    
  195.         attemptContinuousHydration(fiber);
    
  196.       }
    
  197.     }
    
  198.     return queuedEvent;
    
  199.   }
    
  200.   // If we have already queued this exact event, then it's because
    
  201.   // the different event systems have different DOM event listeners.
    
  202.   // We can accumulate the flags, and the targetContainers, and
    
  203.   // store a single event to be replayed.
    
  204.   existingQueuedEvent.eventSystemFlags |= eventSystemFlags;
    
  205.   const targetContainers = existingQueuedEvent.targetContainers;
    
  206.   if (
    
  207.     targetContainer !== null &&
    
  208.     targetContainers.indexOf(targetContainer) === -1
    
  209.   ) {
    
  210.     targetContainers.push(targetContainer);
    
  211.   }
    
  212.   return existingQueuedEvent;
    
  213. }
    
  214. 
    
  215. export function queueIfContinuousEvent(
    
  216.   blockedOn: null | Container | SuspenseInstance,
    
  217.   domEventName: DOMEventName,
    
  218.   eventSystemFlags: EventSystemFlags,
    
  219.   targetContainer: EventTarget,
    
  220.   nativeEvent: AnyNativeEvent,
    
  221. ): boolean {
    
  222.   // These set relatedTarget to null because the replayed event will be treated as if we
    
  223.   // moved from outside the window (no target) onto the target once it hydrates.
    
  224.   // Instead of mutating we could clone the event.
    
  225.   switch (domEventName) {
    
  226.     case 'focusin': {
    
  227.       const focusEvent = ((nativeEvent: any): FocusEvent);
    
  228.       queuedFocus = accumulateOrCreateContinuousQueuedReplayableEvent(
    
  229.         queuedFocus,
    
  230.         blockedOn,
    
  231.         domEventName,
    
  232.         eventSystemFlags,
    
  233.         targetContainer,
    
  234.         focusEvent,
    
  235.       );
    
  236.       return true;
    
  237.     }
    
  238.     case 'dragenter': {
    
  239.       const dragEvent = ((nativeEvent: any): DragEvent);
    
  240.       queuedDrag = accumulateOrCreateContinuousQueuedReplayableEvent(
    
  241.         queuedDrag,
    
  242.         blockedOn,
    
  243.         domEventName,
    
  244.         eventSystemFlags,
    
  245.         targetContainer,
    
  246.         dragEvent,
    
  247.       );
    
  248.       return true;
    
  249.     }
    
  250.     case 'mouseover': {
    
  251.       const mouseEvent = ((nativeEvent: any): MouseEvent);
    
  252.       queuedMouse = accumulateOrCreateContinuousQueuedReplayableEvent(
    
  253.         queuedMouse,
    
  254.         blockedOn,
    
  255.         domEventName,
    
  256.         eventSystemFlags,
    
  257.         targetContainer,
    
  258.         mouseEvent,
    
  259.       );
    
  260.       return true;
    
  261.     }
    
  262.     case 'pointerover': {
    
  263.       const pointerEvent = ((nativeEvent: any): PointerEvent);
    
  264.       const pointerId = pointerEvent.pointerId;
    
  265.       queuedPointers.set(
    
  266.         pointerId,
    
  267.         accumulateOrCreateContinuousQueuedReplayableEvent(
    
  268.           queuedPointers.get(pointerId) || null,
    
  269.           blockedOn,
    
  270.           domEventName,
    
  271.           eventSystemFlags,
    
  272.           targetContainer,
    
  273.           pointerEvent,
    
  274.         ),
    
  275.       );
    
  276.       return true;
    
  277.     }
    
  278.     case 'gotpointercapture': {
    
  279.       const pointerEvent = ((nativeEvent: any): PointerEvent);
    
  280.       const pointerId = pointerEvent.pointerId;
    
  281.       queuedPointerCaptures.set(
    
  282.         pointerId,
    
  283.         accumulateOrCreateContinuousQueuedReplayableEvent(
    
  284.           queuedPointerCaptures.get(pointerId) || null,
    
  285.           blockedOn,
    
  286.           domEventName,
    
  287.           eventSystemFlags,
    
  288.           targetContainer,
    
  289.           pointerEvent,
    
  290.         ),
    
  291.       );
    
  292.       return true;
    
  293.     }
    
  294.   }
    
  295.   return false;
    
  296. }
    
  297. 
    
  298. // Check if this target is unblocked. Returns true if it's unblocked.
    
  299. function attemptExplicitHydrationTarget(
    
  300.   queuedTarget: QueuedHydrationTarget,
    
  301. ): void {
    
  302.   // TODO: This function shares a lot of logic with findInstanceBlockingEvent.
    
  303.   // Try to unify them. It's a bit tricky since it would require two return
    
  304.   // values.
    
  305.   const targetInst = getClosestInstanceFromNode(queuedTarget.target);
    
  306.   if (targetInst !== null) {
    
  307.     const nearestMounted = getNearestMountedFiber(targetInst);
    
  308.     if (nearestMounted !== null) {
    
  309.       const tag = nearestMounted.tag;
    
  310.       if (tag === SuspenseComponent) {
    
  311.         const instance = getSuspenseInstanceFromFiber(nearestMounted);
    
  312.         if (instance !== null) {
    
  313.           // We're blocked on hydrating this boundary.
    
  314.           // Increase its priority.
    
  315.           queuedTarget.blockedOn = instance;
    
  316.           attemptHydrationAtPriority(queuedTarget.priority, () => {
    
  317.             attemptHydrationAtCurrentPriority(nearestMounted);
    
  318.           });
    
  319. 
    
  320.           return;
    
  321.         }
    
  322.       } else if (tag === HostRoot) {
    
  323.         const root: FiberRoot = nearestMounted.stateNode;
    
  324.         if (isRootDehydrated(root)) {
    
  325.           queuedTarget.blockedOn = getContainerFromFiber(nearestMounted);
    
  326.           // We don't currently have a way to increase the priority of
    
  327.           // a root other than sync.
    
  328.           return;
    
  329.         }
    
  330.       }
    
  331.     }
    
  332.   }
    
  333.   queuedTarget.blockedOn = null;
    
  334. }
    
  335. 
    
  336. export function queueExplicitHydrationTarget(target: Node): void {
    
  337.   // TODO: This will read the priority if it's dispatched by the React
    
  338.   // event system but not native events. Should read window.event.type, like
    
  339.   // we do for updates (getCurrentEventPriority).
    
  340.   const updatePriority = getCurrentUpdatePriority();
    
  341.   const queuedTarget: QueuedHydrationTarget = {
    
  342.     blockedOn: null,
    
  343.     target: target,
    
  344.     priority: updatePriority,
    
  345.   };
    
  346.   let i = 0;
    
  347.   for (; i < queuedExplicitHydrationTargets.length; i++) {
    
  348.     // Stop once we hit the first target with lower priority than
    
  349.     if (
    
  350.       !isHigherEventPriority(
    
  351.         updatePriority,
    
  352.         queuedExplicitHydrationTargets[i].priority,
    
  353.       )
    
  354.     ) {
    
  355.       break;
    
  356.     }
    
  357.   }
    
  358.   queuedExplicitHydrationTargets.splice(i, 0, queuedTarget);
    
  359.   if (i === 0) {
    
  360.     attemptExplicitHydrationTarget(queuedTarget);
    
  361.   }
    
  362. }
    
  363. 
    
  364. function attemptReplayContinuousQueuedEvent(
    
  365.   queuedEvent: QueuedReplayableEvent,
    
  366. ): boolean {
    
  367.   if (queuedEvent.blockedOn !== null) {
    
  368.     return false;
    
  369.   }
    
  370.   const targetContainers = queuedEvent.targetContainers;
    
  371.   while (targetContainers.length > 0) {
    
  372.     const nextBlockedOn = findInstanceBlockingEvent(queuedEvent.nativeEvent);
    
  373.     if (nextBlockedOn === null) {
    
  374.       const nativeEvent = queuedEvent.nativeEvent;
    
  375.       const nativeEventClone = new nativeEvent.constructor(
    
  376.         nativeEvent.type,
    
  377.         (nativeEvent: any),
    
  378.       );
    
  379.       setReplayingEvent(nativeEventClone);
    
  380.       nativeEvent.target.dispatchEvent(nativeEventClone);
    
  381.       resetReplayingEvent();
    
  382.     } else {
    
  383.       // We're still blocked. Try again later.
    
  384.       const fiber = getInstanceFromNode(nextBlockedOn);
    
  385.       if (fiber !== null) {
    
  386.         attemptContinuousHydration(fiber);
    
  387.       }
    
  388.       queuedEvent.blockedOn = nextBlockedOn;
    
  389.       return false;
    
  390.     }
    
  391.     // This target container was successfully dispatched. Try the next.
    
  392.     targetContainers.shift();
    
  393.   }
    
  394.   return true;
    
  395. }
    
  396. 
    
  397. function attemptReplayContinuousQueuedEventInMap(
    
  398.   queuedEvent: QueuedReplayableEvent,
    
  399.   key: number,
    
  400.   map: Map<number, QueuedReplayableEvent>,
    
  401. ): void {
    
  402.   if (attemptReplayContinuousQueuedEvent(queuedEvent)) {
    
  403.     map.delete(key);
    
  404.   }
    
  405. }
    
  406. 
    
  407. function replayUnblockedEvents() {
    
  408.   hasScheduledReplayAttempt = false;
    
  409.   // Replay any continuous events.
    
  410.   if (queuedFocus !== null && attemptReplayContinuousQueuedEvent(queuedFocus)) {
    
  411.     queuedFocus = null;
    
  412.   }
    
  413.   if (queuedDrag !== null && attemptReplayContinuousQueuedEvent(queuedDrag)) {
    
  414.     queuedDrag = null;
    
  415.   }
    
  416.   if (queuedMouse !== null && attemptReplayContinuousQueuedEvent(queuedMouse)) {
    
  417.     queuedMouse = null;
    
  418.   }
    
  419.   queuedPointers.forEach(attemptReplayContinuousQueuedEventInMap);
    
  420.   queuedPointerCaptures.forEach(attemptReplayContinuousQueuedEventInMap);
    
  421. }
    
  422. 
    
  423. function scheduleCallbackIfUnblocked(
    
  424.   queuedEvent: QueuedReplayableEvent,
    
  425.   unblocked: Container | SuspenseInstance,
    
  426. ) {
    
  427.   if (queuedEvent.blockedOn === unblocked) {
    
  428.     queuedEvent.blockedOn = null;
    
  429.     if (!hasScheduledReplayAttempt) {
    
  430.       hasScheduledReplayAttempt = true;
    
  431.       // Schedule a callback to attempt replaying as many events as are
    
  432.       // now unblocked. This first might not actually be unblocked yet.
    
  433.       // We could check it early to avoid scheduling an unnecessary callback.
    
  434.       scheduleCallback(NormalPriority, replayUnblockedEvents);
    
  435.     }
    
  436.   }
    
  437. }
    
  438. 
    
  439. type FormAction = FormData => void | Promise<void>;
    
  440. 
    
  441. type FormReplayingQueue = Array<any>; // [form, submitter or action, formData...]
    
  442. 
    
  443. let lastScheduledReplayQueue: null | FormReplayingQueue = null;
    
  444. 
    
  445. function replayUnblockedFormActions(formReplayingQueue: FormReplayingQueue) {
    
  446.   if (lastScheduledReplayQueue === formReplayingQueue) {
    
  447.     lastScheduledReplayQueue = null;
    
  448.   }
    
  449.   for (let i = 0; i < formReplayingQueue.length; i += 3) {
    
  450.     const form: HTMLFormElement = formReplayingQueue[i];
    
  451.     const submitterOrAction:
    
  452.       | null
    
  453.       | HTMLInputElement
    
  454.       | HTMLButtonElement
    
  455.       | FormAction = formReplayingQueue[i + 1];
    
  456.     const formData: FormData = formReplayingQueue[i + 2];
    
  457.     if (typeof submitterOrAction !== 'function') {
    
  458.       // This action is not hydrated yet. This might be because it's blocked on
    
  459.       // a different React instance or higher up our tree.
    
  460.       const blockedOn = findInstanceBlockingTarget(submitterOrAction || form);
    
  461.       if (blockedOn === null) {
    
  462.         // We're not blocked but we don't have an action. This must mean that
    
  463.         // this is in another React instance. We'll just skip past it.
    
  464.         continue;
    
  465.       } else {
    
  466.         // We're blocked on something in this React instance. We'll retry later.
    
  467.         break;
    
  468.       }
    
  469.     }
    
  470.     const formInst = getInstanceFromNode(form);
    
  471.     if (formInst !== null) {
    
  472.       // This is part of our instance.
    
  473.       // We're ready to replay this. Let's delete it from the queue.
    
  474.       formReplayingQueue.splice(i, 3);
    
  475.       i -= 3;
    
  476.       dispatchReplayedFormAction(formInst, form, submitterOrAction, formData);
    
  477.       // Continue without incrementing the index.
    
  478.       continue;
    
  479.     }
    
  480.     // This form must've been part of a different React instance.
    
  481.     // If we want to preserve ordering between React instances on the same root
    
  482.     // we'd need some way for the other instance to ping us when it's done.
    
  483.     // We'll just skip this and let the other instance execute it.
    
  484.   }
    
  485. }
    
  486. 
    
  487. function scheduleReplayQueueIfNeeded(formReplayingQueue: FormReplayingQueue) {
    
  488.   // Schedule a callback to execute any unblocked form actions in.
    
  489.   // We only keep track of the last queue which means that if multiple React oscillate
    
  490.   // commits, we could schedule more callbacks than necessary but it's not a big deal
    
  491.   // and we only really except one instance.
    
  492.   if (lastScheduledReplayQueue !== formReplayingQueue) {
    
  493.     lastScheduledReplayQueue = formReplayingQueue;
    
  494.     scheduleCallback(NormalPriority, () =>
    
  495.       replayUnblockedFormActions(formReplayingQueue),
    
  496.     );
    
  497.   }
    
  498. }
    
  499. 
    
  500. export function retryIfBlockedOn(
    
  501.   unblocked: Container | SuspenseInstance,
    
  502. ): void {
    
  503.   if (queuedFocus !== null) {
    
  504.     scheduleCallbackIfUnblocked(queuedFocus, unblocked);
    
  505.   }
    
  506.   if (queuedDrag !== null) {
    
  507.     scheduleCallbackIfUnblocked(queuedDrag, unblocked);
    
  508.   }
    
  509.   if (queuedMouse !== null) {
    
  510.     scheduleCallbackIfUnblocked(queuedMouse, unblocked);
    
  511.   }
    
  512.   const unblock = (queuedEvent: QueuedReplayableEvent) =>
    
  513.     scheduleCallbackIfUnblocked(queuedEvent, unblocked);
    
  514.   queuedPointers.forEach(unblock);
    
  515.   queuedPointerCaptures.forEach(unblock);
    
  516. 
    
  517.   for (let i = 0; i < queuedExplicitHydrationTargets.length; i++) {
    
  518.     const queuedTarget = queuedExplicitHydrationTargets[i];
    
  519.     if (queuedTarget.blockedOn === unblocked) {
    
  520.       queuedTarget.blockedOn = null;
    
  521.     }
    
  522.   }
    
  523. 
    
  524.   while (queuedExplicitHydrationTargets.length > 0) {
    
  525.     const nextExplicitTarget = queuedExplicitHydrationTargets[0];
    
  526.     if (nextExplicitTarget.blockedOn !== null) {
    
  527.       // We're still blocked.
    
  528.       break;
    
  529.     } else {
    
  530.       attemptExplicitHydrationTarget(nextExplicitTarget);
    
  531.       if (nextExplicitTarget.blockedOn === null) {
    
  532.         // We're unblocked.
    
  533.         queuedExplicitHydrationTargets.shift();
    
  534.       }
    
  535.     }
    
  536.   }
    
  537. 
    
  538.   if (enableFormActions) {
    
  539.     // Check the document if there are any queued form actions.
    
  540.     const root = unblocked.getRootNode();
    
  541.     const formReplayingQueue: void | FormReplayingQueue = (root: any)
    
  542.       .$$reactFormReplay;
    
  543.     if (formReplayingQueue != null) {
    
  544.       for (let i = 0; i < formReplayingQueue.length; i += 3) {
    
  545.         const form: HTMLFormElement = formReplayingQueue[i];
    
  546.         const submitterOrAction:
    
  547.           | null
    
  548.           | HTMLInputElement
    
  549.           | HTMLButtonElement
    
  550.           | FormAction = formReplayingQueue[i + 1];
    
  551.         const formProps = getFiberCurrentPropsFromNode(form);
    
  552.         if (typeof submitterOrAction === 'function') {
    
  553.           // This action has already resolved. We're just waiting to dispatch it.
    
  554.           if (!formProps) {
    
  555.             // This was not part of this React instance. It might have been recently
    
  556.             // unblocking us from dispatching our events. So let's make sure we schedule
    
  557.             // a retry.
    
  558.             scheduleReplayQueueIfNeeded(formReplayingQueue);
    
  559.           }
    
  560.           continue;
    
  561.         }
    
  562.         let target: Node = form;
    
  563.         if (formProps) {
    
  564.           // This form belongs to this React instance but the submitter might
    
  565.           // not be done yet.
    
  566.           let action: null | FormAction = null;
    
  567.           const submitter = submitterOrAction;
    
  568.           if (submitter && submitter.hasAttribute('formAction')) {
    
  569.             // The submitter is the one that is responsible for the action.
    
  570.             target = submitter;
    
  571.             const submitterProps = getFiberCurrentPropsFromNode(submitter);
    
  572.             if (submitterProps) {
    
  573.               // The submitter is part of this instance.
    
  574.               action = (submitterProps: any).formAction;
    
  575.             } else {
    
  576.               const blockedOn = findInstanceBlockingTarget(target);
    
  577.               if (blockedOn !== null) {
    
  578.                 // The submitter is not hydrated yet. We'll wait for it.
    
  579.                 continue;
    
  580.               }
    
  581.               // The submitter must have been a part of a different React instance.
    
  582.               // Except the form isn't. We don't dispatch actions in this scenario.
    
  583.             }
    
  584.           } else {
    
  585.             action = (formProps: any).action;
    
  586.           }
    
  587.           if (typeof action === 'function') {
    
  588.             formReplayingQueue[i + 1] = action;
    
  589.           } else {
    
  590.             // Something went wrong so let's just delete this action.
    
  591.             formReplayingQueue.splice(i, 3);
    
  592.             i -= 3;
    
  593.           }
    
  594.           // Schedule a replay in case this unblocked something.
    
  595.           scheduleReplayQueueIfNeeded(formReplayingQueue);
    
  596.           continue;
    
  597.         }
    
  598.         // Something above this target is still blocked so we can't continue yet.
    
  599.         // We're not sure if this target is actually part of this React instance
    
  600.         // yet. It could be a different React as a child but at least some parent is.
    
  601.         // We must continue for any further queued actions.
    
  602.       }
    
  603.     }
    
  604.   }
    
  605. }