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; }