@decocms/apps 0.23.0 → 0.23.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/apps",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "type": "module",
5
5
  "description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
6
6
  "exports": {
package/vtex/client.ts CHANGED
@@ -483,13 +483,15 @@ export function initVtexFromBlocks(blocks: Record<string, any>) {
483
483
  console.warn("[VTEX] No vtex.json block found.");
484
484
  return;
485
485
  }
486
+ const appKey = typeof vtexBlock.appKey === "string" ? vtexBlock.appKey : undefined;
487
+ const appToken = typeof vtexBlock.appToken === "string" ? vtexBlock.appToken : undefined;
486
488
  configureVtex({
487
489
  account: vtexBlock.account,
488
490
  publicUrl: vtexBlock.publicUrl,
489
491
  salesChannel: vtexBlock.salesChannel || "1",
490
492
  locale: vtexBlock.locale || vtexBlock.defaultLocale,
491
- appKey: vtexBlock.appKey,
492
- appToken: vtexBlock.appToken,
493
+ appKey,
494
+ appToken,
493
495
  country: vtexBlock.country,
494
496
  domain: vtexBlock.domain,
495
497
  });
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  const DEFAULT_MAX_ENTRIES = 500;
11
+ const MAX_RETRIES = 2;
12
+ const RETRY_DELAYS = [200, 400];
11
13
 
12
14
  interface CacheEntry {
13
15
  body: unknown;
@@ -41,23 +43,61 @@ function evictIfNeeded() {
41
43
  for (const [key] of toRemove) store.delete(key);
42
44
  }
43
45
 
46
+ function sleep(ms: number): Promise<void> {
47
+ return new Promise((resolve) => setTimeout(resolve, ms));
48
+ }
49
+
50
+ function isRetryable(response: Response): boolean {
51
+ return response.status >= 500 || response.status === 429;
52
+ }
53
+
44
54
  async function executeFetch(
45
- _url: string,
55
+ url: string,
46
56
  doFetch: () => Promise<Response>,
57
+ retry = true,
47
58
  ): Promise<CacheEntry> {
48
- const response = await doFetch();
49
- if (response.status >= 500) {
50
- throw new Error(
51
- `fetchWithCache: ${response.status} ${response.statusText}`,
52
- );
59
+ let lastError: Error | undefined;
60
+
61
+ const attempts = retry ? MAX_RETRIES + 1 : 1;
62
+ for (let attempt = 0; attempt < attempts; attempt++) {
63
+ try {
64
+ const response = await doFetch();
65
+
66
+ if (isRetryable(response) && attempt < attempts - 1) {
67
+ console.warn(
68
+ `[vtex-fetch] ${response.status} on attempt ${attempt + 1}/${attempts} — ${url}`,
69
+ );
70
+ await sleep(RETRY_DELAYS[attempt] ?? 400);
71
+ continue;
72
+ }
73
+
74
+ if (response.status >= 500) {
75
+ throw new Error(
76
+ `fetchWithCache: ${response.status} ${response.statusText} after ${attempt + 1} attempt(s) — ${url}`,
77
+ );
78
+ }
79
+
80
+ const body = response.ok ? await response.json() : null;
81
+ return {
82
+ body,
83
+ status: response.status,
84
+ createdAt: Date.now(),
85
+ refreshing: false,
86
+ };
87
+ } catch (error) {
88
+ lastError = error instanceof Error ? error : new Error(String(error));
89
+
90
+ if (attempt < attempts - 1) {
91
+ console.warn(
92
+ `[vtex-fetch] attempt ${attempt + 1}/${attempts} failed — ${url}: ${lastError.message}`,
93
+ );
94
+ await sleep(RETRY_DELAYS[attempt] ?? 400);
95
+ continue;
96
+ }
97
+ }
53
98
  }
54
- const body = response.ok ? await response.json() : null;
55
- return {
56
- body,
57
- status: response.status,
58
- createdAt: Date.now(),
59
- refreshing: false,
60
- };
99
+
100
+ throw lastError ?? new Error(`fetchWithCache: all ${attempts} attempts failed — ${url}`);
61
101
  }
62
102
 
63
103
  export interface FetchCacheOptions {
@@ -94,13 +134,10 @@ export function fetchWithCache<T>(
94
134
 
95
135
  if (isStale && !entry.refreshing) {
96
136
  entry.refreshing = true;
97
- executeFetch(cacheKey, doFetch)
137
+ // Background refresh: no retry — stale data is already being served
138
+ executeFetch(cacheKey, doFetch, false)
98
139
  .then((fresh) => {
99
140
  const ttl = opts?.ttl ?? ttlForStatus(fresh.status);
100
- // Prevent a transient 4xx from downgrading a previously
101
- // successful (2xx) cache entry. If the existing entry was
102
- // already a 4xx (e.g. a stale 404), revalidating with
103
- // another cacheable 4xx is fine — the 404 TTL is still valid.
104
141
  const existingWasSuccess = entry.status >= 200 && entry.status < 300;
105
142
  const freshIsError = fresh.status >= 400;
106
143
  const wouldDowngrade = existingWasSuccess && freshIsError;
@@ -158,7 +158,7 @@ export async function proxyToVtex(
158
158
  }
159
159
  }
160
160
 
161
- if (config.appKey && config.appToken) {
161
+ if (typeof config.appKey === "string" && typeof config.appToken === "string") {
162
162
  forwardHeaders.set("X-VTEX-API-AppKey", config.appKey);
163
163
  forwardHeaders.set("X-VTEX-API-AppToken", config.appToken);
164
164
  }