@decocms/apps-vtex 8.0.0 → 8.1.0-next.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.
Files changed (133) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +12 -15
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +76 -72
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +444 -0
  8. package/src/README.md +0 -6
  9. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  10. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  11. package/src/__tests__/schemas.test.ts +0 -71
  12. package/src/actions/address.ts +0 -250
  13. package/src/actions/analytics/sendEvent.ts +0 -86
  14. package/src/actions/auth.ts +0 -333
  15. package/src/actions/checkout.ts +0 -659
  16. package/src/actions/index.ts +0 -11
  17. package/src/actions/masterData.ts +0 -168
  18. package/src/actions/misc.ts +0 -188
  19. package/src/actions/newsletter.ts +0 -105
  20. package/src/actions/orders.ts +0 -34
  21. package/src/actions/profile.ts +0 -201
  22. package/src/actions/session.ts +0 -85
  23. package/src/actions/trigger.ts +0 -43
  24. package/src/actions/wishlist.ts +0 -114
  25. package/src/client.ts +0 -667
  26. package/src/commerceLoaders.ts +0 -267
  27. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  28. package/src/hooks/__tests__/createCart.test.ts +0 -92
  29. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  30. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  31. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  32. package/src/hooks/cartQuery.ts +0 -204
  33. package/src/hooks/createCart.ts +0 -427
  34. package/src/hooks/createUseCart.ts +0 -360
  35. package/src/hooks/createUseUser.ts +0 -153
  36. package/src/hooks/createUseWishlist.ts +0 -242
  37. package/src/hooks/index.ts +0 -25
  38. package/src/hooks/useAutocomplete.ts +0 -83
  39. package/src/hooks/useCart.ts +0 -243
  40. package/src/hooks/useUser.ts +0 -78
  41. package/src/hooks/useWishlist.ts +0 -119
  42. package/src/invoke.ts +0 -235
  43. package/src/loaders/ProductDetailsPage.ts +0 -1
  44. package/src/loaders/ProductList.ts +0 -1
  45. package/src/loaders/ProductListingPage.ts +0 -1
  46. package/src/loaders/address.ts +0 -115
  47. package/src/loaders/autocomplete.ts +0 -58
  48. package/src/loaders/brands.ts +0 -44
  49. package/src/loaders/cart/attachments.ts +0 -53
  50. package/src/loaders/cart/full.ts +0 -67
  51. package/src/loaders/cart/gifts.ts +0 -43
  52. package/src/loaders/cart/orderFormId.ts +0 -31
  53. package/src/loaders/cart/shipping.test.ts +0 -125
  54. package/src/loaders/cart/shipping.ts +0 -136
  55. package/src/loaders/cart/summary.ts +0 -36
  56. package/src/loaders/cart.ts +0 -52
  57. package/src/loaders/catalog.ts +0 -163
  58. package/src/loaders/collections.ts +0 -55
  59. package/src/loaders/index.ts +0 -20
  60. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  61. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -113
  62. package/src/loaders/intelligentSearch/productList.ts +0 -154
  63. package/src/loaders/intelligentSearch/productListingPage.ts +0 -526
  64. package/src/loaders/intelligentSearch/suggestions.ts +0 -46
  65. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  66. package/src/loaders/legacy/productList.ts +0 -1
  67. package/src/loaders/legacy/relatedProductsLoader.ts +0 -81
  68. package/src/loaders/legacy.ts +0 -635
  69. package/src/loaders/logistics.ts +0 -111
  70. package/src/loaders/minicart.ts +0 -78
  71. package/src/loaders/navbar.ts +0 -26
  72. package/src/loaders/orders.ts +0 -92
  73. package/src/loaders/pageType.ts +0 -68
  74. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  75. package/src/loaders/payment.ts +0 -97
  76. package/src/loaders/productListFull.ts +0 -159
  77. package/src/loaders/profile.ts +0 -138
  78. package/src/loaders/promotion.ts +0 -30
  79. package/src/loaders/search.ts +0 -124
  80. package/src/loaders/session.ts +0 -83
  81. package/src/loaders/user.ts +0 -87
  82. package/src/loaders/wishlist.ts +0 -89
  83. package/src/loaders/wishlistProducts.ts +0 -69
  84. package/src/loaders/workflow/products.ts +0 -64
  85. package/src/loaders/workflow.ts +0 -316
  86. package/src/logo.png +0 -0
  87. package/src/manifest.gen.ts +0 -89
  88. package/src/middleware.ts +0 -240
  89. package/src/mod.ts +0 -157
  90. package/src/registry.ts +0 -9
  91. package/src/schemas.gen.ts +0 -3191
  92. package/src/schemas.ts +0 -30
  93. package/src/types.ts +0 -248
  94. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  95. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  96. package/src/utils/__tests__/fetch.test.ts +0 -80
  97. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  98. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  99. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  100. package/src/utils/__tests__/minicart.test.ts +0 -184
  101. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  102. package/src/utils/__tests__/resilience.test.ts +0 -205
  103. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  104. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  105. package/src/utils/__tests__/sitemap.test.ts +0 -185
  106. package/src/utils/__tests__/slugify.test.ts +0 -31
  107. package/src/utils/__tests__/transform.test.ts +0 -769
  108. package/src/utils/accountLoaders.ts +0 -203
  109. package/src/utils/authHelpers.ts +0 -85
  110. package/src/utils/batch.ts +0 -18
  111. package/src/utils/cartProjection.ts +0 -96
  112. package/src/utils/cookies.ts +0 -167
  113. package/src/utils/enrichment.ts +0 -560
  114. package/src/utils/fetch.ts +0 -107
  115. package/src/utils/fetchCache.ts +0 -199
  116. package/src/utils/index.ts +0 -19
  117. package/src/utils/instrumentedFetch.ts +0 -97
  118. package/src/utils/intelligentSearch.ts +0 -96
  119. package/src/utils/legacy.ts +0 -139
  120. package/src/utils/minicart.ts +0 -139
  121. package/src/utils/pickAndOmit.ts +0 -25
  122. package/src/utils/proxy.ts +0 -435
  123. package/src/utils/resilience.ts +0 -352
  124. package/src/utils/resourceRange.ts +0 -10
  125. package/src/utils/segment.ts +0 -152
  126. package/src/utils/similars.ts +0 -37
  127. package/src/utils/simulationCache.ts +0 -73
  128. package/src/utils/sitemap.ts +0 -282
  129. package/src/utils/slugCache.ts +0 -28
  130. package/src/utils/slugify.ts +0 -13
  131. package/src/utils/transform.ts +0 -1522
  132. package/src/utils/vtexId.ts +0 -138
  133. package/tsconfig.json +0 -7
@@ -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
- }
@@ -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
- };
@@ -1,37 +0,0 @@
1
- import type { Product } from "@decocms/apps-commerce/types";
2
- import { getVtexConfig, 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 = config.publicUrl
22
- ? `https://${config.publicUrl}`
23
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
24
-
25
- const similars = rawSimilars.map((p) => {
26
- const sku = pickSku(p);
27
- return toProduct(p, sku, 0, { baseUrl, priceCurrency: "BRL" });
28
- });
29
-
30
- return {
31
- ...product,
32
- isSimilarTo: similars,
33
- };
34
- } catch {
35
- return product;
36
- }
37
- };
@@ -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
- }