algoliasearch-lite.esm.browser.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. function createBrowserLocalStorageCache(options) {
  2. const namespaceKey = `algoliasearch-client-js-${options.key}`;
  3. // eslint-disable-next-line functional/no-let
  4. let storage;
  5. const getStorage = () => {
  6. if (storage === undefined) {
  7. storage = options.localStorage || window.localStorage;
  8. }
  9. return storage;
  10. };
  11. const getNamespace = () => {
  12. return JSON.parse(getStorage().getItem(namespaceKey) || '{}');
  13. };
  14. return {
  15. get(key, defaultValue, events = {
  16. miss: () => Promise.resolve(),
  17. }) {
  18. return Promise.resolve()
  19. .then(() => {
  20. const keyAsString = JSON.stringify(key);
  21. const value = getNamespace()[keyAsString];
  22. return Promise.all([value || defaultValue(), value !== undefined]);
  23. })
  24. .then(([value, exists]) => {
  25. return Promise.all([value, exists || events.miss(value)]);
  26. })
  27. .then(([value]) => value);
  28. },
  29. set(key, value) {
  30. return Promise.resolve().then(() => {
  31. const namespace = getNamespace();
  32. // eslint-disable-next-line functional/immutable-data
  33. namespace[JSON.stringify(key)] = value;
  34. getStorage().setItem(namespaceKey, JSON.stringify(namespace));
  35. return value;
  36. });
  37. },
  38. delete(key) {
  39. return Promise.resolve().then(() => {
  40. const namespace = getNamespace();
  41. // eslint-disable-next-line functional/immutable-data
  42. delete namespace[JSON.stringify(key)];
  43. getStorage().setItem(namespaceKey, JSON.stringify(namespace));
  44. });
  45. },
  46. clear() {
  47. return Promise.resolve().then(() => {
  48. getStorage().removeItem(namespaceKey);
  49. });
  50. },
  51. };
  52. }
  53. // @todo Add logger on options to debug when caches go wrong.
  54. function createFallbackableCache(options) {
  55. const caches = [...options.caches];
  56. const current = caches.shift(); // eslint-disable-line functional/immutable-data
  57. if (current === undefined) {
  58. return createNullCache();
  59. }
  60. return {
  61. get(key, defaultValue, events = {
  62. miss: () => Promise.resolve(),
  63. }) {
  64. return current.get(key, defaultValue, events).catch(() => {
  65. return createFallbackableCache({ caches }).get(key, defaultValue, events);
  66. });
  67. },
  68. set(key, value) {
  69. return current.set(key, value).catch(() => {
  70. return createFallbackableCache({ caches }).set(key, value);
  71. });
  72. },
  73. delete(key) {
  74. return current.delete(key).catch(() => {
  75. return createFallbackableCache({ caches }).delete(key);
  76. });
  77. },
  78. clear() {
  79. return current.clear().catch(() => {
  80. return createFallbackableCache({ caches }).clear();
  81. });
  82. },
  83. };
  84. }
  85. function createNullCache() {
  86. return {
  87. get(_key, defaultValue, events = {
  88. miss: () => Promise.resolve(),
  89. }) {
  90. const value = defaultValue();
  91. return value
  92. .then(result => Promise.all([result, events.miss(result)]))
  93. .then(([result]) => result);
  94. },
  95. set(_key, value) {
  96. return Promise.resolve(value);
  97. },
  98. delete(_key) {
  99. return Promise.resolve();
  100. },
  101. clear() {
  102. return Promise.resolve();
  103. },
  104. };
  105. }
  106. function createInMemoryCache(options = { serializable: true }) {
  107. // eslint-disable-next-line functional/no-let
  108. let cache = {};
  109. return {
  110. get(key, defaultValue, events = {
  111. miss: () => Promise.resolve(),
  112. }) {
  113. const keyAsString = JSON.stringify(key);
  114. if (keyAsString in cache) {
  115. return Promise.resolve(options.serializable ? JSON.parse(cache[keyAsString]) : cache[keyAsString]);
  116. }
  117. const promise = defaultValue();
  118. const miss = (events && events.miss) || (() => Promise.resolve());
  119. return promise.then((value) => miss(value)).then(() => promise);
  120. },
  121. set(key, value) {
  122. // eslint-disable-next-line functional/immutable-data
  123. cache[JSON.stringify(key)] = options.serializable ? JSON.stringify(value) : value;
  124. return Promise.resolve(value);
  125. },
  126. delete(key) {
  127. // eslint-disable-next-line functional/immutable-data
  128. delete cache[JSON.stringify(key)];
  129. return Promise.resolve();
  130. },
  131. clear() {
  132. cache = {};
  133. return Promise.resolve();
  134. },
  135. };
  136. }
  137. function createAuth(authMode, appId, apiKey) {
  138. const credentials = {
  139. 'x-algolia-api-key': apiKey,
  140. 'x-algolia-application-id': appId,
  141. };
  142. return {
  143. headers() {
  144. return authMode === AuthMode.WithinHeaders ? credentials : {};
  145. },
  146. queryParameters() {
  147. return authMode === AuthMode.WithinQueryParameters ? credentials : {};
  148. },
  149. };
  150. }
  151. // eslint-disable-next-line functional/prefer-readonly-type
  152. function shuffle(array) {
  153. let c = array.length - 1; // eslint-disable-line functional/no-let
  154. // eslint-disable-next-line functional/no-loop-statement
  155. for (c; c > 0; c--) {
  156. const b = Math.floor(Math.random() * (c + 1));
  157. const a = array[c];
  158. array[c] = array[b]; // eslint-disable-line functional/immutable-data, no-param-reassign
  159. array[b] = a; // eslint-disable-line functional/immutable-data, no-param-reassign
  160. }
  161. return array;
  162. }
  163. function addMethods(base, methods) {
  164. if (!methods) {
  165. return base;
  166. }
  167. Object.keys(methods).forEach(key => {
  168. // eslint-disable-next-line functional/immutable-data, no-param-reassign
  169. base[key] = methods[key](base);
  170. });
  171. return base;
  172. }
  173. function encode(format, ...args) {
  174. // eslint-disable-next-line functional/no-let
  175. let i = 0;
  176. return format.replace(/%s/g, () => encodeURIComponent(args[i++]));
  177. }
  178. const version = '4.13.0';
  179. const AuthMode = {
  180. /**
  181. * If auth credentials should be in query parameters.
  182. */
  183. WithinQueryParameters: 0,
  184. /**
  185. * If auth credentials should be in headers.
  186. */
  187. WithinHeaders: 1,
  188. };
  189. function createMappedRequestOptions(requestOptions, timeout) {
  190. const options = requestOptions || {};
  191. const data = options.data || {};
  192. Object.keys(options).forEach(key => {
  193. if (['timeout', 'headers', 'queryParameters', 'data', 'cacheable'].indexOf(key) === -1) {
  194. data[key] = options[key]; // eslint-disable-line functional/immutable-data
  195. }
  196. });
  197. return {
  198. data: Object.entries(data).length > 0 ? data : undefined,
  199. timeout: options.timeout || timeout,
  200. headers: options.headers || {},
  201. queryParameters: options.queryParameters || {},
  202. cacheable: options.cacheable,
  203. };
  204. }
  205. const CallEnum = {
  206. /**
  207. * If the host is read only.
  208. */
  209. Read: 1,
  210. /**
  211. * If the host is write only.
  212. */
  213. Write: 2,
  214. /**
  215. * If the host is both read and write.
  216. */
  217. Any: 3,
  218. };
  219. const HostStatusEnum = {
  220. Up: 1,
  221. Down: 2,
  222. Timeouted: 3,
  223. };
  224. // By default, API Clients at Algolia have expiration delay
  225. // of 5 mins. In the JavaScript client, we have 2 mins.
  226. const EXPIRATION_DELAY = 2 * 60 * 1000;
  227. function createStatefulHost(host, status = HostStatusEnum.Up) {
  228. return {
  229. ...host,
  230. status,
  231. lastUpdate: Date.now(),
  232. };
  233. }
  234. function isStatefulHostUp(host) {
  235. return host.status === HostStatusEnum.Up || Date.now() - host.lastUpdate > EXPIRATION_DELAY;
  236. }
  237. function isStatefulHostTimeouted(host) {
  238. return (host.status === HostStatusEnum.Timeouted && Date.now() - host.lastUpdate <= EXPIRATION_DELAY);
  239. }
  240. function createStatelessHost(options) {
  241. if (typeof options === 'string') {
  242. return {
  243. protocol: 'https',
  244. url: options,
  245. accept: CallEnum.Any,
  246. };
  247. }
  248. return {
  249. protocol: options.protocol || 'https',
  250. url: options.url,
  251. accept: options.accept || CallEnum.Any,
  252. };
  253. }
  254. const MethodEnum = {
  255. Delete: 'DELETE',
  256. Get: 'GET',
  257. Post: 'POST',
  258. Put: 'PUT',
  259. };
  260. function createRetryableOptions(hostsCache, statelessHosts) {
  261. return Promise.all(statelessHosts.map(statelessHost => {
  262. return hostsCache.get(statelessHost, () => {
  263. return Promise.resolve(createStatefulHost(statelessHost));
  264. });
  265. })).then(statefulHosts => {
  266. const hostsUp = statefulHosts.filter(host => isStatefulHostUp(host));
  267. const hostsTimeouted = statefulHosts.filter(host => isStatefulHostTimeouted(host));
  268. /**
  269. * Note, we put the hosts that previously timeouted on the end of the list.
  270. */
  271. const hostsAvailable = [...hostsUp, ...hostsTimeouted];
  272. const statelessHostsAvailable = hostsAvailable.length > 0
  273. ? hostsAvailable.map(host => createStatelessHost(host))
  274. : statelessHosts;
  275. return {
  276. getTimeout(timeoutsCount, baseTimeout) {
  277. /**
  278. * Imagine that you have 4 hosts, if timeouts will increase
  279. * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200)
  280. *
  281. * Note that, the very next request, we start from the previous timeout
  282. *
  283. * 5 (timeouted) > 6 (timeouted) > 7 ...
  284. *
  285. * This strategy may need to be reviewed, but is the strategy on the our
  286. * current v3 version.
  287. */
  288. const timeoutMultiplier = hostsTimeouted.length === 0 && timeoutsCount === 0
  289. ? 1
  290. : hostsTimeouted.length + 3 + timeoutsCount;
  291. return timeoutMultiplier * baseTimeout;
  292. },
  293. statelessHosts: statelessHostsAvailable,
  294. };
  295. });
  296. }
  297. const isNetworkError = ({ isTimedOut, status }) => {
  298. return !isTimedOut && ~~status === 0;
  299. };
  300. const isRetryable = (response) => {
  301. const status = response.status;
  302. const isTimedOut = response.isTimedOut;
  303. return (isTimedOut || isNetworkError(response) || (~~(status / 100) !== 2 && ~~(status / 100) !== 4));
  304. };
  305. const isSuccess = ({ status }) => {
  306. return ~~(status / 100) === 2;
  307. };
  308. const retryDecision = (response, outcomes) => {
  309. if (isRetryable(response)) {
  310. return outcomes.onRetry(response);
  311. }
  312. if (isSuccess(response)) {
  313. return outcomes.onSuccess(response);
  314. }
  315. return outcomes.onFail(response);
  316. };
  317. function retryableRequest(transporter, statelessHosts, request, requestOptions) {
  318. const stackTrace = []; // eslint-disable-line functional/prefer-readonly-type
  319. /**
  320. * First we prepare the payload that do not depend from hosts.
  321. */
  322. const data = serializeData(request, requestOptions);
  323. const headers = serializeHeaders(transporter, requestOptions);
  324. const method = request.method;
  325. // On `GET`, the data is proxied to query parameters.
  326. const dataQueryParameters = request.method !== MethodEnum.Get
  327. ? {}
  328. : {
  329. ...request.data,
  330. ...requestOptions.data,
  331. };
  332. const queryParameters = {
  333. 'x-algolia-agent': transporter.userAgent.value,
  334. ...transporter.queryParameters,
  335. ...dataQueryParameters,
  336. ...requestOptions.queryParameters,
  337. };
  338. let timeoutsCount = 0; // eslint-disable-line functional/no-let
  339. const retry = (hosts, // eslint-disable-line functional/prefer-readonly-type
  340. getTimeout) => {
  341. /**
  342. * We iterate on each host, until there is no host left.
  343. */
  344. const host = hosts.pop(); // eslint-disable-line functional/immutable-data
  345. if (host === undefined) {
  346. throw createRetryError(stackTraceWithoutCredentials(stackTrace));
  347. }
  348. const payload = {
  349. data,
  350. headers,
  351. method,
  352. url: serializeUrl(host, request.path, queryParameters),
  353. connectTimeout: getTimeout(timeoutsCount, transporter.timeouts.connect),
  354. responseTimeout: getTimeout(timeoutsCount, requestOptions.timeout),
  355. };
  356. /**
  357. * The stackFrame is pushed to the stackTrace so we
  358. * can have information about onRetry and onFailure
  359. * decisions.
  360. */
  361. const pushToStackTrace = (response) => {
  362. const stackFrame = {
  363. request: payload,
  364. response,
  365. host,
  366. triesLeft: hosts.length,
  367. };
  368. // eslint-disable-next-line functional/immutable-data
  369. stackTrace.push(stackFrame);
  370. return stackFrame;
  371. };
  372. const decisions = {
  373. onSuccess: response => deserializeSuccess(response),
  374. onRetry(response) {
  375. const stackFrame = pushToStackTrace(response);
  376. /**
  377. * If response is a timeout, we increaset the number of
  378. * timeouts so we can increase the timeout later.
  379. */
  380. if (response.isTimedOut) {
  381. timeoutsCount++;
  382. }
  383. return Promise.all([
  384. /**
  385. * Failures are individually send the logger, allowing
  386. * the end user to debug / store stack frames even
  387. * when a retry error does not happen.
  388. */
  389. transporter.logger.info('Retryable failure', stackFrameWithoutCredentials(stackFrame)),
  390. /**
  391. * We also store the state of the host in failure cases. If the host, is
  392. * down it will remain down for the next 2 minutes. In a timeout situation,
  393. * this host will be added end of the list of hosts on the next request.
  394. */
  395. transporter.hostsCache.set(host, createStatefulHost(host, response.isTimedOut ? HostStatusEnum.Timeouted : HostStatusEnum.Down)),
  396. ]).then(() => retry(hosts, getTimeout));
  397. },
  398. onFail(response) {
  399. pushToStackTrace(response);
  400. throw deserializeFailure(response, stackTraceWithoutCredentials(stackTrace));
  401. },
  402. };
  403. return transporter.requester.send(payload).then(response => {
  404. return retryDecision(response, decisions);
  405. });
  406. };
  407. /**
  408. * Finally, for each retryable host perform request until we got a non
  409. * retryable response. Some notes here:
  410. *
  411. * 1. The reverse here is applied so we can apply a `pop` later on => more performant.
  412. * 2. We also get from the retryable options a timeout multiplier that is tailored
  413. * for the current context.
  414. */
  415. return createRetryableOptions(transporter.hostsCache, statelessHosts).then(options => {
  416. return retry([...options.statelessHosts].reverse(), options.getTimeout);
  417. });
  418. }
  419. function createTransporter(options) {
  420. const { hostsCache, logger, requester, requestsCache, responsesCache, timeouts, userAgent, hosts, queryParameters, headers, } = options;
  421. const transporter = {
  422. hostsCache,
  423. logger,
  424. requester,
  425. requestsCache,
  426. responsesCache,
  427. timeouts,
  428. userAgent,
  429. headers,
  430. queryParameters,
  431. hosts: hosts.map(host => createStatelessHost(host)),
  432. read(request, requestOptions) {
  433. /**
  434. * First, we compute the user request options. Now, keep in mind,
  435. * that using request options the user is able to modified the intire
  436. * payload of the request. Such as headers, query parameters, and others.
  437. */
  438. const mappedRequestOptions = createMappedRequestOptions(requestOptions, transporter.timeouts.read);
  439. const createRetryableRequest = () => {
  440. /**
  441. * Then, we prepare a function factory that contains the construction of
  442. * the retryable request. At this point, we may *not* perform the actual
  443. * request. But we want to have the function factory ready.
  444. */
  445. return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Read) !== 0), request, mappedRequestOptions);
  446. };
  447. /**
  448. * Once we have the function factory ready, we need to determine of the
  449. * request is "cacheable" - should be cached. Note that, once again,
  450. * the user can force this option.
  451. */
  452. const cacheable = mappedRequestOptions.cacheable !== undefined
  453. ? mappedRequestOptions.cacheable
  454. : request.cacheable;
  455. /**
  456. * If is not "cacheable", we immediatly trigger the retryable request, no
  457. * need to check cache implementations.
  458. */
  459. if (cacheable !== true) {
  460. return createRetryableRequest();
  461. }
  462. /**
  463. * If the request is "cacheable", we need to first compute the key to ask
  464. * the cache implementations if this request is on progress or if the
  465. * response already exists on the cache.
  466. */
  467. const key = {
  468. request,
  469. mappedRequestOptions,
  470. transporter: {
  471. queryParameters: transporter.queryParameters,
  472. headers: transporter.headers,
  473. },
  474. };
  475. /**
  476. * With the computed key, we first ask the responses cache
  477. * implemention if this request was been resolved before.
  478. */
  479. return transporter.responsesCache.get(key, () => {
  480. /**
  481. * If the request has never resolved before, we actually ask if there
  482. * is a current request with the same key on progress.
  483. */
  484. return transporter.requestsCache.get(key, () => {
  485. return (transporter.requestsCache
  486. /**
  487. * Finally, if there is no request in progress with the same key,
  488. * this `createRetryableRequest()` will actually trigger the
  489. * retryable request.
  490. */
  491. .set(key, createRetryableRequest())
  492. .then(response => Promise.all([transporter.requestsCache.delete(key), response]), err => Promise.all([transporter.requestsCache.delete(key), Promise.reject(err)]))
  493. .then(([_, response]) => response));
  494. });
  495. }, {
  496. /**
  497. * Of course, once we get this response back from the server, we
  498. * tell response cache to actually store the received response
  499. * to be used later.
  500. */
  501. miss: response => transporter.responsesCache.set(key, response),
  502. });
  503. },
  504. write(request, requestOptions) {
  505. /**
  506. * On write requests, no cache mechanisms are applied, and we
  507. * proxy the request immediately to the requester.
  508. */
  509. return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Write) !== 0), request, createMappedRequestOptions(requestOptions, transporter.timeouts.write));
  510. },
  511. };
  512. return transporter;
  513. }
  514. function createUserAgent(version) {
  515. const userAgent = {
  516. value: `Algolia for JavaScript (${version})`,
  517. add(options) {
  518. const addedUserAgent = `; ${options.segment}${options.version !== undefined ? ` (${options.version})` : ''}`;
  519. if (userAgent.value.indexOf(addedUserAgent) === -1) {
  520. // eslint-disable-next-line functional/immutable-data
  521. userAgent.value = `${userAgent.value}${addedUserAgent}`;
  522. }
  523. return userAgent;
  524. },
  525. };
  526. return userAgent;
  527. }
  528. function deserializeSuccess(response) {
  529. // eslint-disable-next-line functional/no-try-statement
  530. try {
  531. return JSON.parse(response.content);
  532. }
  533. catch (e) {
  534. throw createDeserializationError(e.message, response);
  535. }
  536. }
  537. function deserializeFailure({ content, status }, stackFrame) {
  538. // eslint-disable-next-line functional/no-let
  539. let message = content;
  540. // eslint-disable-next-line functional/no-try-statement
  541. try {
  542. message = JSON.parse(content).message;
  543. }
  544. catch (e) {
  545. // ..
  546. }
  547. return createApiError(message, status, stackFrame);
  548. }
  549. function serializeUrl(host, path, queryParameters) {
  550. const queryParametersAsString = serializeQueryParameters(queryParameters);
  551. // eslint-disable-next-line functional/no-let
  552. let url = `${host.protocol}://${host.url}/${path.charAt(0) === '/' ? path.substr(1) : path}`;
  553. if (queryParametersAsString.length) {
  554. url += `?${queryParametersAsString}`;
  555. }
  556. return url;
  557. }
  558. function serializeQueryParameters(parameters) {
  559. const isObjectOrArray = (value) => Object.prototype.toString.call(value) === '[object Object]' ||
  560. Object.prototype.toString.call(value) === '[object Array]';
  561. return Object.keys(parameters)
  562. .map(key => encode('%s=%s', key, isObjectOrArray(parameters[key]) ? JSON.stringify(parameters[key]) : parameters[key]))
  563. .join('&');
  564. }
  565. function serializeData(request, requestOptions) {
  566. if (request.method === MethodEnum.Get ||
  567. (request.data === undefined && requestOptions.data === undefined)) {
  568. return undefined;
  569. }
  570. const data = Array.isArray(request.data)
  571. ? request.data
  572. : { ...request.data, ...requestOptions.data };
  573. return JSON.stringify(data);
  574. }
  575. function serializeHeaders(transporter, requestOptions) {
  576. const headers = {
  577. ...transporter.headers,
  578. ...requestOptions.headers,
  579. };
  580. const serializedHeaders = {};
  581. Object.keys(headers).forEach(header => {
  582. const value = headers[header];
  583. // @ts-ignore
  584. // eslint-disable-next-line functional/immutable-data
  585. serializedHeaders[header.toLowerCase()] = value;
  586. });
  587. return serializedHeaders;
  588. }
  589. function stackTraceWithoutCredentials(stackTrace) {
  590. return stackTrace.map(stackFrame => stackFrameWithoutCredentials(stackFrame));
  591. }
  592. function stackFrameWithoutCredentials(stackFrame) {
  593. const modifiedHeaders = stackFrame.request.headers['x-algolia-api-key']
  594. ? { 'x-algolia-api-key': '*****' }
  595. : {};
  596. return {
  597. ...stackFrame,
  598. request: {
  599. ...stackFrame.request,
  600. headers: {
  601. ...stackFrame.request.headers,
  602. ...modifiedHeaders,
  603. },
  604. },
  605. };
  606. }
  607. function createApiError(message, status, transporterStackTrace) {
  608. return {
  609. name: 'ApiError',
  610. message,
  611. status,
  612. transporterStackTrace,
  613. };
  614. }
  615. function createDeserializationError(message, response) {
  616. return {
  617. name: 'DeserializationError',
  618. message,
  619. response,
  620. };
  621. }
  622. function createRetryError(transporterStackTrace) {
  623. return {
  624. name: 'RetryError',
  625. message: 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.',
  626. transporterStackTrace,
  627. };
  628. }
  629. const createSearchClient = options => {
  630. const appId = options.appId;
  631. const auth = createAuth(options.authMode !== undefined ? options.authMode : AuthMode.WithinHeaders, appId, options.apiKey);
  632. const transporter = createTransporter({
  633. hosts: [
  634. { url: `${appId}-dsn.algolia.net`, accept: CallEnum.Read },
  635. { url: `${appId}.algolia.net`, accept: CallEnum.Write },
  636. ].concat(shuffle([
  637. { url: `${appId}-1.algolianet.com` },
  638. { url: `${appId}-2.algolianet.com` },
  639. { url: `${appId}-3.algolianet.com` },
  640. ])),
  641. ...options,
  642. headers: {
  643. ...auth.headers(),
  644. ...{ 'content-type': 'application/x-www-form-urlencoded' },
  645. ...options.headers,
  646. },
  647. queryParameters: {
  648. ...auth.queryParameters(),
  649. ...options.queryParameters,
  650. },
  651. });
  652. const base = {
  653. transporter,
  654. appId,
  655. addAlgoliaAgent(segment, version) {
  656. transporter.userAgent.add({ segment, version });
  657. },
  658. clearCache() {
  659. return Promise.all([
  660. transporter.requestsCache.clear(),
  661. transporter.responsesCache.clear(),
  662. ]).then(() => undefined);
  663. },
  664. };
  665. return addMethods(base, options.methods);
  666. };
  667. const customRequest = (base) => {
  668. return (request, requestOptions) => {
  669. if (request.method === MethodEnum.Get) {
  670. return base.transporter.read(request, requestOptions);
  671. }
  672. return base.transporter.write(request, requestOptions);
  673. };
  674. };
  675. const initIndex = (base) => {
  676. return (indexName, options = {}) => {
  677. const searchIndex = {
  678. transporter: base.transporter,
  679. appId: base.appId,
  680. indexName,
  681. };
  682. return addMethods(searchIndex, options.methods);
  683. };
  684. };
  685. const multipleQueries = (base) => {
  686. return (queries, requestOptions) => {
  687. const requests = queries.map(query => {
  688. return {
  689. ...query,
  690. params: serializeQueryParameters(query.params || {}),
  691. };
  692. });
  693. return base.transporter.read({
  694. method: MethodEnum.Post,
  695. path: '1/indexes/*/queries',
  696. data: {
  697. requests,
  698. },
  699. cacheable: true,
  700. }, requestOptions);
  701. };
  702. };
  703. const multipleSearchForFacetValues = (base) => {
  704. return (queries, requestOptions) => {
  705. return Promise.all(queries.map(query => {
  706. const { facetName, facetQuery, ...params } = query.params;
  707. return initIndex(base)(query.indexName, {
  708. methods: { searchForFacetValues },
  709. }).searchForFacetValues(facetName, facetQuery, {
  710. ...requestOptions,
  711. ...params,
  712. });
  713. }));
  714. };
  715. };
  716. const findAnswers = (base) => {
  717. return (query, queryLanguages, requestOptions) => {
  718. return base.transporter.read({
  719. method: MethodEnum.Post,
  720. path: encode('1/answers/%s/prediction', base.indexName),
  721. data: {
  722. query,
  723. queryLanguages,
  724. },
  725. cacheable: true,
  726. }, requestOptions);
  727. };
  728. };
  729. const search = (base) => {
  730. return (query, requestOptions) => {
  731. return base.transporter.read({
  732. method: MethodEnum.Post,
  733. path: encode('1/indexes/%s/query', base.indexName),
  734. data: {
  735. query,
  736. },
  737. cacheable: true,
  738. }, requestOptions);
  739. };
  740. };
  741. const searchForFacetValues = (base) => {
  742. return (facetName, facetQuery, requestOptions) => {
  743. return base.transporter.read({
  744. method: MethodEnum.Post,
  745. path: encode('1/indexes/%s/facets/%s/query', base.indexName, facetName),
  746. data: {
  747. facetQuery,
  748. },
  749. cacheable: true,
  750. }, requestOptions);
  751. };
  752. };
  753. const LogLevelEnum = {
  754. Debug: 1,
  755. Info: 2,
  756. Error: 3,
  757. };
  758. /* eslint no-console: 0 */
  759. function createConsoleLogger(logLevel) {
  760. return {
  761. debug(message, args) {
  762. if (LogLevelEnum.Debug >= logLevel) {
  763. console.debug(message, args);
  764. }
  765. return Promise.resolve();
  766. },
  767. info(message, args) {
  768. if (LogLevelEnum.Info >= logLevel) {
  769. console.info(message, args);
  770. }
  771. return Promise.resolve();
  772. },
  773. error(message, args) {
  774. console.error(message, args);
  775. return Promise.resolve();
  776. },
  777. };
  778. }
  779. function createBrowserXhrRequester() {
  780. return {
  781. send(request) {
  782. return new Promise((resolve) => {
  783. const baseRequester = new XMLHttpRequest();
  784. baseRequester.open(request.method, request.url, true);
  785. Object.keys(request.headers).forEach(key => baseRequester.setRequestHeader(key, request.headers[key]));
  786. const createTimeout = (timeout, content) => {
  787. return setTimeout(() => {
  788. baseRequester.abort();
  789. resolve({
  790. status: 0,
  791. content,
  792. isTimedOut: true,
  793. });
  794. }, timeout * 1000);
  795. };
  796. const connectTimeout = createTimeout(request.connectTimeout, 'Connection timeout');
  797. // eslint-disable-next-line functional/no-let
  798. let responseTimeout;
  799. // eslint-disable-next-line functional/immutable-data
  800. baseRequester.onreadystatechange = () => {
  801. if (baseRequester.readyState > baseRequester.OPENED && responseTimeout === undefined) {
  802. clearTimeout(connectTimeout);
  803. responseTimeout = createTimeout(request.responseTimeout, 'Socket timeout');
  804. }
  805. };
  806. // eslint-disable-next-line functional/immutable-data
  807. baseRequester.onerror = () => {
  808. // istanbul ignore next
  809. if (baseRequester.status === 0) {
  810. clearTimeout(connectTimeout);
  811. clearTimeout(responseTimeout);
  812. resolve({
  813. content: baseRequester.responseText || 'Network request failed',
  814. status: baseRequester.status,
  815. isTimedOut: false,
  816. });
  817. }
  818. };
  819. // eslint-disable-next-line functional/immutable-data
  820. baseRequester.onload = () => {
  821. clearTimeout(connectTimeout);
  822. clearTimeout(responseTimeout);
  823. resolve({
  824. content: baseRequester.responseText,
  825. status: baseRequester.status,
  826. isTimedOut: false,
  827. });
  828. };
  829. baseRequester.send(request.data);
  830. });
  831. },
  832. };
  833. }
  834. function algoliasearch(appId, apiKey, options) {
  835. const commonOptions = {
  836. appId,
  837. apiKey,
  838. timeouts: {
  839. connect: 1,
  840. read: 2,
  841. write: 30,
  842. },
  843. requester: createBrowserXhrRequester(),
  844. logger: createConsoleLogger(LogLevelEnum.Error),
  845. responsesCache: createInMemoryCache(),
  846. requestsCache: createInMemoryCache({ serializable: false }),
  847. hostsCache: createFallbackableCache({
  848. caches: [
  849. createBrowserLocalStorageCache({ key: `${version}-${appId}` }),
  850. createInMemoryCache(),
  851. ],
  852. }),
  853. userAgent: createUserAgent(version).add({
  854. segment: 'Browser',
  855. version: 'lite',
  856. }),
  857. authMode: AuthMode.WithinQueryParameters,
  858. };
  859. return createSearchClient({
  860. ...commonOptions,
  861. ...options,
  862. methods: {
  863. search: multipleQueries,
  864. searchForFacetValues: multipleSearchForFacetValues,
  865. multipleQueries,
  866. multipleSearchForFacetValues,
  867. customRequest,
  868. initIndex: base => (indexName) => {
  869. return initIndex(base)(indexName, {
  870. methods: { search, searchForFacetValues, findAnswers },
  871. });
  872. },
  873. },
  874. });
  875. }
  876. // eslint-disable-next-line functional/immutable-data
  877. algoliasearch.version = version;
  878. export default algoliasearch;