shiply-cli 0.23.0 → 0.24.0
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/publish.js +42 -2
- package/package.json +1 -1
package/dist/publish.js
CHANGED
|
@@ -12,8 +12,48 @@ export class ApiError extends Error {
|
|
|
12
12
|
this.name = 'ApiError';
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
// Transient statuses worth retrying: gateway/timeout/rate-limit. NOT 500 — a
|
|
16
|
+
// deterministic server bug shouldn't be hammered (and retrying a non-idempotent
|
|
17
|
+
// create on a 500 risks duplicate work; a 502/503/504 means the origin likely
|
|
18
|
+
// never processed the request, so a retry is safe).
|
|
19
|
+
const TRANSIENT_STATUS = new Set([408, 429, 502, 503, 504]);
|
|
20
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
const backoffMs = (attempt) => Math.min(8000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
|
|
22
|
+
/** fetch() with a per-attempt timeout + retry-with-backoff on transient failures
|
|
23
|
+
* (network errors, aborts/timeouts, and 5xx gateway statuses). Bodies here are
|
|
24
|
+
* strings/Buffers, so re-sending on retry is safe. */
|
|
25
|
+
export async function fetchWithRetry(url, init, opts = {}) {
|
|
26
|
+
const attempts = opts.attempts ?? 4;
|
|
27
|
+
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
28
|
+
const label = opts.label ?? url;
|
|
29
|
+
let lastErr;
|
|
30
|
+
for (let i = 0; i < attempts; i++) {
|
|
31
|
+
const ac = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(url, { ...init, signal: ac.signal });
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
if (TRANSIENT_STATUS.has(res.status) && i < attempts - 1) {
|
|
37
|
+
process.stderr.write(` ↻ ${label} returned ${res.status} — retrying (${i + 2}/${attempts})…\n`);
|
|
38
|
+
await sleep(backoffMs(i));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
return res;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
lastErr = e;
|
|
46
|
+
if (i < attempts - 1) {
|
|
47
|
+
process.stderr.write(` ↻ ${label} failed (${e.message}) — retrying (${i + 2}/${attempts})…\n`);
|
|
48
|
+
await sleep(backoffMs(i));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`request to ${label} failed after ${attempts} attempts: ${lastErr?.message ?? 'unknown error'}`);
|
|
54
|
+
}
|
|
15
55
|
export async function api(url, init) {
|
|
16
|
-
const res = await
|
|
56
|
+
const res = await fetchWithRetry(url, init, { label: `${init.method ?? 'GET'} ${url}` });
|
|
17
57
|
const body = await res.json().catch(() => ({}));
|
|
18
58
|
if (!res.ok) {
|
|
19
59
|
const err = body.error;
|
|
@@ -71,7 +111,7 @@ async function uploadAll(dir, targets) {
|
|
|
71
111
|
while (next < targets.length) {
|
|
72
112
|
const t = targets[next++];
|
|
73
113
|
const body = await readFile(join(dir, t.path));
|
|
74
|
-
const res = await
|
|
114
|
+
const res = await fetchWithRetry(t.url, { method: t.method, headers: t.headers, body }, { label: `upload ${t.path}` });
|
|
75
115
|
if (!res.ok)
|
|
76
116
|
throw new Error(`upload failed for ${t.path}: HTTP ${res.status}`);
|
|
77
117
|
}
|
package/package.json
CHANGED