123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- 'use strict';
- Object.defineProperty(exports, '__esModule', { value: true });
- var requesterCommon = require('@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.onSuccess(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 !== requesterCommon.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 = {
- onSuccess: 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 === requesterCommon.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,
- };
- }
- exports.CallEnum = CallEnum;
- exports.HostStatusEnum = HostStatusEnum;
- exports.createApiError = createApiError;
- exports.createDeserializationError = createDeserializationError;
- exports.createMappedRequestOptions = createMappedRequestOptions;
- exports.createRetryError = createRetryError;
- exports.createStatefulHost = createStatefulHost;
- exports.createStatelessHost = createStatelessHost;
- exports.createTransporter = createTransporter;
- exports.createUserAgent = createUserAgent;
- exports.deserializeFailure = deserializeFailure;
- exports.deserializeSuccess = deserializeSuccess;
- exports.isStatefulHostTimeouted = isStatefulHostTimeouted;
- exports.isStatefulHostUp = isStatefulHostUp;
- exports.serializeData = serializeData;
- exports.serializeHeaders = serializeHeaders;
- exports.serializeQueryParameters = serializeQueryParameters;
- exports.serializeUrl = serializeUrl;
- exports.stackFrameWithoutCredentials = stackFrameWithoutCredentials;
- exports.stackTraceWithoutCredentials = stackTraceWithoutCredentials;
|