/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

import { asSettled } from "./async-value";
import { validateContext } from "./context";

/**
 * memoizableActon is a utility for actions that should only be performed
 * once per key. It is useful for loading sources
 *
 * @getValue - gets the result from the redux store
 * @createKey - creates a key for the requests map
 * @action - kicks off the async work for the action
 *
 *
 * For Example
 *
 * export const setItem = memoizeableAction(
 *   "setItem",
 *   {
 *     hasValue: ({ a }, { getState }) => hasItem(getState(), a),
 *     getValue: ({ a }, { getState }) => getItem(getState(), a),
 *     createKey: ({ a }) => a,
 *     action: ({ a }, thunkArgs) => doSetItem(a, thunkArgs)
 *   }
 * );
 */
export function memoizeableAction(name, { getValue, createKey, action }) {
  const requests = new Map();
  return args => async thunkArgs => {
    let result = asSettled(getValue(args, thunkArgs));
    if (!result) {
      const key = createKey(args, thunkArgs);
      if (!requests.has(key)) {
        requests.set(
          key,
          (async () => {
            try {
              await action(args, thunkArgs);
            } catch (e) {
              console.warn(`Action ${name} had an exception:`, e);
            } finally {
              requests.delete(key);
            }
          })()
        );
      }

      await requests.get(key);

      if (args.cx) {
        validateContext(thunkArgs.getState(), args.cx);
      }

      result = asSettled(getValue(args, thunkArgs));
      if (!result) {
        // Returning null here is not ideal. This means that the action
        // resolved but 'getValue' didn't return a loaded value, for instance
        // if the data the action was meant to store was deleted. In a perfect
        // world we'd throw a ContextError here or handle cancellation somehow.
        // Throwing will also allow us to change the return type on the action
        // to always return a promise for the getValue AsyncValue type, but
        // for now we have to add an additional '| null' for this case.
        return null;
      }
    }

    if (result.state === "rejected") {
      throw result.value;
    }
    return result.value;
  };
}
