@decocms/apps 0.23.1 → 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.1",
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": {
@@ -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;