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 {Thenable, ReactFormState} from 'shared/ReactTypes';
    
  11. 
    
  12. import type {
    
  13.   ServerManifest,
    
  14.   ClientReference as ServerReference,
    
  15. } from 'react-client/src/ReactFlightClientConfig';
    
  16. 
    
  17. import {
    
  18.   resolveServerReference,
    
  19.   preloadModule,
    
  20.   requireModule,
    
  21. } from 'react-client/src/ReactFlightClientConfig';
    
  22. 
    
  23. import {createResponse, close, getRoot} from './ReactFlightReplyServer';
    
  24. 
    
  25. type ServerReferenceId = any;
    
  26. 
    
  27. function bindArgs(fn: any, args: any) {
    
  28.   return fn.bind.apply(fn, [null].concat(args));
    
  29. }
    
  30. 
    
  31. function loadServerReference<T>(
    
  32.   bundlerConfig: ServerManifest,
    
  33.   id: ServerReferenceId,
    
  34.   bound: null | Thenable<Array<any>>,
    
  35. ): Promise<T> {
    
  36.   const serverReference: ServerReference<T> =
    
  37.     resolveServerReference<$FlowFixMe>(bundlerConfig, id);
    
  38.   // We expect most servers to not really need this because you'd just have all
    
  39.   // the relevant modules already loaded but it allows for lazy loading of code
    
  40.   // if needed.
    
  41.   const preloadPromise = preloadModule(serverReference);
    
  42.   if (bound) {
    
  43.     return Promise.all([(bound: any), preloadPromise]).then(
    
  44.       ([args]: Array<any>) => bindArgs(requireModule(serverReference), args),
    
  45.     );
    
  46.   } else if (preloadPromise) {
    
  47.     return Promise.resolve(preloadPromise).then(() =>
    
  48.       requireModule(serverReference),
    
  49.     );
    
  50.   } else {
    
  51.     // Synchronously available
    
  52.     return Promise.resolve(requireModule(serverReference));
    
  53.   }
    
  54. }
    
  55. 
    
  56. function decodeBoundActionMetaData(
    
  57.   body: FormData,
    
  58.   serverManifest: ServerManifest,
    
  59.   formFieldPrefix: string,
    
  60. ): {id: ServerReferenceId, bound: null | Promise<Array<any>>} {
    
  61.   // The data for this reference is encoded in multiple fields under this prefix.
    
  62.   const actionResponse = createResponse(serverManifest, formFieldPrefix, body);
    
  63.   close(actionResponse);
    
  64.   const refPromise = getRoot<{
    
  65.     id: ServerReferenceId,
    
  66.     bound: null | Promise<Array<any>>,
    
  67.   }>(actionResponse);
    
  68.   // Force it to initialize
    
  69.   // $FlowFixMe
    
  70.   refPromise.then(() => {});
    
  71.   if (refPromise.status !== 'fulfilled') {
    
  72.     // $FlowFixMe
    
  73.     throw refPromise.reason;
    
  74.   }
    
  75.   return refPromise.value;
    
  76. }
    
  77. 
    
  78. export function decodeAction<T>(
    
  79.   body: FormData,
    
  80.   serverManifest: ServerManifest,
    
  81. ): Promise<() => T> | null {
    
  82.   // We're going to create a new formData object that holds all the fields except
    
  83.   // the implementation details of the action data.
    
  84.   const formData = new FormData();
    
  85. 
    
  86.   let action: Promise<(formData: FormData) => T> | null = null;
    
  87. 
    
  88.   // $FlowFixMe[prop-missing]
    
  89.   body.forEach((value: string | File, key: string) => {
    
  90.     if (!key.startsWith('$ACTION_')) {
    
  91.       formData.append(key, value);
    
  92.       return;
    
  93.     }
    
  94.     // Later actions may override earlier actions if a button is used to override the default
    
  95.     // form action.
    
  96.     if (key.startsWith('$ACTION_REF_')) {
    
  97.       const formFieldPrefix = '$ACTION_' + key.slice(12) + ':';
    
  98.       const metaData = decodeBoundActionMetaData(
    
  99.         body,
    
  100.         serverManifest,
    
  101.         formFieldPrefix,
    
  102.       );
    
  103.       action = loadServerReference(serverManifest, metaData.id, metaData.bound);
    
  104.       return;
    
  105.     }
    
  106.     if (key.startsWith('$ACTION_ID_')) {
    
  107.       const id = key.slice(11);
    
  108.       action = loadServerReference(serverManifest, id, null);
    
  109.       return;
    
  110.     }
    
  111.   });
    
  112. 
    
  113.   if (action === null) {
    
  114.     return null;
    
  115.   }
    
  116.   // Return the action with the remaining FormData bound to the first argument.
    
  117.   return action.then(fn => fn.bind(null, formData));
    
  118. }
    
  119. 
    
  120. export function decodeFormState<S>(
    
  121.   actionResult: S,
    
  122.   body: FormData,
    
  123.   serverManifest: ServerManifest,
    
  124. ): Promise<ReactFormState<S, ServerReferenceId> | null> {
    
  125.   const keyPath = body.get('$ACTION_KEY');
    
  126.   if (typeof keyPath !== 'string') {
    
  127.     // This form submission did not include any form state.
    
  128.     return Promise.resolve(null);
    
  129.   }
    
  130.   // Search through the form data object to get the reference id and the number
    
  131.   // of bound arguments. This repeats some of the work done in decodeAction.
    
  132.   let metaData = null;
    
  133.   // $FlowFixMe[prop-missing]
    
  134.   body.forEach((value: string | File, key: string) => {
    
  135.     if (key.startsWith('$ACTION_REF_')) {
    
  136.       const formFieldPrefix = '$ACTION_' + key.slice(12) + ':';
    
  137.       metaData = decodeBoundActionMetaData(
    
  138.         body,
    
  139.         serverManifest,
    
  140.         formFieldPrefix,
    
  141.       );
    
  142.     }
    
  143.     // We don't check for the simple $ACTION_ID_ case because form state actions
    
  144.     // are always bound to the state argument.
    
  145.   });
    
  146.   if (metaData === null) {
    
  147.     // Should be unreachable.
    
  148.     return Promise.resolve(null);
    
  149.   }
    
  150.   const referenceId = metaData.id;
    
  151.   return Promise.resolve(metaData.bound).then(bound => {
    
  152.     if (bound === null) {
    
  153.       // Should be unreachable because form state actions are always bound to the
    
  154.       // state argument.
    
  155.       return null;
    
  156.     }
    
  157.     // The form action dispatch method is always bound to the initial state.
    
  158.     // But when comparing signatures, we compare to the original unbound action.
    
  159.     // Subtract one from the arity to account for this.
    
  160.     const boundArity = bound.length - 1;
    
  161.     return [actionResult, keyPath, referenceId, boundArity];
    
  162.   });
    
  163. }