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