text: import { MethodEnum } from '@algolia/requester-common';
  
  function createMappedRequestOptions(requestOptions, timeout) {
      const options = requestOptions || {};
      const data = options.data || {};
      Object.keys(options).forEach(key => {
          if (['timeout', 'headers', 'queryParameters', 'data', 'cacheable'].indexOf(key) === -1) {
              data[key] = options[key]; // eslint-disable-line functional/immutable-data
          }
      });
      return {
          data: Object.entries(data).length > 0 ? data : undefined,
          timeout: options.timeout || timeout,
          headers: options.headers || {},
          queryParameters: options.queryParameters || {},
          cacheable: options.cacheable,
      };
  }
  
  const CallEnum = {
      /**
       * If the host is read only.
       */
      Read: 1,
      /**
       * If the host is write only.
       */
      Write: 2,
      /**
       * If the host is both read and write.
       */
      Any: 3,
  };
  
  const HostStatusEnum = {
      Up: 1,
      Down: 2,
      Timeouted: 3,
  };
  
  // By default, API Clients at Algolia have expiration delay
  // of 5 mins. In the JavaScript client, we have 2 mins.
  const EXPIRATION_DELAY = 2 * 60 * 1000;
  function createStatefulHost(host, status = HostStatusEnum.Up) {
      return {
          ...host,
          status,
          lastUpdate: Date.now(),
      };
  }
  function isStatefulHostUp(host) {
      return host.status === HostStatusEnum.Up || Date.now() - host.lastUpdate > EXPIRATION_DELAY;
  }
  function isStatefulHostTimeouted(host) {
      return (host.status === HostStatusEnum.Timeouted && Date.now() - host.lastUpdate <= EXPIRATION_DELAY);
  }
  
  function createStatelessHost(options) {
      if (typeof options === 'string') {
          return {
              protocol: 'https',
              url: options,
              accept: CallEnum.Any,
          };
      }
      return {
          protocol: options.protocol || 'https',
          url: options.url,
          accept: options.accept || CallEnum.Any,
      };
  }
  
  function createRetryableOptions(hostsCache, statelessHosts) {
      return Promise.all(statelessHosts.map(statelessHost => {
          return hostsCache.get(statelessHost, () => {
              return Promise.resolve(createStatefulHost(statelessHost));
          });
      })).then(statefulHosts => {
          const hostsUp = statefulHosts.filter(host => isStatefulHostUp(host));
          const hostsTimeouted = statefulHosts.filter(host => isStatefulHostTimeouted(host));
          /**
           * Note, we put the hosts that previously timeouted on the end of the list.
           */
          const hostsAvailable = [...hostsUp, ...hostsTimeouted];
          const statelessHostsAvailable = hostsAvailable.length > 0
              ? hostsAvailable.map(host => createStatelessHost(host))
              : statelessHosts;
          return {
              getTimeout(timeoutsCount, baseTimeout) {
                  /**
                   * Imagine that you have 4 hosts, if timeouts will increase
                   * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200)
                   *
                   * Note that, the very next request, we start from the previous timeout
                   *
                   *  5 (timeouted) > 6 (timeouted) > 7 ...
                   *
                   * This strategy may need to be reviewed, but is the strategy on the our
                   * current v3 version.
                   */
                  const timeoutMultiplier = hostsTimeouted.length === 0 && timeoutsCount === 0
                      ? 1
                      : hostsTimeouted.length + 3 + timeoutsCount;
                  return timeoutMultiplier * baseTimeout;
              },
              statelessHosts: statelessHostsAvailable,
          };
      });
  }
  
  const isNetworkError = ({ isTimedOut, status }) => {
      return !isTimedOut && ~~status === 0;
  };
  const isRetryable = (response) => {
      const status = response.status;
      const isTimedOut = response.isTimedOut;
      return (isTimedOut || isNetworkError(response) || (~~(status / 100) !== 2 && ~~(status / 100) !== 4));
  };
  const isSuccess = ({ status }) => {
      return ~~(status / 100) === 2;
  };
  const retryDecision = (response, outcomes) => {
      if (isRetryable(response)) {
          return outcomes.onRetry(response);
      }
      if (isSuccess(response)) {
          return outcomes.onSucess(response);
      }
      return outcomes.onFail(response);
  };
  
  function retryableRequest(transporter, statelessHosts, request, requestOptions) {
      const stackTrace = []; // eslint-disable-line functional/prefer-readonly-type
      /**
       * First we prepare the payload that do not depend from hosts.
       */
      const data = serializeData(request, requestOptions);
      const headers = serializeHeaders(transporter, requestOptions);
      const method = request.method;
      // On `GET`, the data is proxied to query parameters.
      const dataQueryParameters = request.method !== MethodEnum.Get
          ? {}
          : {
              ...request.data,
              ...requestOptions.data,
          };
      const queryParameters = {
          'x-algolia-agent': transporter.userAgent.value,
          ...transporter.queryParameters,
          ...dataQueryParameters,
          ...requestOptions.queryParameters,
      };
      let timeoutsCount = 0; // eslint-disable-line functional/no-let
      const retry = (hosts, // eslint-disable-line functional/prefer-readonly-type
      getTimeout) => {
          /**
           * We iterate on each host, until there is no host left.
           */
          const host = hosts.pop(); // eslint-disable-line functional/immutable-data
          if (host === undefined) {
              throw createRetryError(stackTraceWithoutCredentials(stackTrace));
          }
          const payload = {
              data,
              headers,
              method,
              url: serializeUrl(host, request.path, queryParameters),
              connectTimeout: getTimeout(timeoutsCount, transporter.timeouts.connect),
              responseTimeout: getTimeout(timeoutsCount, requestOptions.timeout),
          };
          /**
           * The stackFrame is pushed to the stackTrace so we
           * can have information about onRetry and onFailure
           * decisions.
           */
          const pushToStackTrace = (response) => {
              const stackFrame = {
                  request: payload,
                  response,
                  host,
                  triesLeft: hosts.length,
              };
              // eslint-disable-next-line functional/immutable-data
              stackTrace.push(stackFrame);
              return stackFrame;
          };
          const decisions = {
              onSucess: response => deserializeSuccess(response),
              onRetry(response) {
                  const stackFrame = pushToStackTrace(response);
                  /**
                   * If response is a timeout, we increaset the number of
                   * timeouts so we can increase the timeout later.
                   */
                  if (response.isTimedOut) {
                      timeoutsCount++;
                  }
                  return Promise.all([
                      /**
                       * Failures are individually send the logger, allowing
                       * the end user to debug / store stack frames even
                       * when a retry error does not happen.
                       */
                      transporter.logger.info('Retryable failure', stackFrameWithoutCredentials(stackFrame)),
                      /**
                       * We also store the state of the host in failure cases. If the host, is
                       * down it will remain down for the next 2 minutes. In a timeout situation,
                       * this host will be added end of the list of hosts on the next request.
                       */
                      transporter.hostsCache.set(host, createStatefulHost(host, response.isTimedOut ? HostStatusEnum.Timeouted : HostStatusEnum.Down)),
                  ]).then(() => retry(hosts, getTimeout));
              },
              onFail(response) {
                  pushToStackTrace(response);
                  throw deserializeFailure(response, stackTraceWithoutCredentials(stackTrace));
              },
          };
          return transporter.requester.send(payload).then(response => {
              return retryDecision(response, decisions);
          });
      };
      /**
       * Finally, for each retryable host perform request until we got a non
       * retryable response. Some notes here:
       *
       * 1. The reverse here is applied so we can apply a `pop` later on => more performant.
       * 2. We also get from the retryable options a timeout multiplier that is tailored
       * for the current context.
       */
      return createRetryableOptions(transporter.hostsCache, statelessHosts).then(options => {
          return retry([...options.statelessHosts].reverse(), options.getTimeout);
      });
  }
  
  function createTransporter(options) {
      const { hostsCache, logger, requester, requestsCache, responsesCache, timeouts, userAgent, hosts, queryParameters, headers, } = options;
      const transporter = {
          hostsCache,
          logger,
          requester,
          requestsCache,
          responsesCache,
          timeouts,
          userAgent,
          headers,
          queryParameters,
          hosts: hosts.map(host => createStatelessHost(host)),
          read(request, requestOptions) {
              /**
               * First, we compute the user request options. Now, keep in mind,
               * that using request options the user is able to modified the intire
               * payload of the request. Such as headers, query parameters, and others.
               */
              const mappedRequestOptions = createMappedRequestOptions(requestOptions, transporter.timeouts.read);
              const createRetryableRequest = () => {
                  /**
                   * Then, we prepare a function factory that contains the construction of
                   * the retryable request. At this point, we may *not* perform the actual
                   * request. But we want to have the function factory ready.
                   */
                  return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Read) !== 0), request, mappedRequestOptions);
              };
              /**
               * Once we have the function factory ready, we need to determine of the
               * request is "cacheable" - should be cached. Note that, once again,
               * the user can force this option.
               */
              const cacheable = mappedRequestOptions.cacheable !== undefined
                  ? mappedRequestOptions.cacheable
                  : request.cacheable;
              /**
               * If is not "cacheable", we immediatly trigger the retryable request, no
               * need to check cache implementations.
               */
              if (cacheable !== true) {
                  return createRetryableRequest();
              }
              /**
               * If the request is "cacheable", we need to first compute the key to ask
               * the cache implementations if this request is on progress or if the
               * response already exists on the cache.
               */
              const key = {
                  request,
                  mappedRequestOptions,
                  transporter: {
                      queryParameters: transporter.queryParameters,
                      headers: transporter.headers,
                  },
              };
              /**
               * With the computed key, we first ask the responses cache
               * implemention if this request was been resolved before.
               */
              return transporter.responsesCache.get(key, () => {
                  /**
                   * If the request has never resolved before, we actually ask if there
                   * is a current request with the same key on progress.
                   */
                  return transporter.requestsCache.get(key, () => {
                      return (transporter.requestsCache
                          /**
                           * Finally, if there is no request in progress with the same key,
                           * this `createRetryableRequest()` will actually trigger the
                           * retryable request.
                           */
                          .set(key, createRetryableRequest())
                          .then(response => Promise.all([transporter.requestsCache.delete(key), response]), err => Promise.all([transporter.requestsCache.delete(key), Promise.reject(err)]))
                          .then(([_, response]) => response));
                  });
              }, {
                  /**
                   * Of course, once we get this response back from the server, we
                   * tell response cache to actually store the received response
                   * to be used later.
                   */
                  miss: response => transporter.responsesCache.set(key, response),
              });
          },
          write(request, requestOptions) {
              /**
               * On write requests, no cache mechanisms are applied, and we
               * proxy the request immediately to the requester.
               */
              return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Write) !== 0), request, createMappedRequestOptions(requestOptions, transporter.timeouts.write));
          },
      };
      return transporter;
  }
  
  function createUserAgent(version) {
      const userAgent = {
          value: `Algolia for JavaScript (${version})`,
          add(options) {
              const addedUserAgent = `; ${options.segment}${options.version !== undefined ? ` (${options.version})` : ''}`;
              if (userAgent.value.indexOf(addedUserAgent) === -1) {
                  // eslint-disable-next-line functional/immutable-data
                  userAgent.value = `${userAgent.value}${addedUserAgent}`;
              }
              return userAgent;
          },
      };
      return userAgent;
  }
  
  function deserializeSuccess(response) {
      // eslint-disable-next-line functional/no-try-statement
      try {
          return JSON.parse(response.content);
      }
      catch (e) {
          throw createDeserializationError(e.message, response);
      }
  }
  function deserializeFailure({ content, status }, stackFrame) {
      // eslint-disable-next-line functional/no-let
      let message = content;
      // eslint-disable-next-line functional/no-try-statement
      try {
          message = JSON.parse(content).message;
      }
      catch (e) {
          // ..
      }
      return createApiError(message, status, stackFrame);
  }
  
  // eslint-disable-next-line functional/prefer-readonly-type
  function encode(format, ...args) {
      // eslint-disable-next-line functional/no-let
      let i = 0;
      return format.replace(/%s/g, () => encodeURIComponent(args[i++]));
  }
  
  function serializeUrl(host, path, queryParameters) {
      const queryParametersAsString = serializeQueryParameters(queryParameters);
      // eslint-disable-next-line functional/no-let
      let url = `${host.protocol}://${host.url}/${path.charAt(0) === '/' ? path.substr(1) : path}`;
      if (queryParametersAsString.length) {
          url += `?${queryParametersAsString}`;
      }
      return url;
  }
  function serializeQueryParameters(parameters) {
      const isObjectOrArray = (value) => Object.prototype.toString.call(value) === '[object Object]' ||
          Object.prototype.toString.call(value) === '[object Array]';
      return Object.keys(parameters)
          .map(key => encode('%s=%s', key, isObjectOrArray(parameters[key]) ? JSON.stringify(parameters[key]) : parameters[key]))
          .join('&');
  }
  function serializeData(request, requestOptions) {
      if (request.method === MethodEnum.Get ||
          (request.data === undefined && requestOptions.data === undefined)) {
          return undefined;
      }
      const data = Array.isArray(request.data)
          ? request.data
          : { ...request.data, ...requestOptions.data };
      return JSON.stringify(data);
  }
  function serializeHeaders(transporter, requestOptions) {
      const headers = {
          ...transporter.headers,
          ...requestOptions.headers,
      };
      const serializedHeaders = {};
      Object.keys(headers).forEach(header => {
          const value = headers[header];
          // @ts-ignore
          // eslint-disable-next-line functional/immutable-data
          serializedHeaders[header.toLowerCase()] = value;
      });
      return serializedHeaders;
  }
  
  function stackTraceWithoutCredentials(stackTrace) {
      return stackTrace.map(stackFrame => stackFrameWithoutCredentials(stackFrame));
  }
  function stackFrameWithoutCredentials(stackFrame) {
      const modifiedHeaders = stackFrame.request.headers['x-algolia-api-key']
          ? { 'x-algolia-api-key': '*****' }
          : {};
      return {
          ...stackFrame,
          request: {
              ...stackFrame.request,
              headers: {
                  ...stackFrame.request.headers,
                  ...modifiedHeaders,
              },
          },
      };
  }
  
  function createApiError(message, status, transporterStackTrace) {
      return {
          name: 'ApiError',
          message,
          status,
          transporterStackTrace,
      };
  }
  
  function createDeserializationError(message, response) {
      return {
          name: 'DeserializationError',
          message,
          response,
      };
  }
  
  function createRetryError(transporterStackTrace) {
      return {
          name: 'RetryError',
          message: 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.',
          transporterStackTrace,
      };
  }
  
  export { CallEnum, HostStatusEnum, createApiError, createDeserializationError, createMappedRequestOptions, createRetryError, createStatefulHost, createStatelessHost, createTransporter, createUserAgent, deserializeFailure, deserializeSuccess, isStatefulHostTimeouted, isStatefulHostUp, serializeData, serializeHeaders, serializeQueryParameters, serializeUrl, stackFrameWithoutCredentials, stackTraceWithoutCredentials };
  