1 | function createBrowserLocalStorageCache(options) {
|
2 | const namespaceKey = `algoliasearch-client-js-${options.key}`;
|
3 |
|
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 |
|
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 |
|
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 |
|
80 | function createFallbackableCache(options) {
|
81 | const caches = [...options.caches];
|
82 | const current = caches.shift();
|
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 |
|
112 | function 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 |
|
134 | function createInMemoryCache(options = { serializable: true }) {
|
135 |
|
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 |
|
151 | cache[JSON.stringify(key)] = options.serializable ? JSON.stringify(value) : value;
|
152 | return Promise.resolve(value);
|
153 | },
|
154 | delete(key) {
|
155 |
|
156 | delete cache[JSON.stringify(key)];
|
157 | return Promise.resolve();
|
158 | },
|
159 | clear() {
|
160 | cache = {};
|
161 | return Promise.resolve();
|
162 | },
|
163 | };
|
164 | }
|
165 |
|
166 | function 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 |
|
182 | function shuffle(array) {
|
183 | let c = array.length - 1;
|
184 |
|
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];
|
189 | array[b] = a;
|
190 | }
|
191 | return array;
|
192 | }
|
193 | function addMethods(base, methods) {
|
194 | if (!methods) {
|
195 | return base;
|
196 | }
|
197 | Object.keys(methods).forEach(key => {
|
198 |
|
199 | base[key] = methods[key](base);
|
200 | });
|
201 | return base;
|
202 | }
|
203 | function encode(format, ...args) {
|
204 |
|
205 | let i = 0;
|
206 | return format.replace(/%s/g, () => encodeURIComponent(args[i++]));
|
207 | }
|
208 |
|
209 | const version = '4.23.3';
|
210 |
|
211 | const AuthMode = {
|
212 | |
213 |
|
214 |
|
215 | WithinQueryParameters: 0,
|
216 | |
217 |
|
218 |
|
219 | WithinHeaders: 1,
|
220 | };
|
221 |
|
222 | function 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];
|
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 |
|
239 | const CallEnum = {
|
240 | |
241 |
|
242 |
|
243 | Read: 1,
|
244 | |
245 |
|
246 |
|
247 | Write: 2,
|
248 | |
249 |
|
250 |
|
251 | Any: 3,
|
252 | };
|
253 |
|
254 | const HostStatusEnum = {
|
255 | Up: 1,
|
256 | Down: 2,
|
257 | Timeouted: 3,
|
258 | };
|
259 |
|
260 |
|
261 |
|
262 | const EXPIRATION_DELAY = 2 * 60 * 1000;
|
263 | function createStatefulHost(host, status = HostStatusEnum.Up) {
|
264 | return {
|
265 | ...host,
|
266 | status,
|
267 | lastUpdate: Date.now(),
|
268 | };
|
269 | }
|
270 | function isStatefulHostUp(host) {
|
271 | return host.status === HostStatusEnum.Up || Date.now() - host.lastUpdate > EXPIRATION_DELAY;
|
272 | }
|
273 | function isStatefulHostTimeouted(host) {
|
274 | return (host.status === HostStatusEnum.Timeouted && Date.now() - host.lastUpdate <= EXPIRATION_DELAY);
|
275 | }
|
276 |
|
277 | function 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 |
|
292 | const MethodEnum = {
|
293 | Delete: 'DELETE',
|
294 | Get: 'GET',
|
295 | Post: 'POST',
|
296 | Put: 'PUT',
|
297 | };
|
298 |
|
299 | function 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 |
|
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 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
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 |
|
337 | const isNetworkError = ({ isTimedOut, status }) => {
|
338 | return !isTimedOut && ~~status === 0;
|
339 | };
|
340 | const isRetryable = (response) => {
|
341 | const status = response.status;
|
342 | const isTimedOut = response.isTimedOut;
|
343 | return (isTimedOut || isNetworkError(response) || (~~(status / 100) !== 2 && ~~(status / 100) !== 4));
|
344 | };
|
345 | const isSuccess = ({ status }) => {
|
346 | return ~~(status / 100) === 2;
|
347 | };
|
348 | const 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 |
|
358 | function retryableRequest(transporter, statelessHosts, request, requestOptions) {
|
359 | const stackTrace = [];
|
360 | |
361 |
|
362 |
|
363 | const data = serializeData(request, requestOptions);
|
364 | const headers = serializeHeaders(transporter, requestOptions);
|
365 | const method = request.method;
|
366 |
|
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;
|
380 | const retry = (hosts,
|
381 | getTimeout) => {
|
382 | |
383 |
|
384 |
|
385 | const host = hosts.pop();
|
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 |
|
399 |
|
400 |
|
401 |
|
402 | const pushToStackTrace = (response) => {
|
403 | const stackFrame = {
|
404 | request: payload,
|
405 | response,
|
406 | host,
|
407 | triesLeft: hosts.length,
|
408 | };
|
409 |
|
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 |
|
419 |
|
420 |
|
421 | if (response.isTimedOut) {
|
422 | timeoutsCount++;
|
423 | }
|
424 | return Promise.all([
|
425 | |
426 |
|
427 |
|
428 |
|
429 |
|
430 | transporter.logger.info('Retryable failure', stackFrameWithoutCredentials(stackFrame)),
|
431 | |
432 |
|
433 |
|
434 |
|
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 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 | return createRetryableOptions(transporter.hostsCache, statelessHosts).then(options => {
|
457 | return retry([...options.statelessHosts].reverse(), options.getTimeout);
|
458 | });
|
459 | }
|
460 |
|
461 | function 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 |
|
477 |
|
478 |
|
479 |
|
480 | const mappedRequestOptions = createMappedRequestOptions(requestOptions, transporter.timeouts.read);
|
481 | const createRetryableRequest = () => {
|
482 | |
483 |
|
484 |
|
485 |
|
486 |
|
487 | return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Read) !== 0), request, mappedRequestOptions);
|
488 | };
|
489 | |
490 |
|
491 |
|
492 |
|
493 |
|
494 | const cacheable = mappedRequestOptions.cacheable !== undefined
|
495 | ? mappedRequestOptions.cacheable
|
496 | : request.cacheable;
|
497 | |
498 |
|
499 |
|
500 |
|
501 | if (cacheable !== true) {
|
502 | return createRetryableRequest();
|
503 | }
|
504 | |
505 |
|
506 |
|
507 |
|
508 |
|
509 | const key = {
|
510 | request,
|
511 | mappedRequestOptions,
|
512 | transporter: {
|
513 | queryParameters: transporter.queryParameters,
|
514 | headers: transporter.headers,
|
515 | },
|
516 | };
|
517 | |
518 |
|
519 |
|
520 |
|
521 | return transporter.responsesCache.get(key, () => {
|
522 | |
523 |
|
524 |
|
525 |
|
526 | return transporter.requestsCache.get(key, () => {
|
527 | return (transporter.requestsCache
|
528 | |
529 |
|
530 |
|
531 |
|
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 |
|
540 |
|
541 |
|
542 |
|
543 | miss: response => transporter.responsesCache.set(key, response),
|
544 | });
|
545 | },
|
546 | write(request, requestOptions) {
|
547 | |
548 |
|
549 |
|
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 |
|
557 | function 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 |
|
564 | userAgent.value = `${userAgent.value}${addedUserAgent}`;
|
565 | }
|
566 | return userAgent;
|
567 | },
|
568 | };
|
569 | return userAgent;
|
570 | }
|
571 |
|
572 | function deserializeSuccess(response) {
|
573 |
|
574 | try {
|
575 | return JSON.parse(response.content);
|
576 | }
|
577 | catch (e) {
|
578 | throw createDeserializationError(e.message, response);
|
579 | }
|
580 | }
|
581 | function deserializeFailure({ content, status }, stackFrame) {
|
582 |
|
583 | let message = content;
|
584 |
|
585 | try {
|
586 | message = JSON.parse(content).message;
|
587 | }
|
588 | catch (e) {
|
589 |
|
590 | }
|
591 | return createApiError(message, status, stackFrame);
|
592 | }
|
593 |
|
594 | function serializeUrl(host, path, queryParameters) {
|
595 | const queryParametersAsString = serializeQueryParameters(queryParameters);
|
596 |
|
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 | }
|
603 | function 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 | }
|
610 | function 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 | }
|
620 | function 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 |
|
629 |
|
630 | serializedHeaders[header.toLowerCase()] = value;
|
631 | });
|
632 | return serializedHeaders;
|
633 | }
|
634 |
|
635 | function stackTraceWithoutCredentials(stackTrace) {
|
636 | return stackTrace.map(stackFrame => stackFrameWithoutCredentials(stackFrame));
|
637 | }
|
638 | function 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 |
|
654 | function createApiError(message, status, transporterStackTrace) {
|
655 | return {
|
656 | name: 'ApiError',
|
657 | message,
|
658 | status,
|
659 | transporterStackTrace,
|
660 | };
|
661 | }
|
662 |
|
663 | function createDeserializationError(message, response) {
|
664 | return {
|
665 | name: 'DeserializationError',
|
666 | message,
|
667 | response,
|
668 | };
|
669 | }
|
670 |
|
671 | function 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 |
|
679 | const 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 |
|
718 | const 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 |
|
727 | const 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 |
|
738 | const 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 |
|
757 | const 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 |
|
771 | const 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 |
|
785 | const 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 |
|
798 | const 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 |
|
811 | const LogLevelEnum = {
|
812 | Debug: 1,
|
813 | Info: 2,
|
814 | Error: 3,
|
815 | };
|
816 |
|
817 |
|
818 | function 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 |
|
839 | const getRecommendations = base => {
|
840 | return (queries, requestOptions) => {
|
841 | const requests = queries.map(query => ({
|
842 | ...query,
|
843 |
|
844 |
|
845 |
|
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 |
|
859 | function 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 |
|
878 | let responseTimeout;
|
879 |
|
880 | baseRequester.onreadystatechange = () => {
|
881 | if (baseRequester.readyState > baseRequester.OPENED && responseTimeout === undefined) {
|
882 | clearTimeout(connectTimeout);
|
883 | responseTimeout = createTimeout(request.responseTimeout, 'Socket timeout');
|
884 | }
|
885 | };
|
886 |
|
887 | baseRequester.onerror = () => {
|
888 |
|
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 |
|
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 |
|
915 | function 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 |
|
959 | algoliasearch.version = version;
|
960 |
|
961 | export default algoliasearch;
|