aboutsummaryrefslogtreecommitdiff
path: root/node_modules/algoliasearch/src/AlgoliaSearchCore.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/algoliasearch/src/AlgoliaSearchCore.js')
-rw-r--r--node_modules/algoliasearch/src/AlgoliaSearchCore.js966
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;
+}