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