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

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/hooks.cjs.js CHANGED
@@ -695,87 +695,6 @@ const processAutocompleteResponse = (response) => {
695
695
  };
696
696
  };
697
697
 
698
- // Resilience wrapper for /search requests. A transient network failure or a
699
- // hung request must not wedge the UI: add a per-attempt timeout (a hang becomes
700
- // a rejection), retry transient failures with backoff, and never retry a
701
- // caller-superseded (aborted) request. Shared by the widget and the headless
702
- // hook. See callers: search-widget.ts (performAutocomplete) and
703
- // hooks/use-evo-search.ts.
704
- const DEFAULTS = {
705
- timeoutMs: 7000,
706
- retries: 2,
707
- backoffMs: [400, 1200],
708
- };
709
- // Deterministic HTTP errors (4xx other than 429) will never succeed on
710
- // retry — only retry timeouts, network errors, 429, and 5xx. Errors without
711
- // an attached `status` (network/timeout failures) are always retryable.
712
- const isRetryable = (error) => {
713
- const status = error === null || error === void 0 ? void 0 : error.status;
714
- return status == null || status === 429 || status >= 500;
715
- };
716
- const delay = (ms, signal) => new Promise((resolve, reject) => {
717
- if (signal.aborted)
718
- return reject(new DOMException('Aborted', 'AbortError'));
719
- const t = setTimeout(resolve, ms);
720
- signal.addEventListener('abort', () => {
721
- clearTimeout(t);
722
- reject(new DOMException('Aborted', 'AbortError'));
723
- }, { once: true });
724
- });
725
- /**
726
- * Run `attempt` with a timeout, retrying transient failures. `callerSignal`
727
- * supersedes everything: if it aborts, we stop immediately without retrying
728
- * (the query changed / the widget cleaned up). A timeout or network error is
729
- * retried up to `retries` times; anything left over is rethrown.
730
- */
731
- async function withSearchResilience(attempt, callerSignal, options = {}) {
732
- var _a, _b;
733
- const { timeoutMs, retries, backoffMs } = { ...DEFAULTS, ...options };
734
- let lastError;
735
- for (let i = 0; i <= retries; i++) {
736
- if (callerSignal.aborted)
737
- throw new DOMException('Aborted', 'AbortError');
738
- const local = new AbortController();
739
- const onCallerAbort = () => local.abort();
740
- callerSignal.addEventListener('abort', onCallerAbort, { once: true });
741
- // Race the attempt against an explicit timeout that REJECTS. Aborting the
742
- // signal alone isn't enough — an attempt that ignores its signal would
743
- // otherwise hang forever and never retry. Reject with a TimeoutError (not
744
- // an AbortError) so callers surface it instead of treating it as a
745
- // caller-cancellation.
746
- let timer;
747
- const timeout = new Promise((_, reject) => {
748
- timer = setTimeout(() => {
749
- local.abort(); // still cancel the in-flight request to free the socket
750
- reject(new DOMException('Search request timed out', 'TimeoutError'));
751
- }, timeoutMs);
752
- });
753
- try {
754
- return await Promise.race([attempt(local.signal), timeout]);
755
- }
756
- catch (error) {
757
- lastError = error;
758
- // The caller superseded this request — do not retry, propagate the abort.
759
- if (callerSignal.aborted)
760
- throw error;
761
- // Otherwise it was a timeout or a transient network error: retry if budget remains.
762
- if (i < retries && isRetryable(error)) {
763
- const wait = (_b = (_a = backoffMs[i]) !== null && _a !== void 0 ? _a : backoffMs[backoffMs.length - 1]) !== null && _b !== void 0 ? _b : 0;
764
- // Rejects with an AbortError if the caller aborts mid-backoff —
765
- // propagate that cancellation instead of the transient `error` above.
766
- await delay(wait, callerSignal);
767
- continue;
768
- }
769
- throw error;
770
- }
771
- finally {
772
- clearTimeout(timer);
773
- callerSignal.removeEventListener('abort', onCallerAbort);
774
- }
775
- }
776
- throw lastError;
777
- }
778
-
779
698
  const isDevEnv$1 = () => typeof process != 'undefined' && process.env['NODE_ENV'] != 'production';
780
699
  const emptyReturn = (query, setQuery, trackClick) => ({
781
700
  query,
@@ -847,7 +766,9 @@ const useEvoSearch = (options = {}) => {
847
766
  const controller = new AbortController();
848
767
  const timer = setTimeout(async () => {
849
768
  try {
850
- const res = await withSearchResilience((signal) => api.search(query, undefined, signal), controller.signal);
769
+ // Resilience is inside api.search() now — wrapping again would
770
+ // double-wrap to 9 attempts.
771
+ const res = await api.search(query, undefined, controller.signal);
851
772
  setResults(processAutocompleteResponse(res).products);
852
773
  if (wantsSuggestions) {
853
774
  setSuggestions((prev) => {
package/hooks.mjs CHANGED
@@ -693,87 +693,6 @@ const processAutocompleteResponse = (response) => {
693
693
  };
694
694
  };
695
695
 
696
- // Resilience wrapper for /search requests. A transient network failure or a
697
- // hung request must not wedge the UI: add a per-attempt timeout (a hang becomes
698
- // a rejection), retry transient failures with backoff, and never retry a
699
- // caller-superseded (aborted) request. Shared by the widget and the headless
700
- // hook. See callers: search-widget.ts (performAutocomplete) and
701
- // hooks/use-evo-search.ts.
702
- const DEFAULTS = {
703
- timeoutMs: 7000,
704
- retries: 2,
705
- backoffMs: [400, 1200],
706
- };
707
- // Deterministic HTTP errors (4xx other than 429) will never succeed on
708
- // retry — only retry timeouts, network errors, 429, and 5xx. Errors without
709
- // an attached `status` (network/timeout failures) are always retryable.
710
- const isRetryable = (error) => {
711
- const status = error === null || error === void 0 ? void 0 : error.status;
712
- return status == null || status === 429 || status >= 500;
713
- };
714
- const delay = (ms, signal) => new Promise((resolve, reject) => {
715
- if (signal.aborted)
716
- return reject(new DOMException('Aborted', 'AbortError'));
717
- const t = setTimeout(resolve, ms);
718
- signal.addEventListener('abort', () => {
719
- clearTimeout(t);
720
- reject(new DOMException('Aborted', 'AbortError'));
721
- }, { once: true });
722
- });
723
- /**
724
- * Run `attempt` with a timeout, retrying transient failures. `callerSignal`
725
- * supersedes everything: if it aborts, we stop immediately without retrying
726
- * (the query changed / the widget cleaned up). A timeout or network error is
727
- * retried up to `retries` times; anything left over is rethrown.
728
- */
729
- async function withSearchResilience(attempt, callerSignal, options = {}) {
730
- var _a, _b;
731
- const { timeoutMs, retries, backoffMs } = { ...DEFAULTS, ...options };
732
- let lastError;
733
- for (let i = 0; i <= retries; i++) {
734
- if (callerSignal.aborted)
735
- throw new DOMException('Aborted', 'AbortError');
736
- const local = new AbortController();
737
- const onCallerAbort = () => local.abort();
738
- callerSignal.addEventListener('abort', onCallerAbort, { once: true });
739
- // Race the attempt against an explicit timeout that REJECTS. Aborting the
740
- // signal alone isn't enough — an attempt that ignores its signal would
741
- // otherwise hang forever and never retry. Reject with a TimeoutError (not
742
- // an AbortError) so callers surface it instead of treating it as a
743
- // caller-cancellation.
744
- let timer;
745
- const timeout = new Promise((_, reject) => {
746
- timer = setTimeout(() => {
747
- local.abort(); // still cancel the in-flight request to free the socket
748
- reject(new DOMException('Search request timed out', 'TimeoutError'));
749
- }, timeoutMs);
750
- });
751
- try {
752
- return await Promise.race([attempt(local.signal), timeout]);
753
- }
754
- catch (error) {
755
- lastError = error;
756
- // The caller superseded this request — do not retry, propagate the abort.
757
- if (callerSignal.aborted)
758
- throw error;
759
- // Otherwise it was a timeout or a transient network error: retry if budget remains.
760
- if (i < retries && isRetryable(error)) {
761
- const wait = (_b = (_a = backoffMs[i]) !== null && _a !== void 0 ? _a : backoffMs[backoffMs.length - 1]) !== null && _b !== void 0 ? _b : 0;
762
- // Rejects with an AbortError if the caller aborts mid-backoff —
763
- // propagate that cancellation instead of the transient `error` above.
764
- await delay(wait, callerSignal);
765
- continue;
766
- }
767
- throw error;
768
- }
769
- finally {
770
- clearTimeout(timer);
771
- callerSignal.removeEventListener('abort', onCallerAbort);
772
- }
773
- }
774
- throw lastError;
775
- }
776
-
777
696
  const isDevEnv$1 = () => typeof process != 'undefined' && process.env['NODE_ENV'] != 'production';
778
697
  const emptyReturn = (query, setQuery, trackClick) => ({
779
698
  query,
@@ -845,7 +764,9 @@ const useEvoSearch = (options = {}) => {
845
764
  const controller = new AbortController();
846
765
  const timer = setTimeout(async () => {
847
766
  try {
848
- const res = await withSearchResilience((signal) => api.search(query, undefined, signal), controller.signal);
767
+ // Resilience is inside api.search() now — wrapping again would
768
+ // double-wrap to 9 attempts.
769
+ const res = await api.search(query, undefined, controller.signal);
849
770
  setResults(processAutocompleteResponse(res).products);
850
771
  if (wantsSuggestions) {
851
772
  setSuggestions((prev) => {
package/index.cjs.js CHANGED
@@ -837,24 +837,48 @@ const isRetryable = (error) => {
837
837
  const status = error === null || error === void 0 ? void 0 : error.status;
838
838
  return status == null || status === 429 || status >= 500;
839
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
- });
840
+ // `navigator.onLine` is only informative when false; wait for the network rather
841
+ // than spending retries on a route that doesn't exist.
842
+ const isOffline = () => typeof navigator != 'undefined' && navigator.onLine === false;
843
+ // Waits `ms`, or until the network returns, whichever is first.
844
+ const delay = (ms, signal) => {
845
+ // Whichever of timer/online/abort settles first tears down the other two.
846
+ let cleanup = () => undefined;
847
+ const waiting = new Promise((resolve, reject) => {
848
+ var _a;
849
+ if (signal.aborted)
850
+ return reject(new DOMException('Aborted', 'AbortError'));
851
+ const timer = setTimeout(resolve, ms);
852
+ const onOnline = () => resolve();
853
+ const onAbort = () => reject(new DOMException('Aborted', 'AbortError'));
854
+ (_a = globalThis.addEventListener) === null || _a === void 0 ? void 0 : _a.call(globalThis, 'online', onOnline);
855
+ signal.addEventListener('abort', onAbort, { once: true });
856
+ cleanup = () => {
857
+ var _a;
858
+ clearTimeout(timer);
859
+ (_a = globalThis.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(globalThis, 'online', onOnline);
860
+ signal.removeEventListener('abort', onAbort);
861
+ };
862
+ });
863
+ return waiting.finally(() => cleanup());
864
+ };
849
865
  /**
850
866
  * Run `attempt` with a timeout, retrying transient failures. `callerSignal`
851
867
  * supersedes everything: if it aborts, we stop immediately without retrying
852
868
  * (the query changed / the widget cleaned up). A timeout or network error is
853
869
  * retried up to `retries` times; anything left over is rethrown.
870
+ *
871
+ * Every phase draws from one shared deadline — the nominal schedule of all
872
+ * attempts plus all backoffs — so waiting out an offline route borrows from the
873
+ * budget instead of widening it, and the whole call still ends within it.
854
874
  */
855
875
  async function withSearchResilience(attempt, callerSignal, options = {}) {
856
876
  var _a, _b;
857
877
  const { timeoutMs, retries, backoffMs } = { ...DEFAULTS, ...options };
878
+ const deadline = Date.now() +
879
+ (retries + 1) * timeoutMs +
880
+ backoffMs.reduce((sum, ms) => sum + ms, 0);
881
+ const remaining = () => deadline - Date.now();
858
882
  let lastError;
859
883
  for (let i = 0; i <= retries; i++) {
860
884
  if (callerSignal.aborted)
@@ -872,7 +896,7 @@ async function withSearchResilience(attempt, callerSignal, options = {}) {
872
896
  timer = setTimeout(() => {
873
897
  local.abort(); // still cancel the in-flight request to free the socket
874
898
  reject(new DOMException('Search request timed out', 'TimeoutError'));
875
- }, timeoutMs);
899
+ }, Math.min(timeoutMs, remaining()));
876
900
  });
877
901
  try {
878
902
  return await Promise.race([attempt(local.signal), timeout]);
@@ -884,7 +908,11 @@ async function withSearchResilience(attempt, callerSignal, options = {}) {
884
908
  throw error;
885
909
  // Otherwise it was a timeout or a transient network error: retry if budget remains.
886
910
  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;
911
+ // Offline: hold for the network instead of burning the retry on a route
912
+ // that doesn't exist — but only with time the call already had.
913
+ const wait = isOffline()
914
+ ? remaining()
915
+ : (_b = (_a = backoffMs[i]) !== null && _a !== void 0 ? _a : backoffMs[backoffMs.length - 1]) !== null && _b !== void 0 ? _b : 0;
888
916
  // Rejects with an AbortError if the caller aborts mid-backoff —
889
917
  // propagate that cancellation instead of the transient `error` above.
890
918
  await delay(wait, callerSignal);
@@ -900,14 +928,21 @@ async function withSearchResilience(attempt, callerSignal, options = {}) {
900
928
  throw lastError;
901
929
  }
902
930
 
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.
931
+ // Ask can't reuse withSearchResilience: a half-consumed stream isn't retryable.
932
+ // Idle timeout resets per chunk; both reject with TimeoutError so submitAsk
933
+ // routes them to the search fallback rather than treating them as cancellation.
909
934
  const ASK_REQUEST_TIMEOUT_MS = 7000;
910
935
  const ASK_SSE_IDLE_TIMEOUT_MS = 10000;
936
+ // Keep-warm cadence for `warm()`. Chrome reaps used-idle sockets after ~5
937
+ // minutes and the edge sooner than that under load; a ping well inside those
938
+ // windows keeps the established h2/h3 connection alive so a keystroke after an
939
+ // idle pause multiplexes onto it instead of paying (and possibly wedging on)
940
+ // fresh connection establishment. Lives here rather than in either caller so
941
+ // the widget's open-scoped timer and init's headless timer can't drift apart.
942
+ const KEEP_WARM_INTERVAL_MS = 60000;
943
+ // Stand-in caller signal for `search()` calls that don't pass one. Module-level
944
+ // and never aborted, so it carries no state between calls.
945
+ const NEVER_ABORTED = new AbortController().signal;
911
946
  // Always defer to what the storefront itself is rendering — same source
912
947
  // shop-gpt reads from. Two widgets on the same page would otherwise drift:
913
948
  // shop-gpt picks Shopify, evo-search would pick its own override and show
@@ -1066,9 +1101,13 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
1066
1101
  };
1067
1102
  const deleteRecent = (value) => sendMutation('/user/recents', 'DeleteRecent', 'DELETE', { value });
1068
1103
  const clearRecents = () => sendMutation('/user/recents', 'ClearRecents', 'DELETE');
1104
+ // Resilience lives here, not at each call site, so no consumer can opt out —
1105
+ // the headless channel previously had no deadline and no retry.
1069
1106
  const search = async (query, limit = 8, signal) => {
1070
1107
  logger.info(`EvoSearch API: Search request - query length: ${query.length}, limit: ${limit}`);
1071
- const data = await getJSON('/search', 'Search', { q: query, limit: String(limit) }, signal);
1108
+ const data = await withSearchResilience((attemptSignal) => getJSON('/search', 'Search', { q: query, limit: String(limit) }, attemptSignal),
1109
+ // Shared stand-in is safe: the wrapper removes its abort listener per attempt.
1110
+ signal !== null && signal !== void 0 ? signal : NEVER_ABORTED);
1072
1111
  logger.info(`EvoSearch API: Search success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
1073
1112
  return data;
1074
1113
  };
@@ -1266,6 +1305,36 @@ const parseSseFrame = (frame) => {
1266
1305
  };
1267
1306
 
1268
1307
  const error = (message) => console.error(message);
1308
+ // Headless hosts render their own UI, so nothing else pings /warm for them.
1309
+ let disposeKeepWarm;
1310
+ // A ping on load, on the interval, and on the two events that mean the shopper
1311
+ // is back — tab return, and the network returning after a sleep suspended the
1312
+ // timer. Skipped while hidden: no keystroke is coming and /warm shares its
1313
+ // per-IP bucket with /search.
1314
+ const startHeadlessKeepWarm = (api) => {
1315
+ // Repeated init() would otherwise leave earlier timers and listeners warming
1316
+ // a replaced API forever.
1317
+ disposeKeepWarm === null || disposeKeepWarm === void 0 ? void 0 : disposeKeepWarm();
1318
+ const ping = () => {
1319
+ if (!document.hidden) {
1320
+ void api.warm();
1321
+ }
1322
+ };
1323
+ ping();
1324
+ const timer = setInterval(ping, KEEP_WARM_INTERVAL_MS);
1325
+ document.addEventListener('visibilitychange', ping);
1326
+ window.addEventListener('online', ping);
1327
+ disposeKeepWarm = () => {
1328
+ clearInterval(timer);
1329
+ document.removeEventListener('visibilitychange', ping);
1330
+ window.removeEventListener('online', ping);
1331
+ // Stopping the cadence must also kill an in-flight retry chain: a
1332
+ // black-holed /warm would otherwise keep re-establishing connections for
1333
+ // the wrapper's full budget against a replaced API.
1334
+ api.cancelWarm();
1335
+ disposeKeepWarm = undefined;
1336
+ };
1337
+ };
1269
1338
  const init = (params) => {
1270
1339
  var _a, _b, _c, _d, _e, _f, _g, _h;
1271
1340
  if (typeof window == 'undefined' || typeof document == 'undefined') {
@@ -1274,6 +1343,9 @@ const init = (params) => {
1274
1343
  try {
1275
1344
  (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
1276
1345
  logger.info('EvoSearch SDK: Initializing');
1346
+ // A re-init can land on a different channel, or bail out entirely, so drop
1347
+ // any previous cadence before deciding which one this run owns.
1348
+ disposeKeepWarm === null || disposeKeepWarm === void 0 ? void 0 : disposeKeepWarm();
1277
1349
  if (new URLSearchParams(window.location.search).get('evs') === 'preview') {
1278
1350
  try {
1279
1351
  sessionStorage.setItem(previewKeyName, '1');
@@ -1313,8 +1385,12 @@ const init = (params) => {
1313
1385
  options: { destination: params.baseUrl },
1314
1386
  });
1315
1387
  if (!uiImplementation) {
1316
- if ((_f = (_e = params.manifest) === null || _e === void 0 ? void 0 : _e.variables) === null || _f === void 0 ? void 0 : _f.disableDefaultUI) {
1388
+ if (((_f = (_e = params.manifest) === null || _e === void 0 ? void 0 : _e.variables) === null || _f === void 0 ? void 0 : _f.disableDefaultUI) === true) {
1317
1389
  logger.info('EvoSearch SDK: Running in headless mode, no default UI will render');
1390
+ // Deliberately keyed on `disableDefaultUI` rather than bare `headless`:
1391
+ // in the else branch the UI is missing by mistake, so search isn't
1392
+ // usable and there's no connection worth holding open.
1393
+ startHeadlessKeepWarm(evoSearchAPI);
1318
1394
  }
1319
1395
  else {
1320
1396
  error('UI implementation is missing');
@@ -8495,12 +8571,6 @@ const searchWidgetStyles = i$6 `
8495
8571
  `;
8496
8572
 
8497
8573
  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;
8504
8574
  // @state fields whose change should be persisted to the session snapshot.
8505
8575
  const SESSION_PERSIST_KEYS = [
8506
8576
  'searchQuery',
@@ -8590,7 +8660,27 @@ class SearchWidget extends ElementWithStylesheet {
8590
8660
  // autocomplete can't overwrite a fresh search's results.
8591
8661
  this.requestGeneration = 0;
8592
8662
  this.keydownListener = (event) => this.handleKeydown(event);
8593
- this.onlineListener = () => this.handleReconnect();
8663
+ // The keep-warm interval below is skipped while hidden, so the next tick can
8664
+ // be a minute after the shopper returns — re-establish on both signals that
8665
+ // mean they're back: tab return, and the network returning after a sleep
8666
+ // suspended the timer. Bails out while hidden: no keystroke is coming, and
8667
+ // both /warm and the self-heal /search share the per-IP bucket. A tab that
8668
+ // errored while hidden heals on the visibilitychange leg instead.
8669
+ // Warm before re-querying: re-querying first would pay connection setup on
8670
+ // the query itself, which is what wedges. warm() never rejects.
8671
+ this.rewarmListener = async () => {
8672
+ var _a, _b;
8673
+ if (document.hidden) {
8674
+ return;
8675
+ }
8676
+ await ((_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.warm) === null || _b === void 0 ? void 0 : _b.call(_a));
8677
+ // The overlay can close during the await; handleReconnect would otherwise
8678
+ // issue a query and mutate state on a detached widget.
8679
+ if (!this.isConnected) {
8680
+ return;
8681
+ }
8682
+ this.handleReconnect();
8683
+ };
8594
8684
  // Streaming tokens are coalesced into one per-frame state update rather than
8595
8685
  // reassigning `askAnswer` on every token — a ~220-token answer would
8596
8686
  // otherwise trigger ~220 full widget renders.
@@ -8665,7 +8755,8 @@ class SearchWidget extends ElementWithStylesheet {
8665
8755
  this.mobileMediaQuery = window.matchMedia('(max-width: 768px)');
8666
8756
  this.isMobile = this.mobileMediaQuery.matches;
8667
8757
  this.mobileMediaQuery.addEventListener('change', this.mobileMediaListener);
8668
- window.addEventListener('online', this.onlineListener);
8758
+ window.addEventListener('online', this.rewarmListener);
8759
+ document.addEventListener('visibilitychange', this.rewarmListener);
8669
8760
  // Restart keep-warm when the same instance re-attaches (a host re-append
8670
8761
  // moves the subtree and fires disconnected→connected without a fresh
8671
8762
  // firstUpdated). On the very first connect `api` may not be bound yet —
@@ -8715,7 +8806,8 @@ class SearchWidget extends ElementWithStylesheet {
8715
8806
  this.resetAnswerBuffer();
8716
8807
  this.removeEventListener('keydown', this.keydownListener);
8717
8808
  (_e = this.mobileMediaQuery) === null || _e === void 0 ? void 0 : _e.removeEventListener('change', this.mobileMediaListener);
8718
- window.removeEventListener('online', this.onlineListener);
8809
+ window.removeEventListener('online', this.rewarmListener);
8810
+ document.removeEventListener('visibilitychange', this.rewarmListener);
8719
8811
  super.disconnectedCallback();
8720
8812
  }
8721
8813
  willUpdate(changed) {
@@ -9135,7 +9227,9 @@ class SearchWidget extends ElementWithStylesheet {
9135
9227
  return;
9136
9228
  }
9137
9229
  const api = this.api;
9138
- const response = await withSearchResilience((signal) => api.search(query, undefined, signal), this.newResultsSignal());
9230
+ // api.search() now owns the per-attempt timeout and retry, so wrapping
9231
+ // here would double-wrap to 9 attempts.
9232
+ const response = await api.search(query, undefined, this.newResultsSignal());
9139
9233
  if (generation !== this.requestGeneration) {
9140
9234
  return;
9141
9235
  }
@@ -9181,7 +9275,9 @@ class SearchWidget extends ElementWithStylesheet {
9181
9275
  return;
9182
9276
  }
9183
9277
  const api = this.api;
9184
- const response = await withSearchResilience((signal) => api.search(query, undefined, signal), this.newResultsSignal());
9278
+ // api.search() now owns the per-attempt timeout and retry, so wrapping
9279
+ // here would double-wrap to 9 attempts.
9280
+ const response = await api.search(query, undefined, this.newResultsSignal());
9185
9281
  if (generation !== this.requestGeneration) {
9186
9282
  return;
9187
9283
  }