@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.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/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/constants.d.ts +28 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +17 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/cookieSanitizer.d.ts +87 -0
- package/dist/utils/cookieSanitizer.d.ts.map +1 -0
- package/dist/utils/cookieSanitizer.js +136 -0
- package/dist/utils/cookieSanitizer.js.map +1 -0
- package/dist/utils/operationRouter.d.ts +25 -0
- package/dist/utils/operationRouter.d.ts.map +1 -0
- package/dist/utils/operationRouter.js +120 -0
- package/dist/utils/operationRouter.js.map +1 -0
- package/dist/utils/types.d.ts +944 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +25 -0
- package/dist/utils/types.js.map +1 -0
- package/dist/vtexClient.d.ts +173 -0
- package/dist/vtexClient.d.ts.map +1 -0
- package/dist/vtexClient.js +242 -0
- package/dist/vtexClient.js.map +1 -0
- package/package.json +16 -57
- package/src/index.ts +11 -29
- package/src/utils/constants.ts +2 -56
- package/src/utils/cookieSanitizer.ts +59 -59
- package/src/utils/operationRouter.ts +75 -65
- package/src/utils/types.ts +709 -1557
- package/src/vtexClient.ts +358 -347
- package/src/README.md +0 -6
- package/src/__conformance__/upstream.test.ts +0 -158
- package/src/__tests__/client-segment-cookie.test.ts +0 -255
- package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
- package/src/__tests__/schemas.test.ts +0 -71
- package/src/actions/address.ts +0 -250
- package/src/actions/analytics/sendEvent.ts +0 -86
- package/src/actions/auth.ts +0 -333
- package/src/actions/checkout.ts +0 -659
- package/src/actions/index.ts +0 -11
- package/src/actions/masterData.ts +0 -168
- package/src/actions/misc.ts +0 -188
- package/src/actions/newsletter.ts +0 -105
- package/src/actions/orders.ts +0 -34
- package/src/actions/profile.ts +0 -201
- package/src/actions/session.ts +0 -85
- package/src/actions/trigger.ts +0 -43
- package/src/actions/wishlist.ts +0 -114
- package/src/client.ts +0 -724
- package/src/commerceLoaders.ts +0 -267
- package/src/hooks/__tests__/createCart.render.test.ts +0 -114
- package/src/hooks/__tests__/createCart.test.ts +0 -92
- package/src/hooks/__tests__/createUseCart.test.ts +0 -184
- package/src/hooks/__tests__/createUseUser.test.ts +0 -48
- package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
- package/src/hooks/cartQuery.ts +0 -204
- package/src/hooks/createCart.ts +0 -427
- package/src/hooks/createUseCart.ts +0 -360
- package/src/hooks/createUseUser.ts +0 -153
- package/src/hooks/createUseWishlist.ts +0 -242
- package/src/hooks/index.ts +0 -25
- package/src/hooks/useAutocomplete.ts +0 -83
- package/src/hooks/useCart.ts +0 -243
- package/src/hooks/useUser.ts +0 -78
- package/src/hooks/useWishlist.ts +0 -119
- package/src/invoke.ts +0 -235
- package/src/loaders/ProductDetailsPage.ts +0 -1
- package/src/loaders/ProductList.ts +0 -1
- package/src/loaders/ProductListingPage.ts +0 -1
- package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
- package/src/loaders/address.ts +0 -115
- package/src/loaders/autocomplete.ts +0 -56
- package/src/loaders/brands.ts +0 -44
- package/src/loaders/cart/attachments.ts +0 -53
- package/src/loaders/cart/full.ts +0 -67
- package/src/loaders/cart/gifts.ts +0 -43
- package/src/loaders/cart/orderFormId.ts +0 -31
- package/src/loaders/cart/shipping.test.ts +0 -125
- package/src/loaders/cart/shipping.ts +0 -136
- package/src/loaders/cart/summary.ts +0 -36
- package/src/loaders/cart.ts +0 -52
- package/src/loaders/catalog.ts +0 -163
- package/src/loaders/collections.ts +0 -55
- package/src/loaders/index.ts +0 -20
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
- package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
- package/src/loaders/intelligentSearch/productList.ts +0 -170
- package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
- package/src/loaders/intelligentSearch/suggestions.ts +0 -44
- package/src/loaders/legacy/productDetailsPage.ts +0 -1
- package/src/loaders/legacy/productList.ts +0 -1
- package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
- package/src/loaders/legacy.ts +0 -710
- package/src/loaders/logistics.ts +0 -111
- package/src/loaders/minicart.ts +0 -78
- package/src/loaders/navbar.ts +0 -26
- package/src/loaders/orders.ts +0 -92
- package/src/loaders/pageType.ts +0 -68
- package/src/loaders/paths/PDPDefaultPath.ts +0 -51
- package/src/loaders/payment.ts +0 -97
- package/src/loaders/productListFull.ts +0 -157
- package/src/loaders/profile.ts +0 -138
- package/src/loaders/promotion.ts +0 -30
- package/src/loaders/search.ts +0 -124
- package/src/loaders/session.ts +0 -83
- package/src/loaders/user.ts +0 -87
- package/src/loaders/wishlist.ts +0 -89
- package/src/loaders/wishlistProducts.ts +0 -69
- package/src/loaders/workflow/products.ts +0 -62
- package/src/loaders/workflow.ts +0 -316
- package/src/logo.png +0 -0
- package/src/manifest.gen.ts +0 -89
- package/src/middleware.cacheHeaders.test.ts +0 -44
- package/src/middleware.ts +0 -240
- package/src/mod.ts +0 -173
- package/src/registry.ts +0 -9
- package/src/schemas.gen.ts +0 -3191
- package/src/schemas.ts +0 -30
- package/src/types.ts +0 -248
- package/src/utils/__tests__/cartProjection.test.ts +0 -118
- package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
- package/src/utils/__tests__/fetch.test.ts +0 -80
- package/src/utils/__tests__/fetchCache.test.ts +0 -155
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
- package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
- package/src/utils/__tests__/minicart.test.ts +0 -184
- package/src/utils/__tests__/operationRouter.test.ts +0 -227
- package/src/utils/__tests__/resilience.test.ts +0 -205
- package/src/utils/__tests__/resourceRange.test.ts +0 -32
- package/src/utils/__tests__/simulationCache.test.ts +0 -40
- package/src/utils/__tests__/sitemap.test.ts +0 -185
- package/src/utils/__tests__/slugify.test.ts +0 -31
- package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
- package/src/utils/__tests__/transform.test.ts +0 -1111
- package/src/utils/accountLoaders.ts +0 -203
- package/src/utils/authHelpers.ts +0 -85
- package/src/utils/batch.ts +0 -18
- package/src/utils/buildOfferShelf.test.ts +0 -59
- package/src/utils/cartProjection.ts +0 -96
- package/src/utils/cookies.ts +0 -167
- package/src/utils/enrichment.ts +0 -556
- package/src/utils/fetch.ts +0 -111
- package/src/utils/fetchCache.ts +0 -55
- package/src/utils/index.ts +0 -19
- package/src/utils/instrumentedFetch.ts +0 -98
- package/src/utils/intelligentSearch.ts +0 -96
- package/src/utils/legacy.ts +0 -139
- package/src/utils/minicart.ts +0 -139
- package/src/utils/pickAndOmit.ts +0 -25
- package/src/utils/proxy.ts +0 -495
- package/src/utils/resilience.ts +0 -352
- package/src/utils/resourceRange.ts +0 -10
- package/src/utils/segment.ts +0 -152
- package/src/utils/similars.ts +0 -35
- package/src/utils/simulationCache.ts +0 -73
- package/src/utils/sitemap.ts +0 -283
- package/src/utils/slugCache.ts +0 -28
- package/src/utils/slugify.ts +0 -13
- package/src/utils/transform.ts +0 -1691
- package/src/utils/vtexId.ts +0 -151
- package/src/vtexClient.test.ts +0 -204
- package/tsconfig.json +0 -7
package/src/utils/resilience.ts
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VTEX fetch resilience — real request abort, circuit breaker, and a bounded
|
|
3
|
-
* retry budget for the whole VTEX egress surface.
|
|
4
|
-
*
|
|
5
|
-
* Wraps a `typeof fetch` and is composed into `createVtexFetch` UNDER the
|
|
6
|
-
* instrumentation layer, so it covers 100% of VTEX traffic — cached GETs,
|
|
7
|
-
* Intelligent Search, and the checkout/cart/session POSTs — with zero
|
|
8
|
-
* call-site changes.
|
|
9
|
-
*
|
|
10
|
-
* Motivation: on a VTEX upstream slowdown, the previous stack cascaded into
|
|
11
|
-
* client-abort 499s (the SSR hangs on a socket that is never freed — the old
|
|
12
|
-
* `fetchCache` timeout was a `Promise.race` that only frees the dedup slot, not
|
|
13
|
-
* the connection) and, on the checkout POST path (which had no timeout/retry at
|
|
14
|
-
* all), hard 500s — while a retry storm amplified load on an already-struggling
|
|
15
|
-
* upstream.
|
|
16
|
-
*
|
|
17
|
-
* Three composed layers, per call:
|
|
18
|
-
* 1. AbortController with a real per-attempt timeout, bounded by a total cap
|
|
19
|
-
* across retries. Frees the TCP connection on VTEX slowness. The caller's
|
|
20
|
-
* own `signal` (e.g. an SSR deadline) is unioned in, so a request-level
|
|
21
|
-
* abort also cancels the in-flight VTEX call.
|
|
22
|
-
* 2. Retry for IDEMPOTENT methods only (GET/HEAD, never a request with a
|
|
23
|
-
* body — a checkout POST must never be retried) on network errors and
|
|
24
|
-
* abort-by-timeout, with exponential backoff + jitter and a per-host retry
|
|
25
|
-
* budget (token bucket) so a broad outage can't become a self-inflicted
|
|
26
|
-
* DDoS. Retrying a 5xx *status* is intentionally left to the SWR
|
|
27
|
-
* `fetchCache` layer to avoid double-retry; this layer still counts a 5xx
|
|
28
|
-
* response as a breaker failure.
|
|
29
|
-
* 3. A circuit breaker per host — opens on consecutive failures, fails fast
|
|
30
|
-
* (no socket, no retry) with {@link VtexCircuitOpenError} while open, and
|
|
31
|
-
* half-opens a bounded number of probes after a cooldown. State is
|
|
32
|
-
* per-isolate: on Workers each isolate self-protects, no shared
|
|
33
|
-
* coordination needed.
|
|
34
|
-
*
|
|
35
|
-
* Kill-switch: set `VTEX_RESILIENCE_DISABLED=true` to fall straight through to
|
|
36
|
-
* the underlying fetch (emergency escape hatch — an env flip, no code deploy).
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
import { RequestContext } from "@decocms/blocks/sdk/requestContext";
|
|
40
|
-
import { DEFAULT_RESILIENCE_CONFIG, type ResilienceConfig } from "./constants";
|
|
41
|
-
|
|
42
|
-
// Re-exported so existing consumers (e.g. `instrumentedFetch.ts`'s
|
|
43
|
-
// `import { type ResilienceConfig } from "./resilience"`) keep working
|
|
44
|
-
// unchanged. The canonical definitions live in `constants.ts` alongside every
|
|
45
|
-
// other resilience/cache tuning knob — see that file to tune or audit them.
|
|
46
|
-
export { DEFAULT_RESILIENCE_CONFIG, type ResilienceConfig };
|
|
47
|
-
|
|
48
|
-
// --- Errors ---------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
export class VtexCircuitOpenError extends Error {
|
|
51
|
-
readonly host: string;
|
|
52
|
-
readonly isVtexResilience = true as const;
|
|
53
|
-
constructor(host: string) {
|
|
54
|
-
super(`VTEX circuit open for ${host} — failing fast`);
|
|
55
|
-
this.name = "VtexCircuitOpenError";
|
|
56
|
-
this.host = host;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export class VtexTimeoutError extends Error {
|
|
61
|
-
readonly host: string;
|
|
62
|
-
readonly isVtexResilience = true as const;
|
|
63
|
-
constructor(host: string, ms: number) {
|
|
64
|
-
super(`VTEX request to ${host} aborted after ${ms}ms`);
|
|
65
|
-
this.name = "VtexTimeoutError";
|
|
66
|
-
this.host = host;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* True for errors that the SWR `fetchCache` layer must NOT retry — the breaker
|
|
72
|
-
* already decided to shed load, and a timed-out request will just time out
|
|
73
|
-
* again. Retrying these would defeat the fast-fail / socket-freeing behavior.
|
|
74
|
-
*/
|
|
75
|
-
export function isNonRetryableVtexError(error: unknown): boolean {
|
|
76
|
-
return error instanceof VtexCircuitOpenError || error instanceof VtexTimeoutError;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// --- Kill switch ----------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
function isDisabled(): boolean {
|
|
82
|
-
try {
|
|
83
|
-
return globalThis.process?.env?.VTEX_RESILIENCE_DISABLED === "true";
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// --- Circuit breaker (per host) ------------------------------------------
|
|
90
|
-
|
|
91
|
-
type BreakerState = "closed" | "open" | "half-open";
|
|
92
|
-
|
|
93
|
-
interface Breaker {
|
|
94
|
-
state: BreakerState;
|
|
95
|
-
consecutiveFailures: number;
|
|
96
|
-
openedAt: number;
|
|
97
|
-
halfOpenInFlight: number;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const breakers = new Map<string, Breaker>();
|
|
101
|
-
|
|
102
|
-
function getBreaker(host: string): Breaker {
|
|
103
|
-
let b = breakers.get(host);
|
|
104
|
-
if (!b) {
|
|
105
|
-
b = { state: "closed", consecutiveFailures: 0, openedAt: 0, halfOpenInFlight: 0 };
|
|
106
|
-
breakers.set(host, b);
|
|
107
|
-
}
|
|
108
|
-
return b;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function breakerAllows(b: Breaker, cfg: ResilienceConfig, now: number): boolean {
|
|
112
|
-
if (b.state === "closed") return true;
|
|
113
|
-
if (b.state === "open") {
|
|
114
|
-
if (now - b.openedAt >= cfg.breakerOpenCooldownMs) {
|
|
115
|
-
b.state = "half-open";
|
|
116
|
-
b.halfOpenInFlight = 0;
|
|
117
|
-
} else {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (b.halfOpenInFlight >= cfg.breakerHalfOpenProbes) return false;
|
|
122
|
-
b.halfOpenInFlight++;
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function breakerOnSuccess(b: Breaker) {
|
|
127
|
-
const wasOpen = b.state !== "closed";
|
|
128
|
-
b.consecutiveFailures = 0;
|
|
129
|
-
b.halfOpenInFlight = Math.max(0, b.halfOpenInFlight - 1);
|
|
130
|
-
b.state = "closed";
|
|
131
|
-
b.openedAt = 0;
|
|
132
|
-
if (wasOpen) console.warn("[vtex-resilience] circuit CLOSED (recovered)");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function breakerOnFailure(b: Breaker, cfg: ResilienceConfig, host: string, now: number) {
|
|
136
|
-
b.halfOpenInFlight = Math.max(0, b.halfOpenInFlight - 1);
|
|
137
|
-
if (b.state === "half-open") {
|
|
138
|
-
b.state = "open";
|
|
139
|
-
b.openedAt = now;
|
|
140
|
-
console.warn(`[vtex-resilience] circuit RE-OPENED for ${host} (probe failed)`);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
b.consecutiveFailures++;
|
|
144
|
-
if (b.state === "closed" && b.consecutiveFailures >= cfg.breakerConsecutiveFailures) {
|
|
145
|
-
b.state = "open";
|
|
146
|
-
b.openedAt = now;
|
|
147
|
-
console.warn(
|
|
148
|
-
`[vtex-resilience] circuit OPEN for ${host} after ${b.consecutiveFailures} consecutive failures`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- Retry budget (token bucket per host) --------------------------------
|
|
154
|
-
|
|
155
|
-
interface Bucket {
|
|
156
|
-
tokens: number;
|
|
157
|
-
lastRefill: number;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const buckets = new Map<string, Bucket>();
|
|
161
|
-
|
|
162
|
-
function tryTakeRetryToken(host: string, cfg: ResilienceConfig, now: number): boolean {
|
|
163
|
-
let bucket = buckets.get(host);
|
|
164
|
-
if (!bucket) {
|
|
165
|
-
bucket = { tokens: cfg.retryBudgetMax, lastRefill: now };
|
|
166
|
-
buckets.set(host, bucket);
|
|
167
|
-
}
|
|
168
|
-
const elapsedSec = (now - bucket.lastRefill) / 1000;
|
|
169
|
-
if (elapsedSec > 0) {
|
|
170
|
-
bucket.tokens = Math.min(
|
|
171
|
-
cfg.retryBudgetMax,
|
|
172
|
-
bucket.tokens + elapsedSec * cfg.retryBudgetRefillPerSec,
|
|
173
|
-
);
|
|
174
|
-
bucket.lastRefill = now;
|
|
175
|
-
}
|
|
176
|
-
if (bucket.tokens >= 1) {
|
|
177
|
-
bucket.tokens -= 1;
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Reset all breaker + budget state. Test-only. */
|
|
184
|
-
export function resetResilienceState() {
|
|
185
|
-
breakers.clear();
|
|
186
|
-
buckets.clear();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// --- Helpers --------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
function hostOf(input: RequestInfo | URL): string {
|
|
192
|
-
try {
|
|
193
|
-
if (typeof input === "string") return new URL(input).host;
|
|
194
|
-
if (input instanceof URL) return input.host;
|
|
195
|
-
if (input instanceof Request) return new URL(input.url).host;
|
|
196
|
-
} catch {
|
|
197
|
-
/* fall through */
|
|
198
|
-
}
|
|
199
|
-
return "unknown";
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function methodOf(input: RequestInfo | URL, init?: RequestInit): string {
|
|
203
|
-
const m = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
204
|
-
return m.toUpperCase();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Only GET/HEAD without a body are safe to retry. Never retry a mutation. */
|
|
208
|
-
function isIdempotent(input: RequestInfo | URL, init?: RequestInit): boolean {
|
|
209
|
-
const method = methodOf(input, init);
|
|
210
|
-
if (method !== "GET" && method !== "HEAD") return false;
|
|
211
|
-
if (init?.body != null) return false;
|
|
212
|
-
if (input instanceof Request && input.body != null) return false;
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function backoffDelay(cfg: ResilienceConfig, attempt: number): number {
|
|
217
|
-
const base = Math.min(cfg.backoffCapMs, cfg.backoffBaseMs * 2 ** attempt);
|
|
218
|
-
// Full jitter, derived from a coarse clock read so we don't depend on
|
|
219
|
-
// Math.random (unavailable in some sandboxes).
|
|
220
|
-
const jitter = (Date.now() % 100) / 100;
|
|
221
|
-
return Math.round(base * (0.5 + 0.5 * jitter));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function sleep(ms: number): Promise<void> {
|
|
225
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** True for a fetch abort (DOMException / Error with name "AbortError"). */
|
|
229
|
-
function isAbortError(err: unknown): boolean {
|
|
230
|
-
return (
|
|
231
|
-
typeof err === "object" && err !== null && (err as { name?: string }).name === "AbortError"
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Union any number of upstream signals with our per-attempt timeout so a caller
|
|
237
|
-
* abort, the ambient request abort (client disconnect / SSR deadline via
|
|
238
|
-
* {@link RequestContext}), OR the timeout all cancel the underlying fetch and
|
|
239
|
-
* free the socket.
|
|
240
|
-
*/
|
|
241
|
-
function withTimeoutSignal(
|
|
242
|
-
callerSignals: Array<AbortSignal | null | undefined>,
|
|
243
|
-
timeoutMs: number,
|
|
244
|
-
) {
|
|
245
|
-
const controller = new AbortController();
|
|
246
|
-
let didTimeout = false;
|
|
247
|
-
const onAbort = () => controller.abort();
|
|
248
|
-
const timer = setTimeout(() => {
|
|
249
|
-
didTimeout = true;
|
|
250
|
-
controller.abort();
|
|
251
|
-
}, timeoutMs);
|
|
252
|
-
const bound: AbortSignal[] = [];
|
|
253
|
-
for (const s of callerSignals) {
|
|
254
|
-
if (!s) continue;
|
|
255
|
-
if (s.aborted) controller.abort();
|
|
256
|
-
else {
|
|
257
|
-
s.addEventListener("abort", onAbort, { once: true });
|
|
258
|
-
bound.push(s);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
signal: controller.signal,
|
|
263
|
-
cleanup: () => {
|
|
264
|
-
clearTimeout(timer);
|
|
265
|
-
for (const s of bound) s.removeEventListener("abort", onAbort);
|
|
266
|
-
},
|
|
267
|
-
timedOut: () => didTimeout,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// --- The resilient fetch --------------------------------------------------
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Wrap a `typeof fetch` with abort/timeout + retry-budget + circuit breaker.
|
|
275
|
-
* Pass the result as the `baseFetch` of {@link createVtexFetch}.
|
|
276
|
-
*/
|
|
277
|
-
export function createResilientFetch(
|
|
278
|
-
underlying: typeof fetch = globalThis.fetch,
|
|
279
|
-
config: Partial<ResilienceConfig> = {},
|
|
280
|
-
): typeof fetch {
|
|
281
|
-
const cfg: ResilienceConfig = { ...DEFAULT_RESILIENCE_CONFIG, ...config };
|
|
282
|
-
|
|
283
|
-
return async function resilientFetch(
|
|
284
|
-
input: RequestInfo | URL,
|
|
285
|
-
init?: RequestInit,
|
|
286
|
-
): Promise<Response> {
|
|
287
|
-
if (isDisabled()) return underlying(input, init);
|
|
288
|
-
|
|
289
|
-
const host = hostOf(input);
|
|
290
|
-
const breaker = getBreaker(host);
|
|
291
|
-
const idempotent = isIdempotent(input, init);
|
|
292
|
-
const startedAt = Date.now();
|
|
293
|
-
|
|
294
|
-
if (!breakerAllows(breaker, cfg, startedAt)) {
|
|
295
|
-
throw new VtexCircuitOpenError(host);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const attempts = idempotent ? cfg.maxRetries + 1 : 1;
|
|
299
|
-
let lastError: unknown;
|
|
300
|
-
|
|
301
|
-
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
302
|
-
const elapsed = Date.now() - startedAt;
|
|
303
|
-
const remaining = cfg.totalTimeoutMs - elapsed;
|
|
304
|
-
if (remaining <= 0) break;
|
|
305
|
-
const perAttempt = Math.min(cfg.perAttemptTimeoutMs, remaining);
|
|
306
|
-
|
|
307
|
-
// Union the caller's signal with the ambient request signal so a client
|
|
308
|
-
// disconnect (or a request-level SSR deadline) also aborts the VTEX call.
|
|
309
|
-
const { signal, cleanup, timedOut } = withTimeoutSignal(
|
|
310
|
-
[init?.signal, RequestContext.current?.signal],
|
|
311
|
-
perAttempt,
|
|
312
|
-
);
|
|
313
|
-
try {
|
|
314
|
-
const response = await underlying(input, { ...init, signal });
|
|
315
|
-
cleanup();
|
|
316
|
-
|
|
317
|
-
// A 5xx counts as a breaker failure even though we return it (the
|
|
318
|
-
// SWR/edge layers decide whether to serve stale). Status retries
|
|
319
|
-
// are owned by the fetchCache layer to avoid double-retry.
|
|
320
|
-
if (response.status >= 500) breakerOnFailure(breaker, cfg, host, Date.now());
|
|
321
|
-
else breakerOnSuccess(breaker);
|
|
322
|
-
return response;
|
|
323
|
-
} catch (err) {
|
|
324
|
-
cleanup();
|
|
325
|
-
|
|
326
|
-
// An abort that did NOT come from our own timeout timer is a caller /
|
|
327
|
-
// ambient abort — a client disconnect or a request-level cancel via
|
|
328
|
-
// RequestContext.signal. That is NOT an upstream failure: it must not
|
|
329
|
-
// count toward the breaker (a burst of users hitting "stop" would
|
|
330
|
-
// otherwise trip the circuit against a perfectly healthy VTEX) and must
|
|
331
|
-
// not be retried (the signal is still aborted). Surface it as-is.
|
|
332
|
-
if (!timedOut() && isAbortError(err)) throw err;
|
|
333
|
-
|
|
334
|
-
lastError = timedOut() ? new VtexTimeoutError(host, perAttempt) : err;
|
|
335
|
-
breakerOnFailure(breaker, cfg, host, Date.now());
|
|
336
|
-
|
|
337
|
-
const canRetry =
|
|
338
|
-
idempotent &&
|
|
339
|
-
attempt < attempts - 1 &&
|
|
340
|
-
Date.now() - startedAt < cfg.totalTimeoutMs &&
|
|
341
|
-
tryTakeRetryToken(host, cfg, Date.now());
|
|
342
|
-
if (canRetry) {
|
|
343
|
-
await sleep(backoffDelay(cfg, attempt));
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
throw lastError;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
throw lastError ?? new VtexTimeoutError(host, cfg.totalTimeoutMs);
|
|
351
|
-
};
|
|
352
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build REST-Range header values for VTEX paginated APIs.
|
|
3
|
-
* Ported from deco-cx/apps vtex/utils/resourceRange.ts
|
|
4
|
-
*/
|
|
5
|
-
export function resourceRange(skip: number, take: number) {
|
|
6
|
-
const from = Math.max(skip, 0);
|
|
7
|
-
const to = from + Math.min(100, take);
|
|
8
|
-
|
|
9
|
-
return { from, to };
|
|
10
|
-
}
|
package/src/utils/segment.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { Segment } from "./types";
|
|
2
|
-
|
|
3
|
-
const removeNonLatin1Chars = (str: string) => str.replace(/[^\x00-\xFF]/g, "");
|
|
4
|
-
|
|
5
|
-
export const SEGMENT_COOKIE_NAME = "vtex_segment";
|
|
6
|
-
export const SALES_CHANNEL_COOKIE = "VTEXSC";
|
|
7
|
-
|
|
8
|
-
export interface WrappedSegment {
|
|
9
|
-
payload: Partial<Segment>;
|
|
10
|
-
token: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const DEFAULT_SEGMENT: Partial<Segment> = {
|
|
14
|
-
utmi_campaign: null,
|
|
15
|
-
utmi_page: null,
|
|
16
|
-
utmi_part: null,
|
|
17
|
-
utm_campaign: null,
|
|
18
|
-
utm_source: null,
|
|
19
|
-
utm_medium: null,
|
|
20
|
-
channel: "1",
|
|
21
|
-
cultureInfo: "pt-BR",
|
|
22
|
-
currencyCode: "BRL",
|
|
23
|
-
currencySymbol: "R$",
|
|
24
|
-
countryCode: "BRA",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Stable serialization.
|
|
29
|
-
*
|
|
30
|
-
* Even if attributes are in a different order, the final segment
|
|
31
|
-
* value will be the same. This improves cache hits.
|
|
32
|
-
*/
|
|
33
|
-
export const serializeSegment = ({
|
|
34
|
-
campaigns,
|
|
35
|
-
channel,
|
|
36
|
-
priceTables,
|
|
37
|
-
regionId,
|
|
38
|
-
utm_campaign,
|
|
39
|
-
utm_source,
|
|
40
|
-
utm_medium,
|
|
41
|
-
utmi_campaign,
|
|
42
|
-
utmi_page,
|
|
43
|
-
utmi_part,
|
|
44
|
-
currencyCode,
|
|
45
|
-
currencySymbol,
|
|
46
|
-
countryCode,
|
|
47
|
-
cultureInfo,
|
|
48
|
-
channelPrivacy,
|
|
49
|
-
}: Partial<Segment>): string => {
|
|
50
|
-
const seg = {
|
|
51
|
-
campaigns,
|
|
52
|
-
channel,
|
|
53
|
-
priceTables,
|
|
54
|
-
regionId,
|
|
55
|
-
utm_campaign: utm_campaign && removeNonLatin1Chars(utm_campaign).replace(/[/[\]{}()<>.]/g, ""),
|
|
56
|
-
utm_source: utm_source && removeNonLatin1Chars(utm_source).replace(/[/[\]{}()<>.]/g, ""),
|
|
57
|
-
utm_medium: utm_medium && removeNonLatin1Chars(utm_medium).replace(/[/[\]{}()<>.]/g, ""),
|
|
58
|
-
utmi_campaign: utmi_campaign && removeNonLatin1Chars(utmi_campaign),
|
|
59
|
-
utmi_page: utmi_page && removeNonLatin1Chars(utmi_page),
|
|
60
|
-
utmi_part: utmi_part && removeNonLatin1Chars(utmi_part),
|
|
61
|
-
currencyCode,
|
|
62
|
-
currencySymbol,
|
|
63
|
-
countryCode,
|
|
64
|
-
cultureInfo,
|
|
65
|
-
channelPrivacy,
|
|
66
|
-
};
|
|
67
|
-
return btoa(JSON.stringify(seg));
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const parseSegment = (cookie: string): Partial<Segment> | null => {
|
|
71
|
-
try {
|
|
72
|
-
return JSON.parse(atob(cookie));
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const SEGMENT_QUERY_PARAMS = [
|
|
79
|
-
"utmi_campaign",
|
|
80
|
-
"utmi_page",
|
|
81
|
-
"utmi_part",
|
|
82
|
-
"utm_campaign",
|
|
83
|
-
"utm_source",
|
|
84
|
-
"utm_medium",
|
|
85
|
-
] as const;
|
|
86
|
-
|
|
87
|
-
export const buildSegmentFromParams = (searchParams: URLSearchParams): Partial<Segment> => {
|
|
88
|
-
const partialSegment: Partial<Segment> = {};
|
|
89
|
-
for (const qs of SEGMENT_QUERY_PARAMS) {
|
|
90
|
-
const param = searchParams.get(qs);
|
|
91
|
-
if (param) {
|
|
92
|
-
partialSegment[qs] = param;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const sc = searchParams.get("sc");
|
|
97
|
-
if (sc) {
|
|
98
|
-
partialSegment.channel = sc;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return partialSegment;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export const withSegmentCookie = (segment: WrappedSegment, headers?: Headers): Headers => {
|
|
105
|
-
const h = new Headers(headers);
|
|
106
|
-
if (!segment) return h;
|
|
107
|
-
|
|
108
|
-
h.set("cookie", `${SEGMENT_COOKIE_NAME}=${segment.token}`);
|
|
109
|
-
return h;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
function getCookieValue(cookieHeader: string, name: string): string | null {
|
|
113
|
-
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
|
|
114
|
-
return match?.[1] ?? null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Build a complete segment from request cookies.
|
|
119
|
-
* Reads both vtex_segment and VTEXSC cookies.
|
|
120
|
-
* VTEXSC contains the sales channel and overrides the segment channel.
|
|
121
|
-
*/
|
|
122
|
-
export const buildSegmentFromCookies = (cookieHeader: string): Partial<Segment> => {
|
|
123
|
-
const segmentCookie = getCookieValue(cookieHeader, SEGMENT_COOKIE_NAME);
|
|
124
|
-
const vtexsc = getCookieValue(cookieHeader, SALES_CHANNEL_COOKIE);
|
|
125
|
-
|
|
126
|
-
const base = segmentCookie ? parseSegment(segmentCookie) : null;
|
|
127
|
-
const segment: Partial<Segment> = { ...DEFAULT_SEGMENT, ...base };
|
|
128
|
-
|
|
129
|
-
if (vtexsc) {
|
|
130
|
-
segment.channel = vtexsc;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return segment;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check if the current segment represents an anonymous user
|
|
138
|
-
* (no campaigns, no UTMs, no regionId, no custom priceTables).
|
|
139
|
-
*/
|
|
140
|
-
export const isAnonymous = (segment: Partial<Segment>): boolean => {
|
|
141
|
-
return (
|
|
142
|
-
!segment.campaigns &&
|
|
143
|
-
!segment.utm_campaign &&
|
|
144
|
-
!segment.utm_source &&
|
|
145
|
-
!segment.utm_medium &&
|
|
146
|
-
!segment.utmi_campaign &&
|
|
147
|
-
!segment.utmi_page &&
|
|
148
|
-
!segment.utmi_part &&
|
|
149
|
-
!segment.regionId &&
|
|
150
|
-
!segment.priceTables
|
|
151
|
-
);
|
|
152
|
-
};
|
package/src/utils/similars.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { Product } from "@decocms/apps-commerce/types";
|
|
2
|
-
import { getVtexConfig, storefrontBaseUrl, vtexFetch } from "../client";
|
|
3
|
-
import { pickSku, toProduct } from "./transform";
|
|
4
|
-
import type { LegacyProduct } from "./types";
|
|
5
|
-
|
|
6
|
-
export const withIsSimilarTo = async (product: Product): Promise<Product> => {
|
|
7
|
-
const id = product.isVariantOf?.productGroupID ?? product.inProductGroupWithID;
|
|
8
|
-
|
|
9
|
-
if (!id) {
|
|
10
|
-
return product;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const rawSimilars = await vtexFetch<LegacyProduct[]>(
|
|
15
|
-
`/api/catalog_system/pub/products/crossselling/similars/${id}`,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
if (!rawSimilars?.length) return product;
|
|
19
|
-
|
|
20
|
-
const config = getVtexConfig();
|
|
21
|
-
const baseUrl = storefrontBaseUrl(config);
|
|
22
|
-
|
|
23
|
-
const similars = rawSimilars.map((p) => {
|
|
24
|
-
const sku = pickSku(p);
|
|
25
|
-
return toProduct(p, sku, 0, { baseUrl, priceCurrency: "BRL" });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
...product,
|
|
30
|
-
isSimilarTo: similars,
|
|
31
|
-
};
|
|
32
|
-
} catch {
|
|
33
|
-
return product;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Injectable cache for shipping simulation responses (#373).
|
|
3
|
-
*
|
|
4
|
-
* `simulateCart`'s POST to `/api/checkout/pub/orderForms/simulation` is not
|
|
5
|
-
* user-personalized for a given `{items, postalCode, salesChannel}` tuple —
|
|
6
|
-
* every shopper with the same cart composition and zip code gets the same
|
|
7
|
-
* SLAs — but it goes through `vtexFetchWithCookies` (cookies can rotate on
|
|
8
|
-
* this endpoint), so caching the raw response risks replaying a stale
|
|
9
|
-
* session's cookies onto a different user.
|
|
10
|
-
*
|
|
11
|
-
* This cache stores ONLY the response body (SLAs/logistics info) — never
|
|
12
|
-
* cookies or session state. The default is an in-process Map (works in both
|
|
13
|
-
* `@decocms/tanstack` and `@decocms/nextjs`, no infra required, but is only
|
|
14
|
-
* warm within a single Worker isolate's lifetime). Sites that want a shared
|
|
15
|
-
* edge cache (Cloudflare Cache API, KV) inject their own implementation via
|
|
16
|
-
* `setSimulationCache` — mirrors the `setFastDeployKVGetter` pattern in
|
|
17
|
-
* `@decocms/blocks-admin/src/admin/decofile.ts`.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export interface SimulationCache {
|
|
21
|
-
get(key: string): Promise<string | null> | string | null;
|
|
22
|
-
put(key: string, value: string, ttlSeconds: number): Promise<void> | void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface InProcessEntry {
|
|
26
|
-
value: string;
|
|
27
|
-
expiresAt: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const DEFAULT_MAX_ENTRIES = 500;
|
|
31
|
-
|
|
32
|
-
function createInProcessCache(): SimulationCache {
|
|
33
|
-
const store = new Map<string, InProcessEntry>();
|
|
34
|
-
|
|
35
|
-
function evictIfNeeded() {
|
|
36
|
-
if (store.size <= DEFAULT_MAX_ENTRIES) return;
|
|
37
|
-
const oldestKey = store.keys().next().value;
|
|
38
|
-
if (oldestKey !== undefined) store.delete(oldestKey);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
get(key) {
|
|
43
|
-
const entry = store.get(key);
|
|
44
|
-
if (!entry) return null;
|
|
45
|
-
if (Date.now() > entry.expiresAt) {
|
|
46
|
-
store.delete(key);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
return entry.value;
|
|
50
|
-
},
|
|
51
|
-
put(key, value, ttlSeconds) {
|
|
52
|
-
store.delete(key); // re-insert for Map's insertion-order-based eviction
|
|
53
|
-
store.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
|
|
54
|
-
evictIfNeeded();
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let cache: SimulationCache = createInProcessCache();
|
|
60
|
-
|
|
61
|
-
/** Inject a custom simulation cache (e.g. Cloudflare Cache API or KV-backed). */
|
|
62
|
-
export function setSimulationCache(custom: SimulationCache): void {
|
|
63
|
-
cache = custom;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getSimulationCache(): SimulationCache {
|
|
67
|
-
return cache;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** @internal exported for tests */
|
|
71
|
-
export function __resetSimulationCacheForTests(): void {
|
|
72
|
-
cache = createInProcessCache();
|
|
73
|
-
}
|