UNPKG

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