@blotoutio/providers-evo-search-sdk 1.64.2 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -819,6 +819,95 @@ const resetRegistry = () => {
819
819
  notifySubscribers(registry.subscribers, 'resetRegistry');
820
820
  };
821
821
 
822
+ // Resilience wrapper for /search requests. A transient network failure or a
823
+ // hung request must not wedge the UI: add a per-attempt timeout (a hang becomes
824
+ // a rejection), retry transient failures with backoff, and never retry a
825
+ // caller-superseded (aborted) request. Shared by the widget and the headless
826
+ // hook. See callers: search-widget.ts (performAutocomplete) and
827
+ // hooks/use-evo-search.ts.
828
+ const DEFAULTS = {
829
+ timeoutMs: 7000,
830
+ retries: 2,
831
+ backoffMs: [400, 1200],
832
+ };
833
+ // Deterministic HTTP errors (4xx other than 429) will never succeed on
834
+ // retry — only retry timeouts, network errors, 429, and 5xx. Errors without
835
+ // an attached `status` (network/timeout failures) are always retryable.
836
+ const isRetryable = (error) => {
837
+ const status = error === null || error === void 0 ? void 0 : error.status;
838
+ return status == null || status === 429 || status >= 500;
839
+ };
840
+ const delay = (ms, signal) => new Promise((resolve, reject) => {
841
+ if (signal.aborted)
842
+ return reject(new DOMException('Aborted', 'AbortError'));
843
+ const t = setTimeout(resolve, ms);
844
+ signal.addEventListener('abort', () => {
845
+ clearTimeout(t);
846
+ reject(new DOMException('Aborted', 'AbortError'));
847
+ }, { once: true });
848
+ });
849
+ /**
850
+ * Run `attempt` with a timeout, retrying transient failures. `callerSignal`
851
+ * supersedes everything: if it aborts, we stop immediately without retrying
852
+ * (the query changed / the widget cleaned up). A timeout or network error is
853
+ * retried up to `retries` times; anything left over is rethrown.
854
+ */
855
+ async function withSearchResilience(attempt, callerSignal, options = {}) {
856
+ var _a, _b;
857
+ const { timeoutMs, retries, backoffMs } = { ...DEFAULTS, ...options };
858
+ let lastError;
859
+ for (let i = 0; i <= retries; i++) {
860
+ if (callerSignal.aborted)
861
+ throw new DOMException('Aborted', 'AbortError');
862
+ const local = new AbortController();
863
+ const onCallerAbort = () => local.abort();
864
+ callerSignal.addEventListener('abort', onCallerAbort, { once: true });
865
+ // Race the attempt against an explicit timeout that REJECTS. Aborting the
866
+ // signal alone isn't enough — an attempt that ignores its signal would
867
+ // otherwise hang forever and never retry. Reject with a TimeoutError (not
868
+ // an AbortError) so callers surface it instead of treating it as a
869
+ // caller-cancellation.
870
+ let timer;
871
+ const timeout = new Promise((_, reject) => {
872
+ timer = setTimeout(() => {
873
+ local.abort(); // still cancel the in-flight request to free the socket
874
+ reject(new DOMException('Search request timed out', 'TimeoutError'));
875
+ }, timeoutMs);
876
+ });
877
+ try {
878
+ return await Promise.race([attempt(local.signal), timeout]);
879
+ }
880
+ catch (error) {
881
+ lastError = error;
882
+ // The caller superseded this request — do not retry, propagate the abort.
883
+ if (callerSignal.aborted)
884
+ throw error;
885
+ // Otherwise it was a timeout or a transient network error: retry if budget remains.
886
+ if (i < retries && isRetryable(error)) {
887
+ const wait = (_b = (_a = backoffMs[i]) !== null && _a !== void 0 ? _a : backoffMs[backoffMs.length - 1]) !== null && _b !== void 0 ? _b : 0;
888
+ // Rejects with an AbortError if the caller aborts mid-backoff —
889
+ // propagate that cancellation instead of the transient `error` above.
890
+ await delay(wait, callerSignal);
891
+ continue;
892
+ }
893
+ throw error;
894
+ }
895
+ finally {
896
+ clearTimeout(timer);
897
+ callerSignal.removeEventListener('abort', onCallerAbort);
898
+ }
899
+ }
900
+ throw lastError;
901
+ }
902
+
903
+ // Client-side deadlines for the Ask SSE path. `/search` already gets these via
904
+ // withSearchResilience; Ask is the streaming counterpart. The request timeout
905
+ // matches the resilience wrapper's per-attempt budget; the idle timeout catches
906
+ // a stream that stalls mid-flight without a terminal frame and resets on every
907
+ // chunk. Both surface a TimeoutError (not an AbortError) so submitAsk routes
908
+ // them to the search fallback instead of treating them as caller-cancellation.
909
+ const ASK_REQUEST_TIMEOUT_MS = 7000;
910
+ const ASK_SSE_IDLE_TIMEOUT_MS = 10000;
822
911
  // Always defer to what the storefront itself is rendering — same source
823
912
  // shop-gpt reads from. Two widgets on the same page would otherwise drift:
824
913
  // shop-gpt picks Shopify, evo-search would pick its own override and show
@@ -846,15 +935,28 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
846
935
  if (!sendTag) {
847
936
  throw new Error('sendTag missing');
848
937
  }
938
+ const getChannel = () => { var _a; return (((_a = window[registryKey]) === null || _a === void 0 ? void 0 : _a.ui) ? 'widget' : 'headless'); };
849
939
  const getHeaders = () => {
850
- var _a;
851
940
  const headers = new Headers({ EdgeTagUserId: userId });
852
941
  if (sessionId) {
853
942
  headers.set('EdgeTagSessionId', sessionId);
854
943
  }
855
- headers.set('X-EvoSearch-Channel', ((_a = window[registryKey]) === null || _a === void 0 ? void 0 : _a.ui) ? 'widget' : 'headless');
944
+ headers.set('X-EvoSearch-Channel', getChannel());
856
945
  return headers;
857
946
  };
947
+ // Identity for the public read endpoints travels as query params instead of
948
+ // headers so the GET stays a CORS-simple request — no OPTIONS preflight,
949
+ // one connection attempt per keystroke instead of two (a hung transport
950
+ // path then wedges half as often, and withSearchResilience's retry isn't
951
+ // preceded by a second preflight). /user/* keeps the header form: the
952
+ // worker's cookie-HMAC auth binds to it, and those routes preflight anyway.
953
+ const setIdentityParams = (url) => {
954
+ url.searchParams.set('uid', userId);
955
+ if (sessionId) {
956
+ url.searchParams.set('sid', sessionId);
957
+ }
958
+ url.searchParams.set('channel', getChannel());
959
+ };
858
960
  // Parsing `baseURL` once at API construction avoids re-parsing the host on
859
961
  // every keystroke when `getURL` runs inside the autocomplete hot path.
860
962
  const baseOrigin = new URL(baseURL).origin;
@@ -877,10 +979,14 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
877
979
  for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
878
980
  url.searchParams.set(key, value);
879
981
  }
982
+ const credentialed = needsCredentials(path);
983
+ if (!credentialed) {
984
+ setIdentityParams(url);
985
+ }
880
986
  const response = await fetchImpl(url, {
881
987
  method: 'GET',
882
- headers: getHeaders(),
883
- credentials: needsCredentials(path) ? 'include' : 'omit',
988
+ ...(credentialed ? { headers: getHeaders() } : {}),
989
+ credentials: credentialed ? 'include' : 'omit',
884
990
  signal,
885
991
  });
886
992
  if (!response.ok) {
@@ -900,17 +1006,38 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
900
1006
  logger.info('EvoSearch API: Fetching ask config');
901
1007
  return getJSON('/config', 'Config', undefined, signal);
902
1008
  };
903
- // Fire-and-forget isolate warmer: tells the worker to prime its search
904
- // index off the request path so the first keystroke doesn't pay the cold
905
- // prime. Errors are irrelevant — autocomplete just falls back to priming
906
- // inline like before.
1009
+ // Isolate warmer doubling as connection establisher: primes the worker's
1010
+ // search index off the request path AND opens the h2/h3 connection before
1011
+ // the first real keystroke needs it. Runs under the resilience wrapper so a
1012
+ // black-holed connection attempt (hung QUIC/IPv6 path) is aborted and
1013
+ // retried — the retry is what lets the browser fall back to a working
1014
+ // transport while the shopper is still reaching for the search box. Any
1015
+ // HTTP response counts as success: even an error status proves the
1016
+ // connection is established. Never rejects; concurrent triggers (open,
1017
+ // focus, first keystroke, keep-warm tick) coalesce into one attempt.
1018
+ let warmInFlight = null;
1019
+ let warmAbort = null;
907
1020
  const warm = () => {
908
- fetchImpl(getURL('/warm'), {
909
- method: 'GET',
910
- headers: getHeaders(),
911
- credentials: 'omit',
912
- }).catch(() => undefined);
1021
+ if (warmInFlight) {
1022
+ return warmInFlight;
1023
+ }
1024
+ const url = getURL('/warm');
1025
+ setIdentityParams(url);
1026
+ warmAbort = new AbortController();
1027
+ warmInFlight = withSearchResilience((signal) => fetchImpl(url, { method: 'GET', credentials: 'omit', signal }), warmAbort.signal)
1028
+ .then(() => undefined)
1029
+ .catch(() => undefined)
1030
+ .finally(() => {
1031
+ warmInFlight = null;
1032
+ warmAbort = null;
1033
+ });
1034
+ return warmInFlight;
913
1035
  };
1036
+ // Closing the widget must kill the chain: a black-holed attempt would
1037
+ // otherwise keep re-establishing connections for the wrapper's full budget
1038
+ // (~23s) after the overlay is gone — exactly the churn warm() exists to
1039
+ // avoid. The next warm() mints a fresh controller.
1040
+ const cancelWarm = () => warmAbort === null || warmAbort === void 0 ? void 0 : warmAbort.abort();
914
1041
  const recents = (signal) => {
915
1042
  logger.info('EvoSearch API: Fetching recent searches');
916
1043
  return getJSON('/user/recents', 'Recents', undefined, signal);
@@ -951,50 +1078,107 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
951
1078
  // reader, the for-await loop drops out, and the iterator returns.
952
1079
  async function* askStream(query, signal) {
953
1080
  const url = getURL('/ask');
954
- const response = await fetchImpl(url, {
955
- method: 'POST',
956
- headers: (() => {
957
- const h = getHeaders();
958
- h.set('Content-Type', 'application/json');
959
- return h;
960
- })(),
961
- body: JSON.stringify({ query }),
962
- credentials: 'omit',
963
- signal,
964
- });
965
- if (!response.ok || !response.body) {
966
- throw new Error(`Ask failed - ${response.status}`);
967
- }
968
- const reader = response.body
969
- .pipeThrough(new TextDecoderStream())
970
- .getReader();
971
- let buffer = '';
1081
+ // One controller drives teardown for the whole stream: the caller's signal,
1082
+ // the initial-request deadline, and the SSE idle timeout all abort through
1083
+ // it, so a stall closes the fetch instead of hanging. `timedOut` lets us
1084
+ // rethrow a TimeoutError rather than the AbortError the abort would raise.
1085
+ const controller = new AbortController();
1086
+ const onCallerAbort = () => controller.abort();
1087
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onCallerAbort, { once: true });
1088
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
1089
+ controller.abort();
1090
+ }
1091
+ let timedOut = false;
1092
+ const initialTimer = setTimeout(() => {
1093
+ timedOut = true;
1094
+ controller.abort();
1095
+ }, ASK_REQUEST_TIMEOUT_MS);
1096
+ // Outer try guarantees the caller-signal listener is removed on EVERY exit
1097
+ // path. It's `{ once: true }`, so a caller abort auto-removes it — but the
1098
+ // initial-timeout, network-error, and non-ok paths never fire the abort
1099
+ // event, so without this they'd leak a listener on a long-lived shared
1100
+ // signal (one per Ask query).
972
1101
  try {
973
- while (true) {
974
- const { done, value } = await reader.read();
975
- if (done) {
976
- break;
1102
+ let response;
1103
+ try {
1104
+ response = await fetchImpl(url, {
1105
+ method: 'POST',
1106
+ headers: (() => {
1107
+ const h = getHeaders();
1108
+ h.set('Content-Type', 'application/json');
1109
+ return h;
1110
+ })(),
1111
+ body: JSON.stringify({ query }),
1112
+ credentials: 'omit',
1113
+ signal: controller.signal,
1114
+ });
1115
+ }
1116
+ catch (error) {
1117
+ if (timedOut) {
1118
+ throw new DOMException('Ask request timed out', 'TimeoutError');
977
1119
  }
978
- buffer += value;
979
- let boundary = buffer.indexOf('\n\n');
980
- while (boundary !== -1) {
981
- const frame = buffer.slice(0, boundary);
982
- buffer = buffer.slice(boundary + 2);
983
- const event = parseSseFrame(frame);
984
- if (event) {
985
- yield event;
1120
+ throw error;
1121
+ }
1122
+ finally {
1123
+ // The idle timeout takes over once the response resolves.
1124
+ clearTimeout(initialTimer);
1125
+ }
1126
+ if (!response.ok || !response.body) {
1127
+ throw new Error(`Ask failed - ${response.status}`);
1128
+ }
1129
+ const reader = response.body
1130
+ .pipeThrough(new TextDecoderStream())
1131
+ .getReader();
1132
+ let buffer = '';
1133
+ try {
1134
+ while (true) {
1135
+ // Idle timeout, reset every chunk: a stream that stalls mid-flight
1136
+ // (no terminal frame) rejects here instead of freezing the skeleton.
1137
+ let idleTimer;
1138
+ const idle = new Promise((_, reject) => {
1139
+ idleTimer = setTimeout(() => {
1140
+ timedOut = true;
1141
+ controller.abort();
1142
+ reject(new DOMException('Ask stream stalled', 'TimeoutError'));
1143
+ }, ASK_SSE_IDLE_TIMEOUT_MS);
1144
+ });
1145
+ let result;
1146
+ try {
1147
+ result = await Promise.race([reader.read(), idle]);
986
1148
  }
987
- boundary = buffer.indexOf('\n\n');
1149
+ finally {
1150
+ clearTimeout(idleTimer);
1151
+ }
1152
+ const { done, value } = result;
1153
+ if (done) {
1154
+ break;
1155
+ }
1156
+ buffer += value;
1157
+ let boundary = buffer.indexOf('\n\n');
1158
+ while (boundary !== -1) {
1159
+ const frame = buffer.slice(0, boundary);
1160
+ buffer = buffer.slice(boundary + 2);
1161
+ const event = parseSseFrame(frame);
1162
+ if (event) {
1163
+ yield event;
1164
+ }
1165
+ boundary = buffer.indexOf('\n\n');
1166
+ }
1167
+ }
1168
+ }
1169
+ finally {
1170
+ try {
1171
+ // cancel (not releaseLock) — safe with an outstanding read and tears
1172
+ // down the upstream fetch on a caller-abort or idle timeout.
1173
+ await reader.cancel();
1174
+ }
1175
+ catch {
1176
+ // ignore — already closed
988
1177
  }
989
1178
  }
990
1179
  }
991
1180
  finally {
992
- try {
993
- reader.releaseLock();
994
- }
995
- catch {
996
- // ignore
997
- }
1181
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onCallerAbort);
998
1182
  }
999
1183
  }
1000
1184
  const sendEvent = (action, currency, actionData, clickData, beacon) => {
@@ -1046,6 +1230,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
1046
1230
  trending,
1047
1231
  config,
1048
1232
  warm,
1233
+ cancelWarm,
1049
1234
  user: {
1050
1235
  recents,
1051
1236
  deleteRecent,
@@ -8309,88 +8494,13 @@ const searchWidgetStyles = i$6 `
8309
8494
  }
8310
8495
  `;
8311
8496
 
8312
- // Resilience wrapper for /search requests. A transient network failure or a
8313
- // hung request must not wedge the UI: add a per-attempt timeout (a hang becomes
8314
- // a rejection), retry transient failures with backoff, and never retry a
8315
- // caller-superseded (aborted) request. Shared by the widget and the headless
8316
- // hook. See callers: search-widget.ts (performAutocomplete) and
8317
- // hooks/use-evo-search.ts.
8318
- const DEFAULTS = {
8319
- timeoutMs: 7000,
8320
- retries: 2,
8321
- backoffMs: [400, 1200],
8322
- };
8323
- // Deterministic HTTP errors (4xx other than 429) will never succeed on
8324
- // retry — only retry timeouts, network errors, 429, and 5xx. Errors without
8325
- // an attached `status` (network/timeout failures) are always retryable.
8326
- const isRetryable = (error) => {
8327
- const status = error === null || error === void 0 ? void 0 : error.status;
8328
- return status == null || status === 429 || status >= 500;
8329
- };
8330
- const delay = (ms, signal) => new Promise((resolve, reject) => {
8331
- if (signal.aborted)
8332
- return reject(new DOMException('Aborted', 'AbortError'));
8333
- const t = setTimeout(resolve, ms);
8334
- signal.addEventListener('abort', () => {
8335
- clearTimeout(t);
8336
- reject(new DOMException('Aborted', 'AbortError'));
8337
- }, { once: true });
8338
- });
8339
- /**
8340
- * Run `attempt` with a timeout, retrying transient failures. `callerSignal`
8341
- * supersedes everything: if it aborts, we stop immediately without retrying
8342
- * (the query changed / the widget cleaned up). A timeout or network error is
8343
- * retried up to `retries` times; anything left over is rethrown.
8344
- */
8345
- async function withSearchResilience(attempt, callerSignal, options = {}) {
8346
- var _a, _b;
8347
- const { timeoutMs, retries, backoffMs } = { ...DEFAULTS, ...options };
8348
- let lastError;
8349
- for (let i = 0; i <= retries; i++) {
8350
- if (callerSignal.aborted)
8351
- throw new DOMException('Aborted', 'AbortError');
8352
- const local = new AbortController();
8353
- const onCallerAbort = () => local.abort();
8354
- callerSignal.addEventListener('abort', onCallerAbort, { once: true });
8355
- // Race the attempt against an explicit timeout that REJECTS. Aborting the
8356
- // signal alone isn't enough — an attempt that ignores its signal would
8357
- // otherwise hang forever and never retry. Reject with a TimeoutError (not
8358
- // an AbortError) so callers surface it instead of treating it as a
8359
- // caller-cancellation.
8360
- let timer;
8361
- const timeout = new Promise((_, reject) => {
8362
- timer = setTimeout(() => {
8363
- local.abort(); // still cancel the in-flight request to free the socket
8364
- reject(new DOMException('Search request timed out', 'TimeoutError'));
8365
- }, timeoutMs);
8366
- });
8367
- try {
8368
- return await Promise.race([attempt(local.signal), timeout]);
8369
- }
8370
- catch (error) {
8371
- lastError = error;
8372
- // The caller superseded this request — do not retry, propagate the abort.
8373
- if (callerSignal.aborted)
8374
- throw error;
8375
- // Otherwise it was a timeout or a transient network error: retry if budget remains.
8376
- if (i < retries && isRetryable(error)) {
8377
- const wait = (_b = (_a = backoffMs[i]) !== null && _a !== void 0 ? _a : backoffMs[backoffMs.length - 1]) !== null && _b !== void 0 ? _b : 0;
8378
- // Rejects with an AbortError if the caller aborts mid-backoff —
8379
- // propagate that cancellation instead of the transient `error` above.
8380
- await delay(wait, callerSignal);
8381
- continue;
8382
- }
8383
- throw error;
8384
- }
8385
- finally {
8386
- clearTimeout(timer);
8387
- callerSignal.removeEventListener('abort', onCallerAbort);
8388
- }
8389
- }
8390
- throw lastError;
8391
- }
8392
-
8393
8497
  const MIN_QUERY_LENGTH = 3;
8498
+ // Keep-warm cadence while the widget is open. Chrome reaps used-idle sockets
8499
+ // after ~5 minutes and the edge sooner than that under load; a ping well
8500
+ // inside those windows keeps the established h2/h3 connection alive so a
8501
+ // keystroke after an idle pause multiplexes onto it instead of paying (and
8502
+ // possibly wedging on) fresh connection establishment.
8503
+ const KEEP_WARM_INTERVAL_MS = 60000;
8394
8504
  // @state fields whose change should be persisted to the session snapshot.
8395
8505
  const SESSION_PERSIST_KEYS = [
8396
8506
  'searchQuery',
@@ -8556,21 +8666,55 @@ class SearchWidget extends ElementWithStylesheet {
8556
8666
  this.isMobile = this.mobileMediaQuery.matches;
8557
8667
  this.mobileMediaQuery.addEventListener('change', this.mobileMediaListener);
8558
8668
  window.addEventListener('online', this.onlineListener);
8669
+ // Restart keep-warm when the same instance re-attaches (a host re-append
8670
+ // moves the subtree and fires disconnected→connected without a fresh
8671
+ // firstUpdated). On the very first connect `api` may not be bound yet —
8672
+ // firstUpdated repeats the call below and the timer guard dedupes.
8673
+ this.startKeepWarm();
8559
8674
  }
8560
8675
  firstUpdated() {
8561
8676
  this.focusSearchBox();
8562
8677
  this.fetchRecents();
8678
+ // Establish the worker connection while the shopper is still reaching for
8679
+ // the keyboard, and re-ping while the widget stays open so it never idles
8680
+ // out. warm() coalesces with the focus/first-keystroke triggers.
8681
+ this.startKeepWarm();
8682
+ }
8683
+ startKeepWarm() {
8684
+ var _a, _b;
8685
+ // The immediate ping stays outside the timer guard: warm() coalesces, so
8686
+ // a duplicate call is free, and skipping it would lose the first-open
8687
+ // preconnect (api binds between connectedCallback and firstUpdated).
8688
+ void ((_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.warm) === null || _b === void 0 ? void 0 : _b.call(_a));
8689
+ if (this.keepWarmTimer !== undefined) {
8690
+ return;
8691
+ }
8692
+ this.keepWarmTimer = setInterval(() => {
8693
+ var _a, _b;
8694
+ // An overlay left open in a backgrounded tab has no keystroke coming,
8695
+ // and /warm shares the per-IP rate-limit bucket with /search — don't
8696
+ // spend a NAT'd storefront's budget on a shopper who isn't looking.
8697
+ // Focus and the first keystroke both re-warm when they return.
8698
+ if (document.hidden) {
8699
+ return;
8700
+ }
8701
+ void ((_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.warm) === null || _b === void 0 ? void 0 : _b.call(_a));
8702
+ }, KEEP_WARM_INTERVAL_MS);
8563
8703
  }
8564
8704
  disconnectedCallback() {
8565
- var _a, _b, _c;
8705
+ var _a, _b, _c, _d, _e;
8566
8706
  // Closing the widget settles whatever query was on screen.
8567
8707
  this.finalizeSettledQuery();
8568
8708
  clearTimeout(this.autocompleteTimer);
8569
- (_a = this.askAbort) === null || _a === void 0 ? void 0 : _a.abort();
8570
- (_b = this.resultsAbort) === null || _b === void 0 ? void 0 : _b.abort();
8709
+ clearInterval(this.keepWarmTimer);
8710
+ this.keepWarmTimer = undefined;
8711
+ // Stops an in-flight warm retry chain from outliving the overlay.
8712
+ (_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.cancelWarm) === null || _b === void 0 ? void 0 : _b.call(_a);
8713
+ (_c = this.askAbort) === null || _c === void 0 ? void 0 : _c.abort();
8714
+ (_d = this.resultsAbort) === null || _d === void 0 ? void 0 : _d.abort();
8571
8715
  this.resetAnswerBuffer();
8572
8716
  this.removeEventListener('keydown', this.keydownListener);
8573
- (_c = this.mobileMediaQuery) === null || _c === void 0 ? void 0 : _c.removeEventListener('change', this.mobileMediaListener);
8717
+ (_e = this.mobileMediaQuery) === null || _e === void 0 ? void 0 : _e.removeEventListener('change', this.mobileMediaListener);
8574
8718
  window.removeEventListener('online', this.onlineListener);
8575
8719
  super.disconnectedCallback();
8576
8720
  }
@@ -9036,7 +9180,8 @@ class SearchWidget extends ElementWithStylesheet {
9036
9180
  if (!this.api) {
9037
9181
  return;
9038
9182
  }
9039
- const response = await this.api.search(query, undefined, this.newResultsSignal());
9183
+ const api = this.api;
9184
+ const response = await withSearchResilience((signal) => api.search(query, undefined, signal), this.newResultsSignal());
9040
9185
  if (generation !== this.requestGeneration) {
9041
9186
  return;
9042
9187
  }