"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getEventSourceManagerBundle = void 0;

var _object = require("../utils/object");

var _reduxBundler = require("redux-bundler");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

const mappedState = {
  0: 'connecting',
  1: 'open',
  2: 'closed'
};
/**
 * @param {  | {
 *       name: string
 *       actionBaseName: string
 *       activeIdsSelectorName: string
 *       dataActionCreatorName: string
 *       buildUrl: (id: string, store: any) => string
 *       eventSourceConstructor?: any
 *       logger?: any
 *       retryFailedTimeout: number
 *     }
 *   | any} Opts
 */

const getEventSourceManagerBundle = (_ref) => {
  let {
    name,
    actionBaseName,
    activeIdsSelectorName,
    dataActionCreatorName,
    buildUrl,
    eventSourceConstructor,
    logger = console,
    retryFailedTimeout = 1000 * 60
  } = _ref,
      rest = _objectWithoutProperties(_ref, ["name", "actionBaseName", "activeIdsSelectorName", "dataActionCreatorName", "buildUrl", "eventSourceConstructor", "logger", "retryFailedTimeout"]);

  const CREATED = "".concat(actionBaseName, "_CREATED");
  const OPENED = "".concat(actionBaseName, "_OPENED");
  const REMOVED = "".concat(actionBaseName, "_REMOVED");
  const FAILED = "".concat(actionBaseName, "_FAILED");
  const uCaseName = name.charAt(0).toUpperCase() + name.slice(1);
  const rawSelectorName = "select".concat(uCaseName, "Raw");
  const connectedSelectorName = "select".concat(uCaseName, "Connected");
  const doCreateName = "doCreate".concat(uCaseName, "ForId");
  const doRemoveName = "doRemove".concat(uCaseName, "ForId");
  return _objectSpread({
    name,
    getReducer: () => {
      const initial = {};
      return (state = initial, {
        type,
        payload
      }) => {
        if (type === CREATED) {
          return _objectSpread(_objectSpread({}, state), {}, {
            [payload.id]: {
              es: payload.eventSource,
              state: mappedState[payload.eventSource.readyState]
            }
          });
        }

        if (type === FAILED) {
          const id = payload.id;
          return _objectSpread(_objectSpread({}, state), {}, {
            [id]: _objectSpread(_objectSpread({}, state[id]), {}, {
              state: 'failed',
              failedAt: payload.time
            })
          });
        }

        if (type === OPENED) {
          const {
            id
          } = payload;
          const existing = state[id];

          if (!existing) {
            return state;
          }

          return _objectSpread(_objectSpread({}, state), {}, {
            [id]: _objectSpread(_objectSpread({}, existing), {}, {
              state: mappedState[existing.es.readyState]
            })
          });
        }

        if (type === REMOVED) {
          return (0, _object.omit)(state, [payload]);
        }

        return state;
      };
    },
    [rawSelectorName]: state => state[name],
    [doRemoveName]: id => ({
      store,
      dispatch
    }) => {
      const existing = store[rawSelectorName]()[id];

      if (existing) {
        const {
          es
        } = existing;

        if (es) {
          try {
            es.close();
          } catch (e) {// empty on purpose
          }
        }

        dispatch({
          type: REMOVED,
          payload: id
        });
      }
    },
    [doCreateName]: id => ({
      store,
      dispatch
    }) => {
      const eventSource = new (eventSourceConstructor || EventSource)(buildUrl(id, store), {
        withCredentials: true
      });
      dispatch({
        type: CREATED,
        payload: {
          id,
          eventSource
        }
      });
      eventSource.addEventListener('open', () => {
        dispatch({
          type: OPENED,
          payload: {
            id
          }
        });
      });
      eventSource.addEventListener('error', errorEvent => {
        logger && logger.debug({
          errorEvent,
          message: "eventSourceError ".concat(name),
          readyState: eventSource.readyState
        }); // closed

        if (eventSource.readyState === 2) {
          setTimeout(() => {
            // if it's still 2 we should retry
            if (eventSource.readyState === 2) {
              store[doCreateName](id);
            }
          }, 1000);
        } // error event status (if available)


        const errorHttpStatus = errorEvent && errorEvent.status && typeof errorEvent.status === 'number' ? errorEvent.status : 0;

        if (errorHttpStatus >= 400 && errorHttpStatus <= 499) {
          // we assume failure
          eventSource.close();
          dispatch({
            type: FAILED,
            payload: {
              id,
              time: Date.now()
            }
          });
        }
      });
      eventSource.addEventListener('message', message => {
        try {
          const data = JSON.parse(message.data);
          store[dataActionCreatorName]({
            id,
            data
          });
          logger && logger.debug({
            message: "eventSourceMessageReceived ".concat(name),
            data
          });
        } catch (e) {
          logger && logger.error({
            error: e,
            message: "eventSourceErrorReceivingMessage ".concat(name)
          });
        }
      });
    },
    [connectedSelectorName]: (0, _reduxBundler.createSelector)(rawSelectorName, activeIdsSelectorName, (raw, idsToBeSubscribedTo) => {
      if (!idsToBeSubscribedTo || !idsToBeSubscribedTo.length) {
        return false;
      }

      return idsToBeSubscribedTo.every(orgId => raw[orgId] && raw[orgId].state === 'open');
    }),
    ["reactShouldCreateRemove".concat(uCaseName)]: (0, _reduxBundler.createSelector)('selectAppTime', activeIdsSelectorName, rawSelectorName, (appTime, idResult, raw) => {
      // always coerce to array of ids
      let ids = [];

      if (idResult) {
        ids = Array.isArray(idResult) ? idResult : [idResult];
      }

      for (const id of ids) {
        if (!raw[id]) {
          return {
            actionCreator: doCreateName,
            args: [id]
          };
        }
      } // any to remove


      for (const id in raw) {
        const idWasRemoved = !ids.includes(id);
        const failedAtTime = raw[id].failedAt;
        const isFailedAndOld = failedAtTime && appTime - failedAtTime > retryFailedTimeout;

        if (idWasRemoved || isFailedAndOld) {
          return {
            actionCreator: doRemoveName,
            args: [id]
          };
        }
      }
    })
  }, rest);
};

exports.getEventSourceManagerBundle = getEventSourceManagerBundle;