@blotoutio/providers-evo-search-sdk 1.65.1 → 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/README.md +2 -0
- package/core.cjs.js +95 -19
- package/core.js +95 -19
- package/core.mjs +95 -19
- package/hooks.cjs.js +3 -82
- package/hooks.mjs +3 -82
- package/index.cjs.js +126 -30
- package/index.js +126 -30
- package/index.mjs +126 -30
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
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) },
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|