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.
Files changed (2) hide show
  1. package/dist/publish.js +42 -2
  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 fetch(url, init);
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 fetch(t.url, { method: t.method, headers: t.headers, body });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiply-cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Publish static sites to shiply.now from the command line \u00e2\u20ac\u201d instant web hosting for agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",