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. // Modules provided by RN:
    
  11. import {
    
  12.   deepDiffer,
    
  13.   flattenStyle,
    
  14. } from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
    
  15. import isArray from 'shared/isArray';
    
  16. 
    
  17. import type {AttributeConfiguration} from './ReactNativeTypes';
    
  18. 
    
  19. const emptyObject = {};
    
  20. 
    
  21. /**
    
  22.  * Create a payload that contains all the updates between two sets of props.
    
  23.  *
    
  24.  * These helpers are all encapsulated into a single module, because they use
    
  25.  * mutation as a performance optimization which leads to subtle shared
    
  26.  * dependencies between the code paths. To avoid this mutable state leaking
    
  27.  * across modules, I've kept them isolated to this module.
    
  28.  */
    
  29. 
    
  30. type NestedNode = Array<NestedNode> | Object;
    
  31. 
    
  32. // Tracks removed keys
    
  33. let removedKeys: {[string]: boolean} | null = null;
    
  34. let removedKeyCount = 0;
    
  35. 
    
  36. const deepDifferOptions = {
    
  37.   unsafelyIgnoreFunctions: true,
    
  38. };
    
  39. 
    
  40. function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
    
  41.   if (typeof nextProp !== 'object' || nextProp === null) {
    
  42.     // Scalars have already been checked for equality
    
  43.     return true;
    
  44.   } else {
    
  45.     // For objects and arrays, the default diffing algorithm is a deep compare
    
  46.     return deepDiffer(prevProp, nextProp, deepDifferOptions);
    
  47.   }
    
  48. }
    
  49. 
    
  50. function restoreDeletedValuesInNestedArray(
    
  51.   updatePayload: Object,
    
  52.   node: NestedNode,
    
  53.   validAttributes: AttributeConfiguration,
    
  54. ) {
    
  55.   if (isArray(node)) {
    
  56.     let i = node.length;
    
  57.     while (i-- && removedKeyCount > 0) {
    
  58.       restoreDeletedValuesInNestedArray(
    
  59.         updatePayload,
    
  60.         node[i],
    
  61.         validAttributes,
    
  62.       );
    
  63.     }
    
  64.   } else if (node && removedKeyCount > 0) {
    
  65.     const obj = node;
    
  66.     for (const propKey in removedKeys) {
    
  67.       // $FlowFixMe[incompatible-use] found when upgrading Flow
    
  68.       if (!removedKeys[propKey]) {
    
  69.         continue;
    
  70.       }
    
  71.       let nextProp = obj[propKey];
    
  72.       if (nextProp === undefined) {
    
  73.         continue;
    
  74.       }
    
  75. 
    
  76.       const attributeConfig = validAttributes[propKey];
    
  77.       if (!attributeConfig) {
    
  78.         continue; // not a valid native prop
    
  79.       }
    
  80. 
    
  81.       if (typeof nextProp === 'function') {
    
  82.         // $FlowFixMe[incompatible-type] found when upgrading Flow
    
  83.         nextProp = true;
    
  84.       }
    
  85.       if (typeof nextProp === 'undefined') {
    
  86.         // $FlowFixMe[incompatible-type] found when upgrading Flow
    
  87.         nextProp = null;
    
  88.       }
    
  89. 
    
  90.       if (typeof attributeConfig !== 'object') {
    
  91.         // case: !Object is the default case
    
  92.         updatePayload[propKey] = nextProp;
    
  93.       } else if (
    
  94.         typeof attributeConfig.diff === 'function' ||
    
  95.         typeof attributeConfig.process === 'function'
    
  96.       ) {
    
  97.         // case: CustomAttributeConfiguration
    
  98.         const nextValue =
    
  99.           typeof attributeConfig.process === 'function'
    
  100.             ? attributeConfig.process(nextProp)
    
  101.             : nextProp;
    
  102.         updatePayload[propKey] = nextValue;
    
  103.       }
    
  104.       // $FlowFixMe[incompatible-use] found when upgrading Flow
    
  105.       removedKeys[propKey] = false;
    
  106.       removedKeyCount--;
    
  107.     }
    
  108.   }
    
  109. }
    
  110. 
    
  111. function diffNestedArrayProperty(
    
  112.   updatePayload: null | Object,
    
  113.   prevArray: Array<NestedNode>,
    
  114.   nextArray: Array<NestedNode>,
    
  115.   validAttributes: AttributeConfiguration,
    
  116. ): null | Object {
    
  117.   const minLength =
    
  118.     prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
    
  119.   let i;
    
  120.   for (i = 0; i < minLength; i++) {
    
  121.     // Diff any items in the array in the forward direction. Repeated keys
    
  122.     // will be overwritten by later values.
    
  123.     updatePayload = diffNestedProperty(
    
  124.       updatePayload,
    
  125.       prevArray[i],
    
  126.       nextArray[i],
    
  127.       validAttributes,
    
  128.     );
    
  129.   }
    
  130.   for (; i < prevArray.length; i++) {
    
  131.     // Clear out all remaining properties.
    
  132.     updatePayload = clearNestedProperty(
    
  133.       updatePayload,
    
  134.       prevArray[i],
    
  135.       validAttributes,
    
  136.     );
    
  137.   }
    
  138.   for (; i < nextArray.length; i++) {
    
  139.     // Add all remaining properties.
    
  140.     updatePayload = addNestedProperty(
    
  141.       updatePayload,
    
  142.       nextArray[i],
    
  143.       validAttributes,
    
  144.     );
    
  145.   }
    
  146.   return updatePayload;
    
  147. }
    
  148. 
    
  149. function diffNestedProperty(
    
  150.   updatePayload: null | Object,
    
  151.   prevProp: NestedNode,
    
  152.   nextProp: NestedNode,
    
  153.   validAttributes: AttributeConfiguration,
    
  154. ): null | Object {
    
  155.   if (!updatePayload && prevProp === nextProp) {
    
  156.     // If no properties have been added, then we can bail out quickly on object
    
  157.     // equality.
    
  158.     return updatePayload;
    
  159.   }
    
  160. 
    
  161.   if (!prevProp || !nextProp) {
    
  162.     if (nextProp) {
    
  163.       return addNestedProperty(updatePayload, nextProp, validAttributes);
    
  164.     }
    
  165.     if (prevProp) {
    
  166.       return clearNestedProperty(updatePayload, prevProp, validAttributes);
    
  167.     }
    
  168.     return updatePayload;
    
  169.   }
    
  170. 
    
  171.   if (!isArray(prevProp) && !isArray(nextProp)) {
    
  172.     // Both are leaves, we can diff the leaves.
    
  173.     return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
    
  174.   }
    
  175. 
    
  176.   if (isArray(prevProp) && isArray(nextProp)) {
    
  177.     // Both are arrays, we can diff the arrays.
    
  178.     return diffNestedArrayProperty(
    
  179.       updatePayload,
    
  180.       prevProp,
    
  181.       nextProp,
    
  182.       validAttributes,
    
  183.     );
    
  184.   }
    
  185. 
    
  186.   if (isArray(prevProp)) {
    
  187.     return diffProperties(
    
  188.       updatePayload,
    
  189.       flattenStyle(prevProp),
    
  190.       nextProp,
    
  191.       validAttributes,
    
  192.     );
    
  193.   }
    
  194. 
    
  195.   return diffProperties(
    
  196.     updatePayload,
    
  197.     prevProp,
    
  198.     flattenStyle(nextProp),
    
  199.     validAttributes,
    
  200.   );
    
  201. }
    
  202. 
    
  203. /**
    
  204.  * addNestedProperty takes a single set of props and valid attribute
    
  205.  * attribute configurations. It processes each prop and adds it to the
    
  206.  * updatePayload.
    
  207.  */
    
  208. function addNestedProperty(
    
  209.   updatePayload: null | Object,
    
  210.   nextProp: NestedNode,
    
  211.   validAttributes: AttributeConfiguration,
    
  212. ): $FlowFixMe {
    
  213.   if (!nextProp) {
    
  214.     return updatePayload;
    
  215.   }
    
  216. 
    
  217.   if (!isArray(nextProp)) {
    
  218.     // Add each property of the leaf.
    
  219.     return addProperties(updatePayload, nextProp, validAttributes);
    
  220.   }
    
  221. 
    
  222.   for (let i = 0; i < nextProp.length; i++) {
    
  223.     // Add all the properties of the array.
    
  224.     updatePayload = addNestedProperty(
    
  225.       updatePayload,
    
  226.       nextProp[i],
    
  227.       validAttributes,
    
  228.     );
    
  229.   }
    
  230. 
    
  231.   return updatePayload;
    
  232. }
    
  233. 
    
  234. /**
    
  235.  * clearNestedProperty takes a single set of props and valid attributes. It
    
  236.  * adds a null sentinel to the updatePayload, for each prop key.
    
  237.  */
    
  238. function clearNestedProperty(
    
  239.   updatePayload: null | Object,
    
  240.   prevProp: NestedNode,
    
  241.   validAttributes: AttributeConfiguration,
    
  242. ): null | Object {
    
  243.   if (!prevProp) {
    
  244.     return updatePayload;
    
  245.   }
    
  246. 
    
  247.   if (!isArray(prevProp)) {
    
  248.     // Add each property of the leaf.
    
  249.     return clearProperties(updatePayload, prevProp, validAttributes);
    
  250.   }
    
  251. 
    
  252.   for (let i = 0; i < prevProp.length; i++) {
    
  253.     // Add all the properties of the array.
    
  254.     updatePayload = clearNestedProperty(
    
  255.       updatePayload,
    
  256.       prevProp[i],
    
  257.       validAttributes,
    
  258.     );
    
  259.   }
    
  260.   return updatePayload;
    
  261. }
    
  262. 
    
  263. /**
    
  264.  * diffProperties takes two sets of props and a set of valid attributes
    
  265.  * and write to updatePayload the values that changed or were deleted.
    
  266.  * If no updatePayload is provided, a new one is created and returned if
    
  267.  * anything changed.
    
  268.  */
    
  269. function diffProperties(
    
  270.   updatePayload: null | Object,
    
  271.   prevProps: Object,
    
  272.   nextProps: Object,
    
  273.   validAttributes: AttributeConfiguration,
    
  274. ): null | Object {
    
  275.   let attributeConfig;
    
  276.   let nextProp;
    
  277.   let prevProp;
    
  278. 
    
  279.   for (const propKey in nextProps) {
    
  280.     attributeConfig = validAttributes[propKey];
    
  281.     if (!attributeConfig) {
    
  282.       continue; // not a valid native prop
    
  283.     }
    
  284. 
    
  285.     prevProp = prevProps[propKey];
    
  286.     nextProp = nextProps[propKey];
    
  287. 
    
  288.     // functions are converted to booleans as markers that the associated
    
  289.     // events should be sent from native.
    
  290.     if (typeof nextProp === 'function') {
    
  291.       nextProp = (true: any);
    
  292.       // If nextProp is not a function, then don't bother changing prevProp
    
  293.       // since nextProp will win and go into the updatePayload regardless.
    
  294.       if (typeof prevProp === 'function') {
    
  295.         prevProp = (true: any);
    
  296.       }
    
  297.     }
    
  298. 
    
  299.     // An explicit value of undefined is treated as a null because it overrides
    
  300.     // any other preceding value.
    
  301.     if (typeof nextProp === 'undefined') {
    
  302.       nextProp = (null: any);
    
  303.       if (typeof prevProp === 'undefined') {
    
  304.         prevProp = (null: any);
    
  305.       }
    
  306.     }
    
  307. 
    
  308.     if (removedKeys) {
    
  309.       removedKeys[propKey] = false;
    
  310.     }
    
  311. 
    
  312.     if (updatePayload && updatePayload[propKey] !== undefined) {
    
  313.       // Something else already triggered an update to this key because another
    
  314.       // value diffed. Since we're now later in the nested arrays our value is
    
  315.       // more important so we need to calculate it and override the existing
    
  316.       // value. It doesn't matter if nothing changed, we'll set it anyway.
    
  317. 
    
  318.       // Pattern match on: attributeConfig
    
  319.       if (typeof attributeConfig !== 'object') {
    
  320.         // case: !Object is the default case
    
  321.         updatePayload[propKey] = nextProp;
    
  322.       } else if (
    
  323.         typeof attributeConfig.diff === 'function' ||
    
  324.         typeof attributeConfig.process === 'function'
    
  325.       ) {
    
  326.         // case: CustomAttributeConfiguration
    
  327.         const nextValue =
    
  328.           typeof attributeConfig.process === 'function'
    
  329.             ? attributeConfig.process(nextProp)
    
  330.             : nextProp;
    
  331.         updatePayload[propKey] = nextValue;
    
  332.       }
    
  333.       continue;
    
  334.     }
    
  335. 
    
  336.     if (prevProp === nextProp) {
    
  337.       continue; // nothing changed
    
  338.     }
    
  339. 
    
  340.     // Pattern match on: attributeConfig
    
  341.     if (typeof attributeConfig !== 'object') {
    
  342.       // case: !Object is the default case
    
  343.       if (defaultDiffer(prevProp, nextProp)) {
    
  344.         // a normal leaf has changed
    
  345.         (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
    
  346.           propKey
    
  347.         ] = nextProp;
    
  348.       }
    
  349.     } else if (
    
  350.       typeof attributeConfig.diff === 'function' ||
    
  351.       typeof attributeConfig.process === 'function'
    
  352.     ) {
    
  353.       // case: CustomAttributeConfiguration
    
  354.       const shouldUpdate =
    
  355.         prevProp === undefined ||
    
  356.         (typeof attributeConfig.diff === 'function'
    
  357.           ? attributeConfig.diff(prevProp, nextProp)
    
  358.           : defaultDiffer(prevProp, nextProp));
    
  359.       if (shouldUpdate) {
    
  360.         const nextValue =
    
  361.           typeof attributeConfig.process === 'function'
    
  362.             ? // $FlowFixMe[incompatible-use] found when upgrading Flow
    
  363.               attributeConfig.process(nextProp)
    
  364.             : nextProp;
    
  365.         (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
    
  366.           propKey
    
  367.         ] = nextValue;
    
  368.       }
    
  369.     } else {
    
  370.       // default: fallthrough case when nested properties are defined
    
  371.       removedKeys = null;
    
  372.       removedKeyCount = 0;
    
  373.       // We think that attributeConfig is not CustomAttributeConfiguration at
    
  374.       // this point so we assume it must be AttributeConfiguration.
    
  375.       updatePayload = diffNestedProperty(
    
  376.         updatePayload,
    
  377.         prevProp,
    
  378.         nextProp,
    
  379.         ((attributeConfig: any): AttributeConfiguration),
    
  380.       );
    
  381.       if (removedKeyCount > 0 && updatePayload) {
    
  382.         restoreDeletedValuesInNestedArray(
    
  383.           updatePayload,
    
  384.           nextProp,
    
  385.           ((attributeConfig: any): AttributeConfiguration),
    
  386.         );
    
  387.         removedKeys = null;
    
  388.       }
    
  389.     }
    
  390.   }
    
  391. 
    
  392.   // Also iterate through all the previous props to catch any that have been
    
  393.   // removed and make sure native gets the signal so it can reset them to the
    
  394.   // default.
    
  395.   for (const propKey in prevProps) {
    
  396.     if (nextProps[propKey] !== undefined) {
    
  397.       continue; // we've already covered this key in the previous pass
    
  398.     }
    
  399.     attributeConfig = validAttributes[propKey];
    
  400.     if (!attributeConfig) {
    
  401.       continue; // not a valid native prop
    
  402.     }
    
  403. 
    
  404.     if (updatePayload && updatePayload[propKey] !== undefined) {
    
  405.       // This was already updated to a diff result earlier.
    
  406.       continue;
    
  407.     }
    
  408. 
    
  409.     prevProp = prevProps[propKey];
    
  410.     if (prevProp === undefined) {
    
  411.       continue; // was already empty anyway
    
  412.     }
    
  413.     // Pattern match on: attributeConfig
    
  414.     if (
    
  415.       typeof attributeConfig !== 'object' ||
    
  416.       typeof attributeConfig.diff === 'function' ||
    
  417.       typeof attributeConfig.process === 'function'
    
  418.     ) {
    
  419.       // case: CustomAttributeConfiguration | !Object
    
  420.       // Flag the leaf property for removal by sending a sentinel.
    
  421.       (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
    
  422.         propKey
    
  423.       ] = null;
    
  424.       if (!removedKeys) {
    
  425.         removedKeys = ({}: {[string]: boolean});
    
  426.       }
    
  427.       if (!removedKeys[propKey]) {
    
  428.         removedKeys[propKey] = true;
    
  429.         removedKeyCount++;
    
  430.       }
    
  431.     } else {
    
  432.       // default:
    
  433.       // This is a nested attribute configuration where all the properties
    
  434.       // were removed so we need to go through and clear out all of them.
    
  435.       updatePayload = clearNestedProperty(
    
  436.         updatePayload,
    
  437.         prevProp,
    
  438.         ((attributeConfig: any): AttributeConfiguration),
    
  439.       );
    
  440.     }
    
  441.   }
    
  442.   return updatePayload;
    
  443. }
    
  444. 
    
  445. /**
    
  446.  * addProperties adds all the valid props to the payload after being processed.
    
  447.  */
    
  448. function addProperties(
    
  449.   updatePayload: null | Object,
    
  450.   props: Object,
    
  451.   validAttributes: AttributeConfiguration,
    
  452. ): null | Object {
    
  453.   // TODO: Fast path
    
  454.   return diffProperties(updatePayload, emptyObject, props, validAttributes);
    
  455. }
    
  456. 
    
  457. /**
    
  458.  * clearProperties clears all the previous props by adding a null sentinel
    
  459.  * to the payload for each valid key.
    
  460.  */
    
  461. function clearProperties(
    
  462.   updatePayload: null | Object,
    
  463.   prevProps: Object,
    
  464.   validAttributes: AttributeConfiguration,
    
  465. ): null | Object {
    
  466.   // TODO: Fast path
    
  467.   return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
    
  468. }
    
  469. 
    
  470. export function create(
    
  471.   props: Object,
    
  472.   validAttributes: AttributeConfiguration,
    
  473. ): null | Object {
    
  474.   return addProperties(
    
  475.     null, // updatePayload
    
  476.     props,
    
  477.     validAttributes,
    
  478.   );
    
  479. }
    
  480. 
    
  481. export function diff(
    
  482.   prevProps: Object,
    
  483.   nextProps: Object,
    
  484.   validAttributes: AttributeConfiguration,
    
  485. ): null | Object {
    
  486.   return diffProperties(
    
  487.     null, // updatePayload
    
  488.     prevProps,
    
  489.     nextProps,
    
  490.     validAttributes,
    
  491.   );
    
  492. }