@blotoutio/providers-evo-search-sdk 1.68.2 → 1.69.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/core.cjs.js CHANGED
@@ -1481,7 +1481,7 @@ const init = (params) => {
1481
1481
  const tag = ({ data }) => {
1482
1482
  try {
1483
1483
  return {
1484
- sdkVersion: "1.68.2" || '',
1484
+ sdkVersion: "1.69.0" || '',
1485
1485
  searchQuery: data['searchQuery'],
1486
1486
  searchResults: data['searchResults'],
1487
1487
  timestamp: Date.now(),
@@ -1490,7 +1490,7 @@ const tag = ({ data }) => {
1490
1490
  catch (err) {
1491
1491
  console.error(`EvoSearch SDK: Tagging failed - ${err}`);
1492
1492
  return {
1493
- sdkVersion: "1.68.2" ,
1493
+ sdkVersion: "1.69.0" ,
1494
1494
  timestamp: Date.now(),
1495
1495
  };
1496
1496
  }
package/core.js CHANGED
@@ -1482,7 +1482,7 @@ var ProvidersEvoSearchSdk = (function () {
1482
1482
  const tag = ({ data }) => {
1483
1483
  try {
1484
1484
  return {
1485
- sdkVersion: "1.68.2" || '',
1485
+ sdkVersion: "1.69.0" || '',
1486
1486
  searchQuery: data['searchQuery'],
1487
1487
  searchResults: data['searchResults'],
1488
1488
  timestamp: Date.now(),
@@ -1491,7 +1491,7 @@ var ProvidersEvoSearchSdk = (function () {
1491
1491
  catch (err) {
1492
1492
  console.error(`EvoSearch SDK: Tagging failed - ${err}`);
1493
1493
  return {
1494
- sdkVersion: "1.68.2" ,
1494
+ sdkVersion: "1.69.0" ,
1495
1495
  timestamp: Date.now(),
1496
1496
  };
1497
1497
  }
package/core.mjs CHANGED
@@ -1479,7 +1479,7 @@ const init = (params) => {
1479
1479
  const tag = ({ data }) => {
1480
1480
  try {
1481
1481
  return {
1482
- sdkVersion: "1.68.2" || '',
1482
+ sdkVersion: "1.69.0" || '',
1483
1483
  searchQuery: data['searchQuery'],
1484
1484
  searchResults: data['searchResults'],
1485
1485
  timestamp: Date.now(),
@@ -1488,7 +1488,7 @@ const tag = ({ data }) => {
1488
1488
  catch (err) {
1489
1489
  console.error(`EvoSearch SDK: Tagging failed - ${err}`);
1490
1490
  return {
1491
- sdkVersion: "1.68.2" ,
1491
+ sdkVersion: "1.69.0" ,
1492
1492
  timestamp: Date.now(),
1493
1493
  };
1494
1494
  }
package/hooks.cjs.js CHANGED
@@ -935,7 +935,19 @@ const useEvoSearch = (options = {}) => {
935
935
  }, [query, ready, api, mode, retryNonce]);
936
936
  // Widget parity: report a settled query as one real search after SEARCH_SETTLE_MS.
937
937
  const lastFinalizedRef = react.useRef('');
938
+ // The query whose rendered results are waiting out the settle pause. Held in
939
+ // a ref so the teardown flush below can reach it: on a headless storefront a
940
+ // product click is a full navigation, so a shopper who clicks within the
941
+ // settle window unmounts this hook before the timer fires — and losing that
942
+ // event deletes exactly the successful searches from the finalized count.
943
+ // Measured on a real store at ~2x scale (zox, solutions#9987).
944
+ const pendingFinalizeRef = react.useRef(null);
938
945
  react.useEffect(() => {
946
+ // Re-evaluated on every dependency change, so anything short of an armed
947
+ // timer means nothing is pending: a superseded query's results left the
948
+ // screen with it, and flushing them from teardown would count a search the
949
+ // shopper abandoned.
950
+ pendingFinalizeRef.current = null;
939
951
  if (!api || mode === 'autocomplete' || loading || error) {
940
952
  return;
941
953
  }
@@ -948,16 +960,51 @@ const useEvoSearch = (options = {}) => {
948
960
  if (cleaned.toLowerCase() === lastFinalizedRef.current) {
949
961
  return;
950
962
  }
963
+ const actionData = {
964
+ query: cleaned,
965
+ resultCount: results.length,
966
+ };
967
+ pendingFinalizeRef.current = { api, actionData };
951
968
  const timer = setTimeout(() => {
969
+ pendingFinalizeRef.current = null;
970
+ // Re-checked at fire time, not only at arm time: a pagehide flush may
971
+ // have counted this query while the timer was pending (a bfcache return
972
+ // resumes it), and firing again would double the search.
973
+ if (cleaned.toLowerCase() === lastFinalizedRef.current) {
974
+ return;
975
+ }
952
976
  lastFinalizedRef.current = cleaned.toLowerCase();
953
- const actionData = {
954
- query: cleaned,
955
- resultCount: results.length,
956
- };
957
977
  api.sendEvent('evoSearchSearchIssued', undefined, actionData);
958
978
  }, SEARCH_SETTLE_MS);
979
+ // Only the timer: the pending marker survives this cleanup so a teardown
980
+ // that never re-runs the effect (unmount, navigation) can still flush it.
981
+ // A dependency change that IS a re-run nulls it at the top first.
959
982
  return () => clearTimeout(timer);
960
983
  }, [api, mode, query, results, loading, error]);
984
+ // The teardown flush. A shopper who clicks a result inside the settle window
985
+ // performed the most successful search there is; `sendEvent` posts with
986
+ // keepalive, so firing it from pagehide/unmount survives the navigation.
987
+ // Marking `lastFinalizedRef` keeps a bfcache return from settling the same
988
+ // query into a second event.
989
+ react.useEffect(() => {
990
+ if (typeof window === 'undefined') {
991
+ return;
992
+ }
993
+ const flush = () => {
994
+ const pending = pendingFinalizeRef.current;
995
+ if (!pending) {
996
+ return;
997
+ }
998
+ pendingFinalizeRef.current = null;
999
+ lastFinalizedRef.current = pending.actionData.query.trim().toLowerCase();
1000
+ pending.api.sendEvent('evoSearchSearchIssued', undefined, pending.actionData);
1001
+ };
1002
+ window.addEventListener('pagehide', flush);
1003
+ return () => {
1004
+ window.removeEventListener('pagehide', flush);
1005
+ flush();
1006
+ };
1007
+ }, []);
961
1008
  // Image (or image+text) search, imperative because it is driven by a file
962
1009
  // picker rather than the query state. Deliberately NOT given the retrying
963
1010
  // resilience defaults: each attempt costs a multimodal embed, so we take the
@@ -982,6 +1029,22 @@ const useEvoSearch = (options = {}) => {
982
1029
  setResults(res.products.map(mapAPIProductToSDKProduct));
983
1030
  // Suggestions are a text-search artifact; an image query has none.
984
1031
  setSuggestions([]);
1032
+ // A typed refinement over a photo is a query with text, and the server
1033
+ // counts it off this event exactly as it counts a settled keyword
1034
+ // search — nothing else counts it, because an `image+text` request row
1035
+ // is neither a keyword row nor a per-action submit. Per action rather
1036
+ // than settled: this is an imperative submit, so there is no typing
1037
+ // pause to wait out and no `query` state for the settle effect to see.
1038
+ // A pure image search is counted server-side from its own request row
1039
+ // and must not be double-reported here.
1040
+ const refinement = text === null || text === void 0 ? void 0 : text.trim();
1041
+ if (refinement) {
1042
+ const imageActionData = {
1043
+ query: refinement,
1044
+ resultCount: res.products.length,
1045
+ };
1046
+ api.sendEvent('evoSearchSearchIssued', undefined, imageActionData);
1047
+ }
985
1048
  }
986
1049
  catch (err) {
987
1050
  if (err instanceof DOMException && err.name === 'AbortError') {
package/hooks.mjs CHANGED
@@ -933,7 +933,19 @@ const useEvoSearch = (options = {}) => {
933
933
  }, [query, ready, api, mode, retryNonce]);
934
934
  // Widget parity: report a settled query as one real search after SEARCH_SETTLE_MS.
935
935
  const lastFinalizedRef = useRef('');
936
+ // The query whose rendered results are waiting out the settle pause. Held in
937
+ // a ref so the teardown flush below can reach it: on a headless storefront a
938
+ // product click is a full navigation, so a shopper who clicks within the
939
+ // settle window unmounts this hook before the timer fires — and losing that
940
+ // event deletes exactly the successful searches from the finalized count.
941
+ // Measured on a real store at ~2x scale (zox, solutions#9987).
942
+ const pendingFinalizeRef = useRef(null);
936
943
  useEffect(() => {
944
+ // Re-evaluated on every dependency change, so anything short of an armed
945
+ // timer means nothing is pending: a superseded query's results left the
946
+ // screen with it, and flushing them from teardown would count a search the
947
+ // shopper abandoned.
948
+ pendingFinalizeRef.current = null;
937
949
  if (!api || mode === 'autocomplete' || loading || error) {
938
950
  return;
939
951
  }
@@ -946,16 +958,51 @@ const useEvoSearch = (options = {}) => {
946
958
  if (cleaned.toLowerCase() === lastFinalizedRef.current) {
947
959
  return;
948
960
  }
961
+ const actionData = {
962
+ query: cleaned,
963
+ resultCount: results.length,
964
+ };
965
+ pendingFinalizeRef.current = { api, actionData };
949
966
  const timer = setTimeout(() => {
967
+ pendingFinalizeRef.current = null;
968
+ // Re-checked at fire time, not only at arm time: a pagehide flush may
969
+ // have counted this query while the timer was pending (a bfcache return
970
+ // resumes it), and firing again would double the search.
971
+ if (cleaned.toLowerCase() === lastFinalizedRef.current) {
972
+ return;
973
+ }
950
974
  lastFinalizedRef.current = cleaned.toLowerCase();
951
- const actionData = {
952
- query: cleaned,
953
- resultCount: results.length,
954
- };
955
975
  api.sendEvent('evoSearchSearchIssued', undefined, actionData);
956
976
  }, SEARCH_SETTLE_MS);
977
+ // Only the timer: the pending marker survives this cleanup so a teardown
978
+ // that never re-runs the effect (unmount, navigation) can still flush it.
979
+ // A dependency change that IS a re-run nulls it at the top first.
957
980
  return () => clearTimeout(timer);
958
981
  }, [api, mode, query, results, loading, error]);
982
+ // The teardown flush. A shopper who clicks a result inside the settle window
983
+ // performed the most successful search there is; `sendEvent` posts with
984
+ // keepalive, so firing it from pagehide/unmount survives the navigation.
985
+ // Marking `lastFinalizedRef` keeps a bfcache return from settling the same
986
+ // query into a second event.
987
+ useEffect(() => {
988
+ if (typeof window === 'undefined') {
989
+ return;
990
+ }
991
+ const flush = () => {
992
+ const pending = pendingFinalizeRef.current;
993
+ if (!pending) {
994
+ return;
995
+ }
996
+ pendingFinalizeRef.current = null;
997
+ lastFinalizedRef.current = pending.actionData.query.trim().toLowerCase();
998
+ pending.api.sendEvent('evoSearchSearchIssued', undefined, pending.actionData);
999
+ };
1000
+ window.addEventListener('pagehide', flush);
1001
+ return () => {
1002
+ window.removeEventListener('pagehide', flush);
1003
+ flush();
1004
+ };
1005
+ }, []);
959
1006
  // Image (or image+text) search, imperative because it is driven by a file
960
1007
  // picker rather than the query state. Deliberately NOT given the retrying
961
1008
  // resilience defaults: each attempt costs a multimodal embed, so we take the
@@ -980,6 +1027,22 @@ const useEvoSearch = (options = {}) => {
980
1027
  setResults(res.products.map(mapAPIProductToSDKProduct));
981
1028
  // Suggestions are a text-search artifact; an image query has none.
982
1029
  setSuggestions([]);
1030
+ // A typed refinement over a photo is a query with text, and the server
1031
+ // counts it off this event exactly as it counts a settled keyword
1032
+ // search — nothing else counts it, because an `image+text` request row
1033
+ // is neither a keyword row nor a per-action submit. Per action rather
1034
+ // than settled: this is an imperative submit, so there is no typing
1035
+ // pause to wait out and no `query` state for the settle effect to see.
1036
+ // A pure image search is counted server-side from its own request row
1037
+ // and must not be double-reported here.
1038
+ const refinement = text === null || text === void 0 ? void 0 : text.trim();
1039
+ if (refinement) {
1040
+ const imageActionData = {
1041
+ query: refinement,
1042
+ resultCount: res.products.length,
1043
+ };
1044
+ api.sendEvent('evoSearchSearchIssued', undefined, imageActionData);
1045
+ }
983
1046
  }
984
1047
  catch (err) {
985
1048
  if (err instanceof DOMException && err.name === 'AbortError') {
package/index.cjs.js CHANGED
@@ -2096,7 +2096,7 @@ const init = (params) => {
2096
2096
  const tag = ({ data }) => {
2097
2097
  try {
2098
2098
  return {
2099
- sdkVersion: "1.68.2" || '',
2099
+ sdkVersion: "1.69.0" || '',
2100
2100
  searchQuery: data['searchQuery'],
2101
2101
  searchResults: data['searchResults'],
2102
2102
  timestamp: Date.now(),
@@ -2105,7 +2105,7 @@ const tag = ({ data }) => {
2105
2105
  catch (err) {
2106
2106
  console.error(`EvoSearch SDK: Tagging failed - ${err}`);
2107
2107
  return {
2108
- sdkVersion: "1.68.2" ,
2108
+ sdkVersion: "1.69.0" ,
2109
2109
  timestamp: Date.now(),
2110
2110
  };
2111
2111
  }
@@ -9870,6 +9870,8 @@ class SearchWidget extends ElementWithStylesheet {
9870
9870
  };
9871
9871
  this.isHandlingSuggestionClick = false;
9872
9872
  this.cachedRailSections = [];
9873
+ // Bound once so connected/disconnected add and remove the SAME reference.
9874
+ this.finalizeFlushListener = () => this.flushSettledQuery();
9873
9875
  // Last finalized query (lowercased) — consecutive duplicates (settle
9874
9876
  // followed by Enter on the same query) collapse into one search.
9875
9877
  this.lastFinalizedQuery = '';
@@ -10035,6 +10037,12 @@ class SearchWidget extends ElementWithStylesheet {
10035
10037
  this.mobileMediaQuery.addEventListener('change', this.mobileMediaListener);
10036
10038
  window.addEventListener('online', this.rewarmListener);
10037
10039
  document.addEventListener('visibilitychange', this.rewarmListener);
10040
+ // A product card is a plain anchor, so a click inside the settle window is
10041
+ // a full navigation that would otherwise kill the pending finalize — and
10042
+ // a fast click-through is the most successful search there is. sendEvent
10043
+ // posts with keepalive, so flushing from pagehide survives the unload;
10044
+ // markQueryFinalized's dedupe keeps a bfcache return from doubling it.
10045
+ window.addEventListener('pagehide', this.finalizeFlushListener);
10038
10046
  // Restart keep-warm when the same instance re-attaches (a host re-append
10039
10047
  // moves the subtree and fires disconnected→connected without a fresh
10040
10048
  // firstUpdated). On the very first connect `api` may not be bound yet —
@@ -10072,8 +10080,11 @@ class SearchWidget extends ElementWithStylesheet {
10072
10080
  }
10073
10081
  disconnectedCallback() {
10074
10082
  var _a, _b, _c, _d, _e;
10075
- // Closing the widget settles whatever query was on screen.
10076
- this.finalizeSettledQuery();
10083
+ // Closing the widget — and a host-driven teardown that removes the overlay
10084
+ // mid-settle — settles whatever query was waiting out its pause, the same
10085
+ // way a navigation does. Guarded, so a query the shopper already typed past
10086
+ // is not counted.
10087
+ this.flushSettledQuery();
10077
10088
  clearTimeout(this.autocompleteTimer);
10078
10089
  clearInterval(this.keepWarmTimer);
10079
10090
  this.keepWarmTimer = undefined;
@@ -10093,6 +10104,7 @@ class SearchWidget extends ElementWithStylesheet {
10093
10104
  (_e = this.mobileMediaQuery) === null || _e === void 0 ? void 0 : _e.removeEventListener('change', this.mobileMediaListener);
10094
10105
  window.removeEventListener('online', this.rewarmListener);
10095
10106
  document.removeEventListener('visibilitychange', this.rewarmListener);
10107
+ window.removeEventListener('pagehide', this.finalizeFlushListener);
10096
10108
  super.disconnectedCallback();
10097
10109
  }
10098
10110
  willUpdate(changed) {
@@ -10417,6 +10429,13 @@ class SearchWidget extends ElementWithStylesheet {
10417
10429
  };
10418
10430
  (_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.sendEvent) === null || _b === void 0 ? void 0 : _b.call(_a, 'evoSearchSearchIssued', undefined, actionData);
10419
10431
  }
10432
+ // Disarms the settle without counting: whatever was on screen has been
10433
+ // superseded (a new query, a refinement, a failed fetch), so a later teardown
10434
+ // flush must not report it.
10435
+ cancelSettle() {
10436
+ clearTimeout(this.settleTimer);
10437
+ this.settleTimer = undefined;
10438
+ }
10420
10439
  // Finalizes the query whose results are currently on screen. No-op when
10421
10440
  // nothing rendered yet, the last fetch errored (an error count of 0 would
10422
10441
  // mislabel the query as zero-result in the metrics), or the search pane
@@ -10424,7 +10443,7 @@ class SearchWidget extends ElementWithStylesheet {
10424
10443
  // the Ask pane is on screen, and finalizing it from there would double-count
10425
10444
  // a query that was already finalized when the shopper switched tabs.
10426
10445
  finalizeSettledQuery() {
10427
- clearTimeout(this.settleTimer);
10446
+ this.cancelSettle();
10428
10447
  if (this.hasError || !this.debouncedQuery || this.mode !== 'search') {
10429
10448
  return;
10430
10449
  }
@@ -10435,9 +10454,20 @@ class SearchWidget extends ElementWithStylesheet {
10435
10454
  this.markQueryFinalized(this.debouncedQuery, this.searchProducts.length + this.searchCollections.length);
10436
10455
  }
10437
10456
  startSettleTimer() {
10438
- clearTimeout(this.settleTimer);
10457
+ this.cancelSettle();
10439
10458
  this.settleTimer = setTimeout(() => this.finalizeSettledQuery(), SEARCH_SETTLE_MS);
10440
10459
  }
10460
+ // Teardown/navigation flush. Only an ARMED settle counts: a shopper who
10461
+ // clicked a result inside the pause performed the search, but one who had
10462
+ // already typed past it (the timer was disarmed and a newer fetch is in
10463
+ // flight) did not, and `debouncedQuery` still holds the old text. The hook
10464
+ // refuses the same case by dropping its pending record on supersession.
10465
+ flushSettledQuery() {
10466
+ if (this.settleTimer === undefined) {
10467
+ return;
10468
+ }
10469
+ this.finalizeSettledQuery();
10470
+ }
10441
10471
  handleSearchInput(e) {
10442
10472
  var _a, _b, _c;
10443
10473
  const { query } = e.detail;
@@ -10463,7 +10493,7 @@ class SearchWidget extends ElementWithStylesheet {
10463
10493
  if (this.attachedImage) {
10464
10494
  this.loading = true;
10465
10495
  // The refinement supersedes whatever the previous query was settling.
10466
- clearTimeout(this.settleTimer);
10496
+ this.cancelSettle();
10467
10497
  this.finalizeOnRender = false;
10468
10498
  clearTimeout(this.autocompleteTimer);
10469
10499
  this.requestGeneration += 1;
@@ -10504,7 +10534,7 @@ class SearchWidget extends ElementWithStylesheet {
10504
10534
  return;
10505
10535
  }
10506
10536
  // The query changed — whatever was settling is no longer settled.
10507
- clearTimeout(this.settleTimer);
10537
+ this.cancelSettle();
10508
10538
  this.finalizeOnRender = false;
10509
10539
  if (trimmed.length >= MIN_QUERY_LENGTH) {
10510
10540
  if (!this.hasWarmedForInput) {
@@ -10598,7 +10628,7 @@ class SearchWidget extends ElementWithStylesheet {
10598
10628
  this.debouncedQuery = query;
10599
10629
  this.loading = false;
10600
10630
  this.finalizeOnRender = false;
10601
- clearTimeout(this.settleTimer);
10631
+ this.cancelSettle();
10602
10632
  }
10603
10633
  }
10604
10634
  // Ask-mode completions are navigation hints only: populate suggestions
@@ -11272,7 +11302,7 @@ class SearchWidget extends ElementWithStylesheet {
11272
11302
  // The applied query supersedes whatever was settling; without this the
11273
11303
  // old query's settle timer races the new fetch and finalizes
11274
11304
  // nondeterministically.
11275
- clearTimeout(this.settleTimer);
11305
+ this.cancelSettle();
11276
11306
  this.finalizeOnRender = false;
11277
11307
  this.isHandlingSuggestionClick = true;
11278
11308
  this.searchQuery = value;
package/index.js CHANGED
@@ -2097,7 +2097,7 @@ var ProvidersEvoSearchSdk = (function () {
2097
2097
  const tag = ({ data }) => {
2098
2098
  try {
2099
2099
  return {
2100
- sdkVersion: "1.68.2" || '',
2100
+ sdkVersion: "1.69.0" || '',
2101
2101
  searchQuery: data['searchQuery'],
2102
2102
  searchResults: data['searchResults'],
2103
2103
  timestamp: Date.now(),
@@ -2106,7 +2106,7 @@ var ProvidersEvoSearchSdk = (function () {
2106
2106
  catch (err) {
2107
2107
  console.error(`EvoSearch SDK: Tagging failed - ${err}`);
2108
2108
  return {
2109
- sdkVersion: "1.68.2" ,
2109
+ sdkVersion: "1.69.0" ,
2110
2110
  timestamp: Date.now(),
2111
2111
  };
2112
2112
  }
@@ -9871,6 +9871,8 @@ var ProvidersEvoSearchSdk = (function () {
9871
9871
  };
9872
9872
  this.isHandlingSuggestionClick = false;
9873
9873
  this.cachedRailSections = [];
9874
+ // Bound once so connected/disconnected add and remove the SAME reference.
9875
+ this.finalizeFlushListener = () => this.flushSettledQuery();
9874
9876
  // Last finalized query (lowercased) — consecutive duplicates (settle
9875
9877
  // followed by Enter on the same query) collapse into one search.
9876
9878
  this.lastFinalizedQuery = '';
@@ -10036,6 +10038,12 @@ var ProvidersEvoSearchSdk = (function () {
10036
10038
  this.mobileMediaQuery.addEventListener('change', this.mobileMediaListener);
10037
10039
  window.addEventListener('online', this.rewarmListener);
10038
10040
  document.addEventListener('visibilitychange', this.rewarmListener);
10041
+ // A product card is a plain anchor, so a click inside the settle window is
10042
+ // a full navigation that would otherwise kill the pending finalize — and
10043
+ // a fast click-through is the most successful search there is. sendEvent
10044
+ // posts with keepalive, so flushing from pagehide survives the unload;
10045
+ // markQueryFinalized's dedupe keeps a bfcache return from doubling it.
10046
+ window.addEventListener('pagehide', this.finalizeFlushListener);
10039
10047
  // Restart keep-warm when the same instance re-attaches (a host re-append
10040
10048
  // moves the subtree and fires disconnected→connected without a fresh
10041
10049
  // firstUpdated). On the very first connect `api` may not be bound yet —
@@ -10073,8 +10081,11 @@ var ProvidersEvoSearchSdk = (function () {
10073
10081
  }
10074
10082
  disconnectedCallback() {
10075
10083
  var _a, _b, _c, _d, _e;
10076
- // Closing the widget settles whatever query was on screen.
10077
- this.finalizeSettledQuery();
10084
+ // Closing the widget — and a host-driven teardown that removes the overlay
10085
+ // mid-settle — settles whatever query was waiting out its pause, the same
10086
+ // way a navigation does. Guarded, so a query the shopper already typed past
10087
+ // is not counted.
10088
+ this.flushSettledQuery();
10078
10089
  clearTimeout(this.autocompleteTimer);
10079
10090
  clearInterval(this.keepWarmTimer);
10080
10091
  this.keepWarmTimer = undefined;
@@ -10094,6 +10105,7 @@ var ProvidersEvoSearchSdk = (function () {
10094
10105
  (_e = this.mobileMediaQuery) === null || _e === void 0 ? void 0 : _e.removeEventListener('change', this.mobileMediaListener);
10095
10106
  window.removeEventListener('online', this.rewarmListener);
10096
10107
  document.removeEventListener('visibilitychange', this.rewarmListener);
10108
+ window.removeEventListener('pagehide', this.finalizeFlushListener);
10097
10109
  super.disconnectedCallback();
10098
10110
  }
10099
10111
  willUpdate(changed) {
@@ -10418,6 +10430,13 @@ var ProvidersEvoSearchSdk = (function () {
10418
10430
  };
10419
10431
  (_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.sendEvent) === null || _b === void 0 ? void 0 : _b.call(_a, 'evoSearchSearchIssued', undefined, actionData);
10420
10432
  }
10433
+ // Disarms the settle without counting: whatever was on screen has been
10434
+ // superseded (a new query, a refinement, a failed fetch), so a later teardown
10435
+ // flush must not report it.
10436
+ cancelSettle() {
10437
+ clearTimeout(this.settleTimer);
10438
+ this.settleTimer = undefined;
10439
+ }
10421
10440
  // Finalizes the query whose results are currently on screen. No-op when
10422
10441
  // nothing rendered yet, the last fetch errored (an error count of 0 would
10423
10442
  // mislabel the query as zero-result in the metrics), or the search pane
@@ -10425,7 +10444,7 @@ var ProvidersEvoSearchSdk = (function () {
10425
10444
  // the Ask pane is on screen, and finalizing it from there would double-count
10426
10445
  // a query that was already finalized when the shopper switched tabs.
10427
10446
  finalizeSettledQuery() {
10428
- clearTimeout(this.settleTimer);
10447
+ this.cancelSettle();
10429
10448
  if (this.hasError || !this.debouncedQuery || this.mode !== 'search') {
10430
10449
  return;
10431
10450
  }
@@ -10436,9 +10455,20 @@ var ProvidersEvoSearchSdk = (function () {
10436
10455
  this.markQueryFinalized(this.debouncedQuery, this.searchProducts.length + this.searchCollections.length);
10437
10456
  }
10438
10457
  startSettleTimer() {
10439
- clearTimeout(this.settleTimer);
10458
+ this.cancelSettle();
10440
10459
  this.settleTimer = setTimeout(() => this.finalizeSettledQuery(), SEARCH_SETTLE_MS);
10441
10460
  }
10461
+ // Teardown/navigation flush. Only an ARMED settle counts: a shopper who
10462
+ // clicked a result inside the pause performed the search, but one who had
10463
+ // already typed past it (the timer was disarmed and a newer fetch is in
10464
+ // flight) did not, and `debouncedQuery` still holds the old text. The hook
10465
+ // refuses the same case by dropping its pending record on supersession.
10466
+ flushSettledQuery() {
10467
+ if (this.settleTimer === undefined) {
10468
+ return;
10469
+ }
10470
+ this.finalizeSettledQuery();
10471
+ }
10442
10472
  handleSearchInput(e) {
10443
10473
  var _a, _b, _c;
10444
10474
  const { query } = e.detail;
@@ -10464,7 +10494,7 @@ var ProvidersEvoSearchSdk = (function () {
10464
10494
  if (this.attachedImage) {
10465
10495
  this.loading = true;
10466
10496
  // The refinement supersedes whatever the previous query was settling.
10467
- clearTimeout(this.settleTimer);
10497
+ this.cancelSettle();
10468
10498
  this.finalizeOnRender = false;
10469
10499
  clearTimeout(this.autocompleteTimer);
10470
10500
  this.requestGeneration += 1;
@@ -10505,7 +10535,7 @@ var ProvidersEvoSearchSdk = (function () {
10505
10535
  return;
10506
10536
  }
10507
10537
  // The query changed — whatever was settling is no longer settled.
10508
- clearTimeout(this.settleTimer);
10538
+ this.cancelSettle();
10509
10539
  this.finalizeOnRender = false;
10510
10540
  if (trimmed.length >= MIN_QUERY_LENGTH) {
10511
10541
  if (!this.hasWarmedForInput) {
@@ -10599,7 +10629,7 @@ var ProvidersEvoSearchSdk = (function () {
10599
10629
  this.debouncedQuery = query;
10600
10630
  this.loading = false;
10601
10631
  this.finalizeOnRender = false;
10602
- clearTimeout(this.settleTimer);
10632
+ this.cancelSettle();
10603
10633
  }
10604
10634
  }
10605
10635
  // Ask-mode completions are navigation hints only: populate suggestions
@@ -11273,7 +11303,7 @@ var ProvidersEvoSearchSdk = (function () {
11273
11303
  // The applied query supersedes whatever was settling; without this the
11274
11304
  // old query's settle timer races the new fetch and finalizes
11275
11305
  // nondeterministically.
11276
- clearTimeout(this.settleTimer);
11306
+ this.cancelSettle();
11277
11307
  this.finalizeOnRender = false;
11278
11308
  this.isHandlingSuggestionClick = true;
11279
11309
  this.searchQuery = value;
package/index.mjs CHANGED
@@ -2094,7 +2094,7 @@ const init = (params) => {
2094
2094
  const tag = ({ data }) => {
2095
2095
  try {
2096
2096
  return {
2097
- sdkVersion: "1.68.2" || '',
2097
+ sdkVersion: "1.69.0" || '',
2098
2098
  searchQuery: data['searchQuery'],
2099
2099
  searchResults: data['searchResults'],
2100
2100
  timestamp: Date.now(),
@@ -2103,7 +2103,7 @@ const tag = ({ data }) => {
2103
2103
  catch (err) {
2104
2104
  console.error(`EvoSearch SDK: Tagging failed - ${err}`);
2105
2105
  return {
2106
- sdkVersion: "1.68.2" ,
2106
+ sdkVersion: "1.69.0" ,
2107
2107
  timestamp: Date.now(),
2108
2108
  };
2109
2109
  }
@@ -9868,6 +9868,8 @@ class SearchWidget extends ElementWithStylesheet {
9868
9868
  };
9869
9869
  this.isHandlingSuggestionClick = false;
9870
9870
  this.cachedRailSections = [];
9871
+ // Bound once so connected/disconnected add and remove the SAME reference.
9872
+ this.finalizeFlushListener = () => this.flushSettledQuery();
9871
9873
  // Last finalized query (lowercased) — consecutive duplicates (settle
9872
9874
  // followed by Enter on the same query) collapse into one search.
9873
9875
  this.lastFinalizedQuery = '';
@@ -10033,6 +10035,12 @@ class SearchWidget extends ElementWithStylesheet {
10033
10035
  this.mobileMediaQuery.addEventListener('change', this.mobileMediaListener);
10034
10036
  window.addEventListener('online', this.rewarmListener);
10035
10037
  document.addEventListener('visibilitychange', this.rewarmListener);
10038
+ // A product card is a plain anchor, so a click inside the settle window is
10039
+ // a full navigation that would otherwise kill the pending finalize — and
10040
+ // a fast click-through is the most successful search there is. sendEvent
10041
+ // posts with keepalive, so flushing from pagehide survives the unload;
10042
+ // markQueryFinalized's dedupe keeps a bfcache return from doubling it.
10043
+ window.addEventListener('pagehide', this.finalizeFlushListener);
10036
10044
  // Restart keep-warm when the same instance re-attaches (a host re-append
10037
10045
  // moves the subtree and fires disconnected→connected without a fresh
10038
10046
  // firstUpdated). On the very first connect `api` may not be bound yet —
@@ -10070,8 +10078,11 @@ class SearchWidget extends ElementWithStylesheet {
10070
10078
  }
10071
10079
  disconnectedCallback() {
10072
10080
  var _a, _b, _c, _d, _e;
10073
- // Closing the widget settles whatever query was on screen.
10074
- this.finalizeSettledQuery();
10081
+ // Closing the widget — and a host-driven teardown that removes the overlay
10082
+ // mid-settle — settles whatever query was waiting out its pause, the same
10083
+ // way a navigation does. Guarded, so a query the shopper already typed past
10084
+ // is not counted.
10085
+ this.flushSettledQuery();
10075
10086
  clearTimeout(this.autocompleteTimer);
10076
10087
  clearInterval(this.keepWarmTimer);
10077
10088
  this.keepWarmTimer = undefined;
@@ -10091,6 +10102,7 @@ class SearchWidget extends ElementWithStylesheet {
10091
10102
  (_e = this.mobileMediaQuery) === null || _e === void 0 ? void 0 : _e.removeEventListener('change', this.mobileMediaListener);
10092
10103
  window.removeEventListener('online', this.rewarmListener);
10093
10104
  document.removeEventListener('visibilitychange', this.rewarmListener);
10105
+ window.removeEventListener('pagehide', this.finalizeFlushListener);
10094
10106
  super.disconnectedCallback();
10095
10107
  }
10096
10108
  willUpdate(changed) {
@@ -10415,6 +10427,13 @@ class SearchWidget extends ElementWithStylesheet {
10415
10427
  };
10416
10428
  (_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.sendEvent) === null || _b === void 0 ? void 0 : _b.call(_a, 'evoSearchSearchIssued', undefined, actionData);
10417
10429
  }
10430
+ // Disarms the settle without counting: whatever was on screen has been
10431
+ // superseded (a new query, a refinement, a failed fetch), so a later teardown
10432
+ // flush must not report it.
10433
+ cancelSettle() {
10434
+ clearTimeout(this.settleTimer);
10435
+ this.settleTimer = undefined;
10436
+ }
10418
10437
  // Finalizes the query whose results are currently on screen. No-op when
10419
10438
  // nothing rendered yet, the last fetch errored (an error count of 0 would
10420
10439
  // mislabel the query as zero-result in the metrics), or the search pane
@@ -10422,7 +10441,7 @@ class SearchWidget extends ElementWithStylesheet {
10422
10441
  // the Ask pane is on screen, and finalizing it from there would double-count
10423
10442
  // a query that was already finalized when the shopper switched tabs.
10424
10443
  finalizeSettledQuery() {
10425
- clearTimeout(this.settleTimer);
10444
+ this.cancelSettle();
10426
10445
  if (this.hasError || !this.debouncedQuery || this.mode !== 'search') {
10427
10446
  return;
10428
10447
  }
@@ -10433,9 +10452,20 @@ class SearchWidget extends ElementWithStylesheet {
10433
10452
  this.markQueryFinalized(this.debouncedQuery, this.searchProducts.length + this.searchCollections.length);
10434
10453
  }
10435
10454
  startSettleTimer() {
10436
- clearTimeout(this.settleTimer);
10455
+ this.cancelSettle();
10437
10456
  this.settleTimer = setTimeout(() => this.finalizeSettledQuery(), SEARCH_SETTLE_MS);
10438
10457
  }
10458
+ // Teardown/navigation flush. Only an ARMED settle counts: a shopper who
10459
+ // clicked a result inside the pause performed the search, but one who had
10460
+ // already typed past it (the timer was disarmed and a newer fetch is in
10461
+ // flight) did not, and `debouncedQuery` still holds the old text. The hook
10462
+ // refuses the same case by dropping its pending record on supersession.
10463
+ flushSettledQuery() {
10464
+ if (this.settleTimer === undefined) {
10465
+ return;
10466
+ }
10467
+ this.finalizeSettledQuery();
10468
+ }
10439
10469
  handleSearchInput(e) {
10440
10470
  var _a, _b, _c;
10441
10471
  const { query } = e.detail;
@@ -10461,7 +10491,7 @@ class SearchWidget extends ElementWithStylesheet {
10461
10491
  if (this.attachedImage) {
10462
10492
  this.loading = true;
10463
10493
  // The refinement supersedes whatever the previous query was settling.
10464
- clearTimeout(this.settleTimer);
10494
+ this.cancelSettle();
10465
10495
  this.finalizeOnRender = false;
10466
10496
  clearTimeout(this.autocompleteTimer);
10467
10497
  this.requestGeneration += 1;
@@ -10502,7 +10532,7 @@ class SearchWidget extends ElementWithStylesheet {
10502
10532
  return;
10503
10533
  }
10504
10534
  // The query changed — whatever was settling is no longer settled.
10505
- clearTimeout(this.settleTimer);
10535
+ this.cancelSettle();
10506
10536
  this.finalizeOnRender = false;
10507
10537
  if (trimmed.length >= MIN_QUERY_LENGTH) {
10508
10538
  if (!this.hasWarmedForInput) {
@@ -10596,7 +10626,7 @@ class SearchWidget extends ElementWithStylesheet {
10596
10626
  this.debouncedQuery = query;
10597
10627
  this.loading = false;
10598
10628
  this.finalizeOnRender = false;
10599
- clearTimeout(this.settleTimer);
10629
+ this.cancelSettle();
10600
10630
  }
10601
10631
  }
10602
10632
  // Ask-mode completions are navigation hints only: populate suggestions
@@ -11270,7 +11300,7 @@ class SearchWidget extends ElementWithStylesheet {
11270
11300
  // The applied query supersedes whatever was settling; without this the
11271
11301
  // old query's settle timer races the new fetch and finalizes
11272
11302
  // nondeterministically.
11273
- clearTimeout(this.settleTimer);
11303
+ this.cancelSettle();
11274
11304
  this.finalizeOnRender = false;
11275
11305
  this.isHandlingSuggestionClick = true;
11276
11306
  this.searchQuery = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotoutio/providers-evo-search-sdk",
3
- "version": "1.68.2",
3
+ "version": "1.69.0",
4
4
  "description": "Evo Search SDK for EdgeTag",
5
5
  "author": "Blotout",
6
6
  "license": "MIT",