diff options
Diffstat (limited to 'node_modules/algoliasearch/src/AlgoliaSearchCore.js')
| -rw-r--r-- | node_modules/algoliasearch/src/AlgoliaSearchCore.js | 966 |
1 files changed, 966 insertions, 0 deletions
diff --git a/node_modules/algoliasearch/src/AlgoliaSearchCore.js b/node_modules/algoliasearch/src/AlgoliaSearchCore.js new file mode 100644 index 00000000..a94e99ca --- /dev/null +++ b/node_modules/algoliasearch/src/AlgoliaSearchCore.js @@ -0,0 +1,966 @@ +module.exports = AlgoliaSearchCore; + +var errors = require('./errors'); +var exitPromise = require('./exitPromise.js'); +var IndexCore = require('./IndexCore.js'); +var store = require('./store.js'); + +// We will always put the API KEY in the JSON body in case of too long API KEY, +// to avoid query string being too long and failing in various conditions (our server limit, browser limit, +// proxies limit) +var MAX_API_KEY_LENGTH = 500; +var RESET_APP_DATA_TIMER = + process.env.RESET_APP_DATA_TIMER && parseInt(process.env.RESET_APP_DATA_TIMER, 10) || + 60 * 2 * 1000; // after 2 minutes reset to first host + +/* + * Algolia Search library initialization + * https://www.algolia.com/ + * + * @param {string} applicationID - Your applicationID, found in your dashboard + * @param {string} apiKey - Your API key, found in your dashboard + * @param {Object} [opts] + * @param {number} [opts.timeout=2000] - The request timeout set in milliseconds, + * another request will be issued after this timeout + * @param {string} [opts.protocol='https:'] - The protocol used to query Algolia Search API. + * Set to 'http:' to force using http. + * @param {Object|Array} [opts.hosts={ + * read: [this.applicationID + '-dsn.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]), + * write: [this.applicationID + '.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]) - The hosts to use for Algolia Search API. + * If you provide them, you will less benefit from our HA implementation + */ +function AlgoliaSearchCore(applicationID, apiKey, opts) { + var debug = require('debug')('algoliasearch'); + + var clone = require('./clone.js'); + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: algoliasearch(applicationID, apiKey, opts)'; + + if (opts._allowEmptyCredentials !== true && !applicationID) { + throw new errors.AlgoliaSearchError('Please provide an application ID. ' + usage); + } + + if (opts._allowEmptyCredentials !== true && !apiKey) { + throw new errors.AlgoliaSearchError('Please provide an API key. ' + usage); + } + + this.applicationID = applicationID; + this.apiKey = apiKey; + + this.hosts = { + read: [], + write: [] + }; + + opts = opts || {}; + + this._timeouts = opts.timeouts || { + connect: 1 * 1000, // 500ms connect is GPRS latency + read: 2 * 1000, + write: 30 * 1000 + }; + + // backward compat, if opts.timeout is passed, we use it to configure all timeouts like before + if (opts.timeout) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = opts.timeout; + } + + var protocol = opts.protocol || 'https:'; + // while we advocate for colon-at-the-end values: 'http:' for `opts.protocol` + // we also accept `http` and `https`. It's a common error. + if (!/:$/.test(protocol)) { + protocol = protocol + ':'; + } + + if (protocol !== 'http:' && protocol !== 'https:') { + throw new errors.AlgoliaSearchError('protocol must be `http:` or `https:` (was `' + opts.protocol + '`)'); + } + + this._checkAppIdData(); + + if (!opts.hosts) { + var defaultHosts = map(this._shuffleResult, function(hostNumber) { + return applicationID + '-' + hostNumber + '.algolianet.com'; + }); + + // no hosts given, compute defaults + var mainSuffix = (opts.dsn === false ? '' : '-dsn') + '.algolia.net'; + this.hosts.read = [this.applicationID + mainSuffix].concat(defaultHosts); + this.hosts.write = [this.applicationID + '.algolia.net'].concat(defaultHosts); + } else if (isArray(opts.hosts)) { + // when passing custom hosts, we need to have a different host index if the number + // of write/read hosts are different. + this.hosts.read = clone(opts.hosts); + this.hosts.write = clone(opts.hosts); + } else { + this.hosts.read = clone(opts.hosts.read); + this.hosts.write = clone(opts.hosts.write); + } + + // add protocol and lowercase hosts + this.hosts.read = map(this.hosts.read, prepareHost(protocol)); + this.hosts.write = map(this.hosts.write, prepareHost(protocol)); + + this.extraHeaders = {}; + + // In some situations you might want to warm the cache + this.cache = opts._cache || {}; + + this._ua = opts._ua; + this._useCache = opts._useCache === undefined || opts._cache ? true : opts._useCache; + this._useRequestCache = this._useCache && opts._useRequestCache; + this._useFallback = opts.useFallback === undefined ? true : opts.useFallback; + + this._setTimeout = opts._setTimeout; + + debug('init done, %j', this); +} + +/* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ +AlgoliaSearchCore.prototype.initIndex = function(indexName) { + return new IndexCore(this, indexName); +}; + +/** +* Add an extra field to the HTTP request +* +* @param name the header field name +* @param value the header field value +*/ +AlgoliaSearchCore.prototype.setExtraHeader = function(name, value) { + this.extraHeaders[name.toLowerCase()] = value; +}; + +/** +* Get the value of an extra HTTP header +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.getExtraHeader = function(name) { + return this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Remove an extra field from the HTTP request +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.unsetExtraHeader = function(name) { + delete this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Augment sent x-algolia-agent with more data, each agent part +* is automatically separated from the others by a semicolon; +* +* @param algoliaAgent the agent to add +*/ +AlgoliaSearchCore.prototype.addAlgoliaAgent = function(algoliaAgent) { + if (this._ua.indexOf(';' + algoliaAgent) === -1) { + this._ua += ';' + algoliaAgent; + } +}; + +/* + * Wrapper that try all hosts to maximize the quality of service + */ +AlgoliaSearchCore.prototype._jsonRequest = function(initialOpts) { + this._checkAppIdData(); + + var requestDebug = require('debug')('algoliasearch:' + initialOpts.url); + + + var body; + var cacheID; + var additionalUA = initialOpts.additionalUA || ''; + var cache = initialOpts.cache; + var client = this; + var tries = 0; + var usingFallback = false; + var hasFallback = client._useFallback && client._request.fallback && initialOpts.fallback; + var headers; + + if ( + this.apiKey.length > MAX_API_KEY_LENGTH && + initialOpts.body !== undefined && + (initialOpts.body.params !== undefined || // index.search() + initialOpts.body.requests !== undefined) // client.search() + ) { + initialOpts.body.apiKey = this.apiKey; + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + withApiKey: false, + headers: initialOpts.headers + }); + } else { + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + } + + if (initialOpts.body !== undefined) { + body = safeJSONStringify(initialOpts.body); + } + + requestDebug('request start'); + var debugData = []; + + + function doRequest(requester, reqOpts) { + client._checkAppIdData(); + + var startTime = new Date(); + + if (client._useCache && !client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && !client._useRequestCache && body) { + cacheID += '_body_' + reqOpts.body; + } + + // handle cache existence + if (isCacheValidWithCurrentID(!client._useRequestCache, cache, cacheID)) { + requestDebug('serving response from cache'); + + var responseText = cache[cacheID]; + + // Cache response must match the type of the original one + return client._promise.resolve({ + body: JSON.parse(responseText), + responseText: responseText + }); + } + + // if we reached max tries + if (tries >= client.hosts[initialOpts.hostType].length) { + if (!hasFallback || usingFallback) { + requestDebug('could not get any response'); + // then stop + return client._promise.reject(new errors.AlgoliaSearchError( + 'Cannot connect to the AlgoliaSearch API.' + + ' Send an email to support@algolia.com to report and resolve the issue.' + + ' Application id was: ' + client.applicationID, {debugData: debugData} + )); + } + + requestDebug('switching to fallback'); + + // let's try the fallback starting from here + tries = 0; + + // method, url and body are fallback dependent + reqOpts.method = initialOpts.fallback.method; + reqOpts.url = initialOpts.fallback.url; + reqOpts.jsonBody = initialOpts.fallback.body; + if (reqOpts.jsonBody) { + reqOpts.body = safeJSONStringify(reqOpts.jsonBody); + } + // re-compute headers, they could be omitting the API KEY + headers = client._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + client._setHostIndexByType(0, initialOpts.hostType); + usingFallback = true; // the current request is now using fallback + return doRequest(client._request.fallback, reqOpts); + } + + var currentHost = client._getHostByType(initialOpts.hostType); + + var url = currentHost + reqOpts.url; + var options = { + body: reqOpts.body, + jsonBody: reqOpts.jsonBody, + method: reqOpts.method, + headers: headers, + timeouts: reqOpts.timeouts, + debug: requestDebug, + forceAuthHeaders: reqOpts.forceAuthHeaders + }; + + requestDebug('method: %s, url: %s, headers: %j, timeouts: %d', + options.method, url, options.headers, options.timeouts); + + if (requester === client._request.fallback) { + requestDebug('using fallback'); + } + + // `requester` is any of this._request or this._request.fallback + // thus it needs to be called using the client as context + return requester.call(client, url, options).then(success, tryFallback); + + function success(httpResponse) { + // compute the status of the response, + // + // When in browser mode, using XDR or JSONP, we have no statusCode available + // So we rely on our API response `status` property. + // But `waitTask` can set a `status` property which is not the statusCode (it's the task status) + // So we check if there's a `message` along `status` and it means it's an error + // + // That's the only case where we have a response.status that's not the http statusCode + var status = httpResponse && httpResponse.body && httpResponse.body.message && httpResponse.body.status || + + // this is important to check the request statusCode AFTER the body eventual + // statusCode because some implementations (jQuery XDomainRequest transport) may + // send statusCode 200 while we had an error + httpResponse.statusCode || + + // When in browser mode, using XDR or JSONP + // we default to success when no error (no response.status && response.message) + // If there was a JSON.parse() error then body is null and it fails + httpResponse && httpResponse.body && 200; + + requestDebug('received response: statusCode: %s, computed statusCode: %d, headers: %j', + httpResponse.statusCode, status, httpResponse.headers); + + var httpResponseOk = Math.floor(status / 100) === 2; + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime, + statusCode: status + }); + + if (httpResponseOk) { + if (client._useCache && !client._useRequestCache && cache) { + cache[cacheID] = httpResponse.responseText; + } + + return { + responseText: httpResponse.responseText, + body: httpResponse.body + }; + } + + var shouldRetry = Math.floor(status / 100) !== 4; + + if (shouldRetry) { + tries += 1; + return retryRequest(); + } + + requestDebug('unrecoverable error'); + + // no success and no retry => fail + var unrecoverableError = new errors.AlgoliaSearchError( + httpResponse.body && httpResponse.body.message, {debugData: debugData, statusCode: status} + ); + + return client._promise.reject(unrecoverableError); + } + + function tryFallback(err) { + // error cases: + // While not in fallback mode: + // - CORS not supported + // - network error + // While in fallback mode: + // - timeout + // - network error + // - badly formatted JSONP (script loaded, did not call our callback) + // In both cases: + // - uncaught exception occurs (TypeError) + requestDebug('error: %s, stack: %s', err.message, err.stack); + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime + }); + + if (!(err instanceof errors.AlgoliaSearchError)) { + err = new errors.Unknown(err && err.message, err); + } + + tries += 1; + + // stop the request implementation when: + if ( + // we did not generate this error, + // it comes from a throw in some other piece of code + err instanceof errors.Unknown || + + // server sent unparsable JSON + err instanceof errors.UnparsableJSON || + + // max tries and already using fallback or no fallback + tries >= client.hosts[initialOpts.hostType].length && + (usingFallback || !hasFallback)) { + // stop request implementation for this command + err.debugData = debugData; + return client._promise.reject(err); + } + + // When a timeout occured, retry by raising timeout + if (err instanceof errors.RequestTimeout) { + return retryRequestWithHigherTimeout(); + } + + return retryRequest(); + } + + function retryRequest() { + requestDebug('retrying request'); + client._incrementHostIndex(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + + function retryRequestWithHigherTimeout() { + requestDebug('retrying request with higher timeout'); + client._incrementHostIndex(initialOpts.hostType); + client._incrementTimeoutMultipler(); + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + } + + function isCacheValidWithCurrentID( + useRequestCache, + currentCache, + currentCacheID + ) { + return ( + client._useCache && + useRequestCache && + currentCache && + currentCache[currentCacheID] !== undefined + ); + } + + + function interopCallbackReturn(request, callback) { + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + request.catch(function() { + // Release the cache on error + delete cache[cacheID]; + }); + } + + if (typeof initialOpts.callback === 'function') { + // either we have a callback + request.then(function okCb(content) { + exitPromise(function() { + initialOpts.callback(null, callback(content)); + }, client._setTimeout || setTimeout); + }, function nookCb(err) { + exitPromise(function() { + initialOpts.callback(err); + }, client._setTimeout || setTimeout); + }); + } else { + // either we are using promises + return request.then(callback); + } + } + + if (client._useCache && client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && client._useRequestCache && body) { + cacheID += '_body_' + body; + } + + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + requestDebug('serving request from cache'); + + var maybePromiseForCache = cache[cacheID]; + + // In case the cache is warmup with value that is not a promise + var promiseForCache = typeof maybePromiseForCache.then !== 'function' + ? client._promise.resolve({responseText: maybePromiseForCache}) + : maybePromiseForCache; + + return interopCallbackReturn(promiseForCache, function(content) { + // In case of the cache request, return the original value + return JSON.parse(content.responseText); + }); + } + + var request = doRequest( + client._request, { + url: initialOpts.url, + method: initialOpts.method, + body: body, + jsonBody: initialOpts.body, + timeouts: client._getTimeoutsForRequest(initialOpts.hostType), + forceAuthHeaders: initialOpts.forceAuthHeaders + } + ); + + if (client._useCache && client._useRequestCache && cache) { + cache[cacheID] = request; + } + + return interopCallbackReturn(request, function(content) { + // In case of the first request, return the JSON value + return content.body; + }); +}; + +/* +* Transform search param object in query string +* @param {object} args arguments to add to the current query string +* @param {string} params current query string +* @return {string} the final query string +*/ +AlgoliaSearchCore.prototype._getSearchParams = function(args, params) { + if (args === undefined || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args[key] !== undefined && args.hasOwnProperty(key)) { + params += params === '' ? '' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? safeJSONStringify(args[key]) : args[key]); + } + } + return params; +}; + +/** + * Compute the headers for a request + * + * @param [string] options.additionalUA semi-colon separated string with other user agents to add + * @param [boolean=true] options.withApiKey Send the api key as a header + * @param [Object] options.headers Extra headers to send + */ +AlgoliaSearchCore.prototype._computeRequestHeaders = function(options) { + var forEach = require('foreach'); + + var ua = options.additionalUA ? + this._ua + ';' + options.additionalUA : + this._ua; + + var requestHeaders = { + 'x-algolia-agent': ua, + 'x-algolia-application-id': this.applicationID + }; + + // browser will inline headers in the url, node.js will use http headers + // but in some situations, the API KEY will be too long (big secured API keys) + // so if the request is a POST and the KEY is very long, we will be asked to not put + // it into headers but in the JSON body + if (options.withApiKey !== false) { + requestHeaders['x-algolia-api-key'] = this.apiKey; + } + + if (this.userToken) { + requestHeaders['x-algolia-usertoken'] = this.userToken; + } + + if (this.securityTags) { + requestHeaders['x-algolia-tagfilters'] = this.securityTags; + } + + forEach(this.extraHeaders, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + + if (options.headers) { + forEach(options.headers, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + } + + return requestHeaders; +}; + +/** + * Search through multiple indices at the same time + * @param {Object[]} queries An array of queries you want to run. + * @param {string} queries[].indexName The index name you want to target + * @param {string} [queries[].query] The query to issue on this index. Can also be passed into `params` + * @param {Object} queries[].params Any search param like hitsPerPage, .. + * @param {Function} callback Callback to be called + * @return {Promise|undefined} Returns a promise if no callback given + */ +AlgoliaSearchCore.prototype.search = function(queries, opts, callback) { + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: client.search(arrayOfQueries[, callback])'; + + if (!isArray(queries)) { + throw new Error(usage); + } + + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var client = this; + + var postObj = { + requests: map(queries, function prepareRequest(query) { + var params = ''; + + // allow query.query + // so we are mimicing the index.search(query, params) method + // {indexName:, query:, params:} + if (query.query !== undefined) { + params += 'query=' + encodeURIComponent(query.query); + } + + return { + indexName: query.indexName, + params: client._getSearchParams(query.params, params) + }; + }) + }; + + var JSONPParams = map(postObj.requests, function prepareJSONPParams(request, requestId) { + return requestId + '=' + + encodeURIComponent( + '/1/indexes/' + encodeURIComponent(request.indexName) + '?' + + request.params + ); + }).join('&'); + + var url = '/1/indexes/*/queries'; + + if (opts.strategy !== undefined) { + postObj.strategy = opts.strategy; + } + + return this._jsonRequest({ + cache: this.cache, + method: 'POST', + url: url, + body: postObj, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/*', + body: { + params: JSONPParams + } + }, + callback: callback + }); +}; + +/** +* Search for facet values +* https://www.algolia.com/doc/rest-api/search#search-for-facet-values +* This is the top-level API for SFFV. +* +* @param {object[]} queries An array of queries to run. +* @param {string} queries[].indexName Index name, name of the index to search. +* @param {object} queries[].params Query parameters. +* @param {string} queries[].params.facetName Facet name, name of the attribute to search for values in. +* Must be declared as a facet +* @param {string} queries[].params.facetQuery Query for the facet search +* @param {string} [queries[].params.*] Any search parameter of Algolia, +* see https://www.algolia.com/doc/api-client/javascript/search#search-parameters +* Pagination is not supported. The page and hitsPerPage parameters will be ignored. +*/ +AlgoliaSearchCore.prototype.searchForFacetValues = function(queries) { + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])'; // eslint-disable-line max-len + + if (!isArray(queries)) { + throw new Error(usage); + } + + var client = this; + + return client._promise.all(map(queries, function performQuery(query) { + if ( + !query || + query.indexName === undefined || + query.params.facetName === undefined || + query.params.facetQuery === undefined + ) { + throw new Error(usage); + } + + var clone = require('./clone.js'); + var omit = require('./omit.js'); + + var indexName = query.indexName; + var params = query.params; + + var facetName = params.facetName; + var filteredParams = omit(clone(params), function(keyName) { + return keyName === 'facetName'; + }); + var searchParameters = client._getSearchParams(filteredParams, ''); + + return client._jsonRequest({ + cache: client.cache, + method: 'POST', + url: + '/1/indexes/' + + encodeURIComponent(indexName) + + '/facets/' + + encodeURIComponent(facetName) + + '/query', + hostType: 'read', + body: {params: searchParameters} + }); + })); +}; + +/** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ +AlgoliaSearchCore.prototype.setSecurityTags = function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + + this.securityTags = tags; +}; + +/** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ +AlgoliaSearchCore.prototype.setUserToken = function(userToken) { + this.userToken = userToken; +}; + +/** + * Clear all queries in client's cache + * @return undefined + */ +AlgoliaSearchCore.prototype.clearCache = function() { + this.cache = {}; +}; + +/** +* Set the number of milliseconds a request can take before automatically being terminated. +* @deprecated +* @param {Number} milliseconds +*/ +AlgoliaSearchCore.prototype.setRequestTimeout = function(milliseconds) { + if (milliseconds) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = milliseconds; + } +}; + +/** +* Set the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.setTimeouts = function(timeouts) { + this._timeouts = timeouts; +}; + +/** +* Get the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.getTimeouts = function() { + return this._timeouts; +}; + +AlgoliaSearchCore.prototype._getAppIdData = function() { + var data = store.get(this.applicationID); + if (data !== null) this._cacheAppIdData(data); + return data; +}; + +AlgoliaSearchCore.prototype._setAppIdData = function(data) { + data.lastChange = (new Date()).getTime(); + this._cacheAppIdData(data); + return store.set(this.applicationID, data); +}; + +AlgoliaSearchCore.prototype._checkAppIdData = function() { + var data = this._getAppIdData(); + var now = (new Date()).getTime(); + if (data === null || now - data.lastChange > RESET_APP_DATA_TIMER) { + return this._resetInitialAppIdData(data); + } + + return data; +}; + +AlgoliaSearchCore.prototype._resetInitialAppIdData = function(data) { + var newData = data || {}; + newData.hostIndexes = {read: 0, write: 0}; + newData.timeoutMultiplier = 1; + newData.shuffleResult = newData.shuffleResult || shuffle([1, 2, 3]); + return this._setAppIdData(newData); +}; + +AlgoliaSearchCore.prototype._cacheAppIdData = function(data) { + this._hostIndexes = data.hostIndexes; + this._timeoutMultiplier = data.timeoutMultiplier; + this._shuffleResult = data.shuffleResult; +}; + +AlgoliaSearchCore.prototype._partialAppIdDataUpdate = function(newData) { + var foreach = require('foreach'); + var currentData = this._getAppIdData(); + foreach(newData, function(value, key) { + currentData[key] = value; + }); + + return this._setAppIdData(currentData); +}; + +AlgoliaSearchCore.prototype._getHostByType = function(hostType) { + return this.hosts[hostType][this._getHostIndexByType(hostType)]; +}; + +AlgoliaSearchCore.prototype._getTimeoutMultiplier = function() { + return this._timeoutMultiplier; +}; + +AlgoliaSearchCore.prototype._getHostIndexByType = function(hostType) { + return this._hostIndexes[hostType]; +}; + +AlgoliaSearchCore.prototype._setHostIndexByType = function(hostIndex, hostType) { + var clone = require('./clone'); + var newHostIndexes = clone(this._hostIndexes); + newHostIndexes[hostType] = hostIndex; + this._partialAppIdDataUpdate({hostIndexes: newHostIndexes}); + return hostIndex; +}; + +AlgoliaSearchCore.prototype._incrementHostIndex = function(hostType) { + return this._setHostIndexByType( + (this._getHostIndexByType(hostType) + 1) % this.hosts[hostType].length, hostType + ); +}; + +AlgoliaSearchCore.prototype._incrementTimeoutMultipler = function() { + var timeoutMultiplier = Math.max(this._timeoutMultiplier + 1, 4); + return this._partialAppIdDataUpdate({timeoutMultiplier: timeoutMultiplier}); +}; + +AlgoliaSearchCore.prototype._getTimeoutsForRequest = function(hostType) { + return { + connect: this._timeouts.connect * this._timeoutMultiplier, + complete: this._timeouts[hostType] * this._timeoutMultiplier + }; +}; + +function prepareHost(protocol) { + return function prepare(host) { + return protocol + '//' + host.toLowerCase(); + }; +} + +// Prototype.js < 1.7, a widely used library, defines a weird +// Array.prototype.toJSON function that will fail to stringify our content +// appropriately +// refs: +// - https://groups.google.com/forum/#!topic/prototype-core/E-SAVvV_V9Q +// - https://github.com/sstephenson/prototype/commit/038a2985a70593c1a86c230fadbdfe2e4898a48c +// - http://stackoverflow.com/a/3148441/147079 +function safeJSONStringify(obj) { + /* eslint no-extend-native:0 */ + + if (Array.prototype.toJSON === undefined) { + return JSON.stringify(obj); + } + + var toJSON = Array.prototype.toJSON; + delete Array.prototype.toJSON; + var out = JSON.stringify(obj); + Array.prototype.toJSON = toJSON; + + return out; +} + +function shuffle(array) { + var currentIndex = array.length; + var temporaryValue; + var randomIndex; + + // While there remain elements to shuffle... + while (currentIndex !== 0) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + +function removeCredentials(headers) { + var newHeaders = {}; + + for (var headerName in headers) { + if (Object.prototype.hasOwnProperty.call(headers, headerName)) { + var value; + + if (headerName === 'x-algolia-api-key' || headerName === 'x-algolia-application-id') { + value = '**hidden for security purposes**'; + } else { + value = headers[headerName]; + } + + newHeaders[headerName] = value; + } + } + + return newHeaders; +} |
