@blotoutio/providers-evo-search-sdk 1.58.0 → 1.58.1
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 +12 -0
- package/core.js +12 -0
- package/core.mjs +12 -0
- package/hooks.d.ts +1 -0
- package/index.cjs.js +85 -13
- package/index.js +85 -13
- package/index.mjs +85 -13
- package/package.json +1 -1
package/core.cjs.js
CHANGED
|
@@ -693,6 +693,17 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
693
693
|
logger.info('EvoSearch API: Fetching ask config');
|
|
694
694
|
return getJSON('/config', 'Config', undefined, signal);
|
|
695
695
|
};
|
|
696
|
+
// Fire-and-forget isolate warmer: tells the worker to prime its search
|
|
697
|
+
// index off the request path so the first keystroke doesn't pay the cold
|
|
698
|
+
// prime. Errors are irrelevant — autocomplete just falls back to priming
|
|
699
|
+
// inline like before.
|
|
700
|
+
const warm = () => {
|
|
701
|
+
fetchImpl(getURL('/warm'), {
|
|
702
|
+
method: 'GET',
|
|
703
|
+
headers: getHeaders(),
|
|
704
|
+
credentials: 'omit',
|
|
705
|
+
}).catch(() => undefined);
|
|
706
|
+
};
|
|
696
707
|
const recents = (signal) => {
|
|
697
708
|
logger.info('EvoSearch API: Fetching recent searches');
|
|
698
709
|
return getJSON('/user/recents', 'Recents', undefined, signal);
|
|
@@ -823,6 +834,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
823
834
|
autocomplete,
|
|
824
835
|
trending,
|
|
825
836
|
config,
|
|
837
|
+
warm,
|
|
826
838
|
user: {
|
|
827
839
|
recents,
|
|
828
840
|
deleteRecent,
|
package/core.js
CHANGED
|
@@ -694,6 +694,17 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
694
694
|
logger.info('EvoSearch API: Fetching ask config');
|
|
695
695
|
return getJSON('/config', 'Config', undefined, signal);
|
|
696
696
|
};
|
|
697
|
+
// Fire-and-forget isolate warmer: tells the worker to prime its search
|
|
698
|
+
// index off the request path so the first keystroke doesn't pay the cold
|
|
699
|
+
// prime. Errors are irrelevant — autocomplete just falls back to priming
|
|
700
|
+
// inline like before.
|
|
701
|
+
const warm = () => {
|
|
702
|
+
fetchImpl(getURL('/warm'), {
|
|
703
|
+
method: 'GET',
|
|
704
|
+
headers: getHeaders(),
|
|
705
|
+
credentials: 'omit',
|
|
706
|
+
}).catch(() => undefined);
|
|
707
|
+
};
|
|
697
708
|
const recents = (signal) => {
|
|
698
709
|
logger.info('EvoSearch API: Fetching recent searches');
|
|
699
710
|
return getJSON('/user/recents', 'Recents', undefined, signal);
|
|
@@ -824,6 +835,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
824
835
|
autocomplete,
|
|
825
836
|
trending,
|
|
826
837
|
config,
|
|
838
|
+
warm,
|
|
827
839
|
user: {
|
|
828
840
|
recents,
|
|
829
841
|
deleteRecent,
|
package/core.mjs
CHANGED
|
@@ -691,6 +691,17 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
691
691
|
logger.info('EvoSearch API: Fetching ask config');
|
|
692
692
|
return getJSON('/config', 'Config', undefined, signal);
|
|
693
693
|
};
|
|
694
|
+
// Fire-and-forget isolate warmer: tells the worker to prime its search
|
|
695
|
+
// index off the request path so the first keystroke doesn't pay the cold
|
|
696
|
+
// prime. Errors are irrelevant — autocomplete just falls back to priming
|
|
697
|
+
// inline like before.
|
|
698
|
+
const warm = () => {
|
|
699
|
+
fetchImpl(getURL('/warm'), {
|
|
700
|
+
method: 'GET',
|
|
701
|
+
headers: getHeaders(),
|
|
702
|
+
credentials: 'omit',
|
|
703
|
+
}).catch(() => undefined);
|
|
704
|
+
};
|
|
694
705
|
const recents = (signal) => {
|
|
695
706
|
logger.info('EvoSearch API: Fetching recent searches');
|
|
696
707
|
return getJSON('/user/recents', 'Recents', undefined, signal);
|
|
@@ -821,6 +832,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
821
832
|
autocomplete,
|
|
822
833
|
trending,
|
|
823
834
|
config,
|
|
835
|
+
warm,
|
|
824
836
|
user: {
|
|
825
837
|
recents,
|
|
826
838
|
deleteRecent,
|
package/hooks.d.ts
CHANGED
|
@@ -145,6 +145,7 @@ declare module '@blotoutio/providers-evo-search-sdk/hooks' {
|
|
|
145
145
|
recommended_products: AutocompleteAPIProduct[]
|
|
146
146
|
}>
|
|
147
147
|
config: (signal?: AbortSignal) => Promise<AskConfigResponse>
|
|
148
|
+
warm: () => void
|
|
148
149
|
user: EvoSearchUserAPI
|
|
149
150
|
askStream: (query: string, signal?: AbortSignal) => AsyncIterable<AskEvent>
|
|
150
151
|
sendEvent: (
|
package/index.cjs.js
CHANGED
|
@@ -829,6 +829,17 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
829
829
|
logger.info('EvoSearch API: Fetching ask config');
|
|
830
830
|
return getJSON('/config', 'Config', undefined, signal);
|
|
831
831
|
};
|
|
832
|
+
// Fire-and-forget isolate warmer: tells the worker to prime its search
|
|
833
|
+
// index off the request path so the first keystroke doesn't pay the cold
|
|
834
|
+
// prime. Errors are irrelevant — autocomplete just falls back to priming
|
|
835
|
+
// inline like before.
|
|
836
|
+
const warm = () => {
|
|
837
|
+
fetchImpl(getURL('/warm'), {
|
|
838
|
+
method: 'GET',
|
|
839
|
+
headers: getHeaders(),
|
|
840
|
+
credentials: 'omit',
|
|
841
|
+
}).catch(() => undefined);
|
|
842
|
+
};
|
|
832
843
|
const recents = (signal) => {
|
|
833
844
|
logger.info('EvoSearch API: Fetching recent searches');
|
|
834
845
|
return getJSON('/user/recents', 'Recents', undefined, signal);
|
|
@@ -959,6 +970,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
959
970
|
autocomplete,
|
|
960
971
|
trending,
|
|
961
972
|
config,
|
|
973
|
+
warm,
|
|
962
974
|
user: {
|
|
963
975
|
recents,
|
|
964
976
|
deleteRecent,
|
|
@@ -8491,6 +8503,9 @@ const searchWidgetStyles = i$5 `
|
|
|
8491
8503
|
`;
|
|
8492
8504
|
|
|
8493
8505
|
const MIN_QUERY_LENGTH = 3;
|
|
8506
|
+
// An aborted/superseded request is expected, not an error.
|
|
8507
|
+
const isCancelled = (error) => error instanceof Error &&
|
|
8508
|
+
(error.message === 'Request cancelled' || error.name === 'AbortError');
|
|
8494
8509
|
// Default "Try asking" prompts shown on the Ask AI empty state when the
|
|
8495
8510
|
// merchant hasn't configured their own via `uiPreferences.askStarters`.
|
|
8496
8511
|
const FALLBACK_ASK_STARTERS = [
|
|
@@ -8520,6 +8535,12 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
8520
8535
|
this.askFollowUps = [];
|
|
8521
8536
|
this.askLoading = false;
|
|
8522
8537
|
this.askAbort = null;
|
|
8538
|
+
// In-flight autocomplete/search request. Each new keystroke aborts the
|
|
8539
|
+
// previous request at the network layer — the `requestGeneration` guard
|
|
8540
|
+
// already prevents stale results from rendering, but without an abort the
|
|
8541
|
+
// dead requests still hold browser connections and worker CPU, delaying
|
|
8542
|
+
// the response the shopper actually wants.
|
|
8543
|
+
this.resultsAbort = null;
|
|
8523
8544
|
this.searchQuery = '';
|
|
8524
8545
|
this.debouncedQuery = '';
|
|
8525
8546
|
this.suggestions = [];
|
|
@@ -8591,6 +8612,12 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
8591
8612
|
get askActive() {
|
|
8592
8613
|
return this.askAiEnabled && !this.askDisabled;
|
|
8593
8614
|
}
|
|
8615
|
+
newResultsSignal() {
|
|
8616
|
+
var _a;
|
|
8617
|
+
(_a = this.resultsAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
8618
|
+
this.resultsAbort = new AbortController();
|
|
8619
|
+
return this.resultsAbort.signal;
|
|
8620
|
+
}
|
|
8594
8621
|
connectedCallback() {
|
|
8595
8622
|
var _a, _b;
|
|
8596
8623
|
super.connectedCallback();
|
|
@@ -8622,13 +8649,14 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
8622
8649
|
this.fetchRecents();
|
|
8623
8650
|
}
|
|
8624
8651
|
disconnectedCallback() {
|
|
8625
|
-
var _a, _b, _c;
|
|
8652
|
+
var _a, _b, _c, _d;
|
|
8626
8653
|
clearTimeout(this.autocompleteTimer);
|
|
8627
8654
|
(_a = this.askAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
8655
|
+
(_b = this.resultsAbort) === null || _b === void 0 ? void 0 : _b.abort();
|
|
8628
8656
|
this.resetAnswerBuffer();
|
|
8629
8657
|
this.removeEventListener('keydown', this.keydownListener);
|
|
8630
|
-
(
|
|
8631
|
-
(
|
|
8658
|
+
(_c = this.collectionsResizeObserver) === null || _c === void 0 ? void 0 : _c.disconnect();
|
|
8659
|
+
(_d = this.mobileMediaQuery) === null || _d === void 0 ? void 0 : _d.removeEventListener('change', this.mobileMediaListener);
|
|
8632
8660
|
super.disconnectedCallback();
|
|
8633
8661
|
}
|
|
8634
8662
|
willUpdate(changed) {
|
|
@@ -9008,6 +9036,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9008
9036
|
].slice(0, MAX_RECENT_SEARCHES);
|
|
9009
9037
|
}
|
|
9010
9038
|
handleSearchInput(e) {
|
|
9039
|
+
var _a;
|
|
9011
9040
|
const { query } = e.detail;
|
|
9012
9041
|
this.searchQuery = query;
|
|
9013
9042
|
this.activeSuggestionIndex = -1;
|
|
@@ -9028,6 +9057,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9028
9057
|
else {
|
|
9029
9058
|
clearTimeout(this.autocompleteTimer);
|
|
9030
9059
|
this.requestGeneration += 1;
|
|
9060
|
+
(_a = this.resultsAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
9031
9061
|
if (this.loading)
|
|
9032
9062
|
this.loading = false;
|
|
9033
9063
|
if (this.debouncedQuery)
|
|
@@ -9060,7 +9090,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9060
9090
|
this.loading = false;
|
|
9061
9091
|
return;
|
|
9062
9092
|
}
|
|
9063
|
-
const response = await this.api.autocomplete(query);
|
|
9093
|
+
const response = await this.api.autocomplete(query, undefined, this.newResultsSignal());
|
|
9064
9094
|
if (generation !== this.requestGeneration) {
|
|
9065
9095
|
return;
|
|
9066
9096
|
}
|
|
@@ -9073,7 +9103,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9073
9103
|
this.loading = false;
|
|
9074
9104
|
}
|
|
9075
9105
|
catch (error) {
|
|
9076
|
-
if (error
|
|
9106
|
+
if (isCancelled(error)) {
|
|
9077
9107
|
return;
|
|
9078
9108
|
}
|
|
9079
9109
|
if (generation !== this.requestGeneration) {
|
|
@@ -9097,7 +9127,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9097
9127
|
if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.search)) {
|
|
9098
9128
|
return this.performAutocomplete(query);
|
|
9099
9129
|
}
|
|
9100
|
-
const response = await this.api.search(query);
|
|
9130
|
+
const response = await this.api.search(query, undefined, this.newResultsSignal());
|
|
9101
9131
|
if (generation !== this.requestGeneration) {
|
|
9102
9132
|
return;
|
|
9103
9133
|
}
|
|
@@ -9108,7 +9138,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9108
9138
|
this.loading = false;
|
|
9109
9139
|
}
|
|
9110
9140
|
catch (error) {
|
|
9111
|
-
if (error
|
|
9141
|
+
if (isCancelled(error)) {
|
|
9112
9142
|
return;
|
|
9113
9143
|
}
|
|
9114
9144
|
if (generation !== this.requestGeneration) {
|
|
@@ -9995,10 +10025,51 @@ let evoSearch;
|
|
|
9995
10025
|
let trendingController;
|
|
9996
10026
|
let trendingFetched = false;
|
|
9997
10027
|
let configController;
|
|
10028
|
+
// Session cache for the trending rails. Storefronts are MPAs: every navigation
|
|
10029
|
+
// resets module state, so without this the trending fetch refires on each page
|
|
10030
|
+
// view and a shopper opening search right after a page load stares at empty
|
|
10031
|
+
// rails until it lands. A fresh entry paints instantly and skips the network
|
|
10032
|
+
// round-trip; a stale one still paints instantly while the normal fetch
|
|
10033
|
+
// revalidates it in the background.
|
|
10034
|
+
const TRENDING_CACHE_KEY = 'evs:trending';
|
|
10035
|
+
const TRENDING_CACHE_TTL_MS = 5 * 60000;
|
|
10036
|
+
const readTrendingCache = () => {
|
|
10037
|
+
try {
|
|
10038
|
+
const raw = window.sessionStorage.getItem(TRENDING_CACHE_KEY);
|
|
10039
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
10040
|
+
return parsed && typeof parsed.at === 'number' && parsed.data
|
|
10041
|
+
? parsed
|
|
10042
|
+
: null;
|
|
10043
|
+
}
|
|
10044
|
+
catch {
|
|
10045
|
+
// Storage access can throw (privacy modes); treat as a cache miss.
|
|
10046
|
+
return null;
|
|
10047
|
+
}
|
|
10048
|
+
};
|
|
10049
|
+
const writeTrendingCache = (data) => {
|
|
10050
|
+
try {
|
|
10051
|
+
window.sessionStorage.setItem(TRENDING_CACHE_KEY, JSON.stringify({ at: Date.now(), data }));
|
|
10052
|
+
}
|
|
10053
|
+
catch {
|
|
10054
|
+
// Best-effort: quota or privacy-mode failures just mean no cache.
|
|
10055
|
+
}
|
|
10056
|
+
};
|
|
10057
|
+
const applyTrending = (target, data) => {
|
|
10058
|
+
target.trendingSearches = data.trending;
|
|
10059
|
+
target.handpickedProducts = data.recommended_products.map(mapAPIProductToSDKProduct);
|
|
10060
|
+
};
|
|
9998
10061
|
const tryFetchTrending = (target) => {
|
|
9999
10062
|
if (!target.api || trendingFetched || trendingController) {
|
|
10000
10063
|
return;
|
|
10001
10064
|
}
|
|
10065
|
+
const cached = readTrendingCache();
|
|
10066
|
+
if (cached) {
|
|
10067
|
+
applyTrending(target, cached.data);
|
|
10068
|
+
if (Date.now() - cached.at < TRENDING_CACHE_TTL_MS) {
|
|
10069
|
+
trendingFetched = true;
|
|
10070
|
+
return;
|
|
10071
|
+
}
|
|
10072
|
+
}
|
|
10002
10073
|
const controller = new AbortController();
|
|
10003
10074
|
trendingController = controller;
|
|
10004
10075
|
target.api
|
|
@@ -10006,12 +10077,8 @@ const tryFetchTrending = (target) => {
|
|
|
10006
10077
|
.then((data) => {
|
|
10007
10078
|
var _a, _b, _c, _d;
|
|
10008
10079
|
trendingFetched = true;
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
}
|
|
10012
|
-
if (data.recommended_products && data.recommended_products.length > 0) {
|
|
10013
|
-
target.handpickedProducts = data.recommended_products.map(mapAPIProductToSDKProduct);
|
|
10014
|
-
}
|
|
10080
|
+
writeTrendingCache(data);
|
|
10081
|
+
applyTrending(target, data);
|
|
10015
10082
|
logger.info(`EvoSearch: Loaded ${(_b = (_a = data.trending) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0} trending, ${(_d = (_c = data.recommended_products) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0} products`);
|
|
10016
10083
|
})
|
|
10017
10084
|
.catch((error) => {
|
|
@@ -10110,6 +10177,7 @@ if (typeof window != 'undefined' && typeof document != 'undefined') {
|
|
|
10110
10177
|
logger.info('EvoSearch destroyed');
|
|
10111
10178
|
},
|
|
10112
10179
|
show() {
|
|
10180
|
+
var _a;
|
|
10113
10181
|
if (!evoSearch) {
|
|
10114
10182
|
logger.error('EvoSearch not initialized');
|
|
10115
10183
|
return;
|
|
@@ -10117,6 +10185,10 @@ if (typeof window != 'undefined' && typeof document != 'undefined') {
|
|
|
10117
10185
|
if (!evoSearch.parentNode) {
|
|
10118
10186
|
document.body.append(evoSearch);
|
|
10119
10187
|
}
|
|
10188
|
+
// Warm the worker's search index off the request path: the prime runs
|
|
10189
|
+
// while the shopper reaches for the keyboard, so the first keystroke's
|
|
10190
|
+
// autocomplete doesn't pay the cold-isolate prime inline.
|
|
10191
|
+
(_a = evoSearch.api) === null || _a === void 0 ? void 0 : _a.warm();
|
|
10120
10192
|
tryFetchTrending(evoSearch);
|
|
10121
10193
|
tryFetchConfig(evoSearch);
|
|
10122
10194
|
evoSearch.open = true;
|
package/index.js
CHANGED
|
@@ -830,6 +830,17 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
830
830
|
logger.info('EvoSearch API: Fetching ask config');
|
|
831
831
|
return getJSON('/config', 'Config', undefined, signal);
|
|
832
832
|
};
|
|
833
|
+
// Fire-and-forget isolate warmer: tells the worker to prime its search
|
|
834
|
+
// index off the request path so the first keystroke doesn't pay the cold
|
|
835
|
+
// prime. Errors are irrelevant — autocomplete just falls back to priming
|
|
836
|
+
// inline like before.
|
|
837
|
+
const warm = () => {
|
|
838
|
+
fetchImpl(getURL('/warm'), {
|
|
839
|
+
method: 'GET',
|
|
840
|
+
headers: getHeaders(),
|
|
841
|
+
credentials: 'omit',
|
|
842
|
+
}).catch(() => undefined);
|
|
843
|
+
};
|
|
833
844
|
const recents = (signal) => {
|
|
834
845
|
logger.info('EvoSearch API: Fetching recent searches');
|
|
835
846
|
return getJSON('/user/recents', 'Recents', undefined, signal);
|
|
@@ -960,6 +971,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
960
971
|
autocomplete,
|
|
961
972
|
trending,
|
|
962
973
|
config,
|
|
974
|
+
warm,
|
|
963
975
|
user: {
|
|
964
976
|
recents,
|
|
965
977
|
deleteRecent,
|
|
@@ -8492,6 +8504,9 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
8492
8504
|
`;
|
|
8493
8505
|
|
|
8494
8506
|
const MIN_QUERY_LENGTH = 3;
|
|
8507
|
+
// An aborted/superseded request is expected, not an error.
|
|
8508
|
+
const isCancelled = (error) => error instanceof Error &&
|
|
8509
|
+
(error.message === 'Request cancelled' || error.name === 'AbortError');
|
|
8495
8510
|
// Default "Try asking" prompts shown on the Ask AI empty state when the
|
|
8496
8511
|
// merchant hasn't configured their own via `uiPreferences.askStarters`.
|
|
8497
8512
|
const FALLBACK_ASK_STARTERS = [
|
|
@@ -8521,6 +8536,12 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
8521
8536
|
this.askFollowUps = [];
|
|
8522
8537
|
this.askLoading = false;
|
|
8523
8538
|
this.askAbort = null;
|
|
8539
|
+
// In-flight autocomplete/search request. Each new keystroke aborts the
|
|
8540
|
+
// previous request at the network layer — the `requestGeneration` guard
|
|
8541
|
+
// already prevents stale results from rendering, but without an abort the
|
|
8542
|
+
// dead requests still hold browser connections and worker CPU, delaying
|
|
8543
|
+
// the response the shopper actually wants.
|
|
8544
|
+
this.resultsAbort = null;
|
|
8524
8545
|
this.searchQuery = '';
|
|
8525
8546
|
this.debouncedQuery = '';
|
|
8526
8547
|
this.suggestions = [];
|
|
@@ -8592,6 +8613,12 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
8592
8613
|
get askActive() {
|
|
8593
8614
|
return this.askAiEnabled && !this.askDisabled;
|
|
8594
8615
|
}
|
|
8616
|
+
newResultsSignal() {
|
|
8617
|
+
var _a;
|
|
8618
|
+
(_a = this.resultsAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
8619
|
+
this.resultsAbort = new AbortController();
|
|
8620
|
+
return this.resultsAbort.signal;
|
|
8621
|
+
}
|
|
8595
8622
|
connectedCallback() {
|
|
8596
8623
|
var _a, _b;
|
|
8597
8624
|
super.connectedCallback();
|
|
@@ -8623,13 +8650,14 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
8623
8650
|
this.fetchRecents();
|
|
8624
8651
|
}
|
|
8625
8652
|
disconnectedCallback() {
|
|
8626
|
-
var _a, _b, _c;
|
|
8653
|
+
var _a, _b, _c, _d;
|
|
8627
8654
|
clearTimeout(this.autocompleteTimer);
|
|
8628
8655
|
(_a = this.askAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
8656
|
+
(_b = this.resultsAbort) === null || _b === void 0 ? void 0 : _b.abort();
|
|
8629
8657
|
this.resetAnswerBuffer();
|
|
8630
8658
|
this.removeEventListener('keydown', this.keydownListener);
|
|
8631
|
-
(
|
|
8632
|
-
(
|
|
8659
|
+
(_c = this.collectionsResizeObserver) === null || _c === void 0 ? void 0 : _c.disconnect();
|
|
8660
|
+
(_d = this.mobileMediaQuery) === null || _d === void 0 ? void 0 : _d.removeEventListener('change', this.mobileMediaListener);
|
|
8633
8661
|
super.disconnectedCallback();
|
|
8634
8662
|
}
|
|
8635
8663
|
willUpdate(changed) {
|
|
@@ -9009,6 +9037,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
9009
9037
|
].slice(0, MAX_RECENT_SEARCHES);
|
|
9010
9038
|
}
|
|
9011
9039
|
handleSearchInput(e) {
|
|
9040
|
+
var _a;
|
|
9012
9041
|
const { query } = e.detail;
|
|
9013
9042
|
this.searchQuery = query;
|
|
9014
9043
|
this.activeSuggestionIndex = -1;
|
|
@@ -9029,6 +9058,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
9029
9058
|
else {
|
|
9030
9059
|
clearTimeout(this.autocompleteTimer);
|
|
9031
9060
|
this.requestGeneration += 1;
|
|
9061
|
+
(_a = this.resultsAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
9032
9062
|
if (this.loading)
|
|
9033
9063
|
this.loading = false;
|
|
9034
9064
|
if (this.debouncedQuery)
|
|
@@ -9061,7 +9091,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
9061
9091
|
this.loading = false;
|
|
9062
9092
|
return;
|
|
9063
9093
|
}
|
|
9064
|
-
const response = await this.api.autocomplete(query);
|
|
9094
|
+
const response = await this.api.autocomplete(query, undefined, this.newResultsSignal());
|
|
9065
9095
|
if (generation !== this.requestGeneration) {
|
|
9066
9096
|
return;
|
|
9067
9097
|
}
|
|
@@ -9074,7 +9104,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
9074
9104
|
this.loading = false;
|
|
9075
9105
|
}
|
|
9076
9106
|
catch (error) {
|
|
9077
|
-
if (error
|
|
9107
|
+
if (isCancelled(error)) {
|
|
9078
9108
|
return;
|
|
9079
9109
|
}
|
|
9080
9110
|
if (generation !== this.requestGeneration) {
|
|
@@ -9098,7 +9128,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
9098
9128
|
if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.search)) {
|
|
9099
9129
|
return this.performAutocomplete(query);
|
|
9100
9130
|
}
|
|
9101
|
-
const response = await this.api.search(query);
|
|
9131
|
+
const response = await this.api.search(query, undefined, this.newResultsSignal());
|
|
9102
9132
|
if (generation !== this.requestGeneration) {
|
|
9103
9133
|
return;
|
|
9104
9134
|
}
|
|
@@ -9109,7 +9139,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
9109
9139
|
this.loading = false;
|
|
9110
9140
|
}
|
|
9111
9141
|
catch (error) {
|
|
9112
|
-
if (error
|
|
9142
|
+
if (isCancelled(error)) {
|
|
9113
9143
|
return;
|
|
9114
9144
|
}
|
|
9115
9145
|
if (generation !== this.requestGeneration) {
|
|
@@ -9996,10 +10026,51 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
9996
10026
|
let trendingController;
|
|
9997
10027
|
let trendingFetched = false;
|
|
9998
10028
|
let configController;
|
|
10029
|
+
// Session cache for the trending rails. Storefronts are MPAs: every navigation
|
|
10030
|
+
// resets module state, so without this the trending fetch refires on each page
|
|
10031
|
+
// view and a shopper opening search right after a page load stares at empty
|
|
10032
|
+
// rails until it lands. A fresh entry paints instantly and skips the network
|
|
10033
|
+
// round-trip; a stale one still paints instantly while the normal fetch
|
|
10034
|
+
// revalidates it in the background.
|
|
10035
|
+
const TRENDING_CACHE_KEY = 'evs:trending';
|
|
10036
|
+
const TRENDING_CACHE_TTL_MS = 5 * 60000;
|
|
10037
|
+
const readTrendingCache = () => {
|
|
10038
|
+
try {
|
|
10039
|
+
const raw = window.sessionStorage.getItem(TRENDING_CACHE_KEY);
|
|
10040
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
10041
|
+
return parsed && typeof parsed.at === 'number' && parsed.data
|
|
10042
|
+
? parsed
|
|
10043
|
+
: null;
|
|
10044
|
+
}
|
|
10045
|
+
catch {
|
|
10046
|
+
// Storage access can throw (privacy modes); treat as a cache miss.
|
|
10047
|
+
return null;
|
|
10048
|
+
}
|
|
10049
|
+
};
|
|
10050
|
+
const writeTrendingCache = (data) => {
|
|
10051
|
+
try {
|
|
10052
|
+
window.sessionStorage.setItem(TRENDING_CACHE_KEY, JSON.stringify({ at: Date.now(), data }));
|
|
10053
|
+
}
|
|
10054
|
+
catch {
|
|
10055
|
+
// Best-effort: quota or privacy-mode failures just mean no cache.
|
|
10056
|
+
}
|
|
10057
|
+
};
|
|
10058
|
+
const applyTrending = (target, data) => {
|
|
10059
|
+
target.trendingSearches = data.trending;
|
|
10060
|
+
target.handpickedProducts = data.recommended_products.map(mapAPIProductToSDKProduct);
|
|
10061
|
+
};
|
|
9999
10062
|
const tryFetchTrending = (target) => {
|
|
10000
10063
|
if (!target.api || trendingFetched || trendingController) {
|
|
10001
10064
|
return;
|
|
10002
10065
|
}
|
|
10066
|
+
const cached = readTrendingCache();
|
|
10067
|
+
if (cached) {
|
|
10068
|
+
applyTrending(target, cached.data);
|
|
10069
|
+
if (Date.now() - cached.at < TRENDING_CACHE_TTL_MS) {
|
|
10070
|
+
trendingFetched = true;
|
|
10071
|
+
return;
|
|
10072
|
+
}
|
|
10073
|
+
}
|
|
10003
10074
|
const controller = new AbortController();
|
|
10004
10075
|
trendingController = controller;
|
|
10005
10076
|
target.api
|
|
@@ -10007,12 +10078,8 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
10007
10078
|
.then((data) => {
|
|
10008
10079
|
var _a, _b, _c, _d;
|
|
10009
10080
|
trendingFetched = true;
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
}
|
|
10013
|
-
if (data.recommended_products && data.recommended_products.length > 0) {
|
|
10014
|
-
target.handpickedProducts = data.recommended_products.map(mapAPIProductToSDKProduct);
|
|
10015
|
-
}
|
|
10081
|
+
writeTrendingCache(data);
|
|
10082
|
+
applyTrending(target, data);
|
|
10016
10083
|
logger.info(`EvoSearch: Loaded ${(_b = (_a = data.trending) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0} trending, ${(_d = (_c = data.recommended_products) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0} products`);
|
|
10017
10084
|
})
|
|
10018
10085
|
.catch((error) => {
|
|
@@ -10111,6 +10178,7 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
10111
10178
|
logger.info('EvoSearch destroyed');
|
|
10112
10179
|
},
|
|
10113
10180
|
show() {
|
|
10181
|
+
var _a;
|
|
10114
10182
|
if (!evoSearch) {
|
|
10115
10183
|
logger.error('EvoSearch not initialized');
|
|
10116
10184
|
return;
|
|
@@ -10118,6 +10186,10 @@ var ProvidersEvoSearchSdk = (function () {
|
|
|
10118
10186
|
if (!evoSearch.parentNode) {
|
|
10119
10187
|
document.body.append(evoSearch);
|
|
10120
10188
|
}
|
|
10189
|
+
// Warm the worker's search index off the request path: the prime runs
|
|
10190
|
+
// while the shopper reaches for the keyboard, so the first keystroke's
|
|
10191
|
+
// autocomplete doesn't pay the cold-isolate prime inline.
|
|
10192
|
+
(_a = evoSearch.api) === null || _a === void 0 ? void 0 : _a.warm();
|
|
10121
10193
|
tryFetchTrending(evoSearch);
|
|
10122
10194
|
tryFetchConfig(evoSearch);
|
|
10123
10195
|
evoSearch.open = true;
|
package/index.mjs
CHANGED
|
@@ -827,6 +827,17 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
827
827
|
logger.info('EvoSearch API: Fetching ask config');
|
|
828
828
|
return getJSON('/config', 'Config', undefined, signal);
|
|
829
829
|
};
|
|
830
|
+
// Fire-and-forget isolate warmer: tells the worker to prime its search
|
|
831
|
+
// index off the request path so the first keystroke doesn't pay the cold
|
|
832
|
+
// prime. Errors are irrelevant — autocomplete just falls back to priming
|
|
833
|
+
// inline like before.
|
|
834
|
+
const warm = () => {
|
|
835
|
+
fetchImpl(getURL('/warm'), {
|
|
836
|
+
method: 'GET',
|
|
837
|
+
headers: getHeaders(),
|
|
838
|
+
credentials: 'omit',
|
|
839
|
+
}).catch(() => undefined);
|
|
840
|
+
};
|
|
830
841
|
const recents = (signal) => {
|
|
831
842
|
logger.info('EvoSearch API: Fetching recent searches');
|
|
832
843
|
return getJSON('/user/recents', 'Recents', undefined, signal);
|
|
@@ -957,6 +968,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
957
968
|
autocomplete,
|
|
958
969
|
trending,
|
|
959
970
|
config,
|
|
971
|
+
warm,
|
|
960
972
|
user: {
|
|
961
973
|
recents,
|
|
962
974
|
deleteRecent,
|
|
@@ -8489,6 +8501,9 @@ const searchWidgetStyles = i$5 `
|
|
|
8489
8501
|
`;
|
|
8490
8502
|
|
|
8491
8503
|
const MIN_QUERY_LENGTH = 3;
|
|
8504
|
+
// An aborted/superseded request is expected, not an error.
|
|
8505
|
+
const isCancelled = (error) => error instanceof Error &&
|
|
8506
|
+
(error.message === 'Request cancelled' || error.name === 'AbortError');
|
|
8492
8507
|
// Default "Try asking" prompts shown on the Ask AI empty state when the
|
|
8493
8508
|
// merchant hasn't configured their own via `uiPreferences.askStarters`.
|
|
8494
8509
|
const FALLBACK_ASK_STARTERS = [
|
|
@@ -8518,6 +8533,12 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
8518
8533
|
this.askFollowUps = [];
|
|
8519
8534
|
this.askLoading = false;
|
|
8520
8535
|
this.askAbort = null;
|
|
8536
|
+
// In-flight autocomplete/search request. Each new keystroke aborts the
|
|
8537
|
+
// previous request at the network layer — the `requestGeneration` guard
|
|
8538
|
+
// already prevents stale results from rendering, but without an abort the
|
|
8539
|
+
// dead requests still hold browser connections and worker CPU, delaying
|
|
8540
|
+
// the response the shopper actually wants.
|
|
8541
|
+
this.resultsAbort = null;
|
|
8521
8542
|
this.searchQuery = '';
|
|
8522
8543
|
this.debouncedQuery = '';
|
|
8523
8544
|
this.suggestions = [];
|
|
@@ -8589,6 +8610,12 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
8589
8610
|
get askActive() {
|
|
8590
8611
|
return this.askAiEnabled && !this.askDisabled;
|
|
8591
8612
|
}
|
|
8613
|
+
newResultsSignal() {
|
|
8614
|
+
var _a;
|
|
8615
|
+
(_a = this.resultsAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
8616
|
+
this.resultsAbort = new AbortController();
|
|
8617
|
+
return this.resultsAbort.signal;
|
|
8618
|
+
}
|
|
8592
8619
|
connectedCallback() {
|
|
8593
8620
|
var _a, _b;
|
|
8594
8621
|
super.connectedCallback();
|
|
@@ -8620,13 +8647,14 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
8620
8647
|
this.fetchRecents();
|
|
8621
8648
|
}
|
|
8622
8649
|
disconnectedCallback() {
|
|
8623
|
-
var _a, _b, _c;
|
|
8650
|
+
var _a, _b, _c, _d;
|
|
8624
8651
|
clearTimeout(this.autocompleteTimer);
|
|
8625
8652
|
(_a = this.askAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
8653
|
+
(_b = this.resultsAbort) === null || _b === void 0 ? void 0 : _b.abort();
|
|
8626
8654
|
this.resetAnswerBuffer();
|
|
8627
8655
|
this.removeEventListener('keydown', this.keydownListener);
|
|
8628
|
-
(
|
|
8629
|
-
(
|
|
8656
|
+
(_c = this.collectionsResizeObserver) === null || _c === void 0 ? void 0 : _c.disconnect();
|
|
8657
|
+
(_d = this.mobileMediaQuery) === null || _d === void 0 ? void 0 : _d.removeEventListener('change', this.mobileMediaListener);
|
|
8630
8658
|
super.disconnectedCallback();
|
|
8631
8659
|
}
|
|
8632
8660
|
willUpdate(changed) {
|
|
@@ -9006,6 +9034,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9006
9034
|
].slice(0, MAX_RECENT_SEARCHES);
|
|
9007
9035
|
}
|
|
9008
9036
|
handleSearchInput(e) {
|
|
9037
|
+
var _a;
|
|
9009
9038
|
const { query } = e.detail;
|
|
9010
9039
|
this.searchQuery = query;
|
|
9011
9040
|
this.activeSuggestionIndex = -1;
|
|
@@ -9026,6 +9055,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9026
9055
|
else {
|
|
9027
9056
|
clearTimeout(this.autocompleteTimer);
|
|
9028
9057
|
this.requestGeneration += 1;
|
|
9058
|
+
(_a = this.resultsAbort) === null || _a === void 0 ? void 0 : _a.abort();
|
|
9029
9059
|
if (this.loading)
|
|
9030
9060
|
this.loading = false;
|
|
9031
9061
|
if (this.debouncedQuery)
|
|
@@ -9058,7 +9088,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9058
9088
|
this.loading = false;
|
|
9059
9089
|
return;
|
|
9060
9090
|
}
|
|
9061
|
-
const response = await this.api.autocomplete(query);
|
|
9091
|
+
const response = await this.api.autocomplete(query, undefined, this.newResultsSignal());
|
|
9062
9092
|
if (generation !== this.requestGeneration) {
|
|
9063
9093
|
return;
|
|
9064
9094
|
}
|
|
@@ -9071,7 +9101,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9071
9101
|
this.loading = false;
|
|
9072
9102
|
}
|
|
9073
9103
|
catch (error) {
|
|
9074
|
-
if (error
|
|
9104
|
+
if (isCancelled(error)) {
|
|
9075
9105
|
return;
|
|
9076
9106
|
}
|
|
9077
9107
|
if (generation !== this.requestGeneration) {
|
|
@@ -9095,7 +9125,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9095
9125
|
if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.search)) {
|
|
9096
9126
|
return this.performAutocomplete(query);
|
|
9097
9127
|
}
|
|
9098
|
-
const response = await this.api.search(query);
|
|
9128
|
+
const response = await this.api.search(query, undefined, this.newResultsSignal());
|
|
9099
9129
|
if (generation !== this.requestGeneration) {
|
|
9100
9130
|
return;
|
|
9101
9131
|
}
|
|
@@ -9106,7 +9136,7 @@ class SearchWidget extends ElementWithStylesheet {
|
|
|
9106
9136
|
this.loading = false;
|
|
9107
9137
|
}
|
|
9108
9138
|
catch (error) {
|
|
9109
|
-
if (error
|
|
9139
|
+
if (isCancelled(error)) {
|
|
9110
9140
|
return;
|
|
9111
9141
|
}
|
|
9112
9142
|
if (generation !== this.requestGeneration) {
|
|
@@ -9993,10 +10023,51 @@ let evoSearch;
|
|
|
9993
10023
|
let trendingController;
|
|
9994
10024
|
let trendingFetched = false;
|
|
9995
10025
|
let configController;
|
|
10026
|
+
// Session cache for the trending rails. Storefronts are MPAs: every navigation
|
|
10027
|
+
// resets module state, so without this the trending fetch refires on each page
|
|
10028
|
+
// view and a shopper opening search right after a page load stares at empty
|
|
10029
|
+
// rails until it lands. A fresh entry paints instantly and skips the network
|
|
10030
|
+
// round-trip; a stale one still paints instantly while the normal fetch
|
|
10031
|
+
// revalidates it in the background.
|
|
10032
|
+
const TRENDING_CACHE_KEY = 'evs:trending';
|
|
10033
|
+
const TRENDING_CACHE_TTL_MS = 5 * 60000;
|
|
10034
|
+
const readTrendingCache = () => {
|
|
10035
|
+
try {
|
|
10036
|
+
const raw = window.sessionStorage.getItem(TRENDING_CACHE_KEY);
|
|
10037
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
10038
|
+
return parsed && typeof parsed.at === 'number' && parsed.data
|
|
10039
|
+
? parsed
|
|
10040
|
+
: null;
|
|
10041
|
+
}
|
|
10042
|
+
catch {
|
|
10043
|
+
// Storage access can throw (privacy modes); treat as a cache miss.
|
|
10044
|
+
return null;
|
|
10045
|
+
}
|
|
10046
|
+
};
|
|
10047
|
+
const writeTrendingCache = (data) => {
|
|
10048
|
+
try {
|
|
10049
|
+
window.sessionStorage.setItem(TRENDING_CACHE_KEY, JSON.stringify({ at: Date.now(), data }));
|
|
10050
|
+
}
|
|
10051
|
+
catch {
|
|
10052
|
+
// Best-effort: quota or privacy-mode failures just mean no cache.
|
|
10053
|
+
}
|
|
10054
|
+
};
|
|
10055
|
+
const applyTrending = (target, data) => {
|
|
10056
|
+
target.trendingSearches = data.trending;
|
|
10057
|
+
target.handpickedProducts = data.recommended_products.map(mapAPIProductToSDKProduct);
|
|
10058
|
+
};
|
|
9996
10059
|
const tryFetchTrending = (target) => {
|
|
9997
10060
|
if (!target.api || trendingFetched || trendingController) {
|
|
9998
10061
|
return;
|
|
9999
10062
|
}
|
|
10063
|
+
const cached = readTrendingCache();
|
|
10064
|
+
if (cached) {
|
|
10065
|
+
applyTrending(target, cached.data);
|
|
10066
|
+
if (Date.now() - cached.at < TRENDING_CACHE_TTL_MS) {
|
|
10067
|
+
trendingFetched = true;
|
|
10068
|
+
return;
|
|
10069
|
+
}
|
|
10070
|
+
}
|
|
10000
10071
|
const controller = new AbortController();
|
|
10001
10072
|
trendingController = controller;
|
|
10002
10073
|
target.api
|
|
@@ -10004,12 +10075,8 @@ const tryFetchTrending = (target) => {
|
|
|
10004
10075
|
.then((data) => {
|
|
10005
10076
|
var _a, _b, _c, _d;
|
|
10006
10077
|
trendingFetched = true;
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
}
|
|
10010
|
-
if (data.recommended_products && data.recommended_products.length > 0) {
|
|
10011
|
-
target.handpickedProducts = data.recommended_products.map(mapAPIProductToSDKProduct);
|
|
10012
|
-
}
|
|
10078
|
+
writeTrendingCache(data);
|
|
10079
|
+
applyTrending(target, data);
|
|
10013
10080
|
logger.info(`EvoSearch: Loaded ${(_b = (_a = data.trending) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0} trending, ${(_d = (_c = data.recommended_products) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0} products`);
|
|
10014
10081
|
})
|
|
10015
10082
|
.catch((error) => {
|
|
@@ -10108,6 +10175,7 @@ if (typeof window != 'undefined' && typeof document != 'undefined') {
|
|
|
10108
10175
|
logger.info('EvoSearch destroyed');
|
|
10109
10176
|
},
|
|
10110
10177
|
show() {
|
|
10178
|
+
var _a;
|
|
10111
10179
|
if (!evoSearch) {
|
|
10112
10180
|
logger.error('EvoSearch not initialized');
|
|
10113
10181
|
return;
|
|
@@ -10115,6 +10183,10 @@ if (typeof window != 'undefined' && typeof document != 'undefined') {
|
|
|
10115
10183
|
if (!evoSearch.parentNode) {
|
|
10116
10184
|
document.body.append(evoSearch);
|
|
10117
10185
|
}
|
|
10186
|
+
// Warm the worker's search index off the request path: the prime runs
|
|
10187
|
+
// while the shopper reaches for the keyboard, so the first keystroke's
|
|
10188
|
+
// autocomplete doesn't pay the cold-isolate prime inline.
|
|
10189
|
+
(_a = evoSearch.api) === null || _a === void 0 ? void 0 : _a.warm();
|
|
10118
10190
|
tryFetchTrending(evoSearch);
|
|
10119
10191
|
tryFetchConfig(evoSearch);
|
|
10120
10192
|
evoSearch.open = true;
|