@glash/cli 0.2.21 → 0.2.22

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.
@@ -153,6 +153,7 @@ export async function deploy({ flags }) {
153
153
  formData.append('gitBranch', gitBranch || '');
154
154
  formData.append('target', target);
155
155
 
156
+ info(color.dim('→ uploading bundle to glashDB…'));
156
157
  dep = await apiPost('/deployments/upload', formData);
157
158
  await rm(zipPath, { force: true }).catch(() => {});
158
159
  } catch (e) {
package/lib/api.mjs CHANGED
@@ -28,6 +28,12 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
28
28
  // proxies across machines.
29
29
  const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504, 522, 524]);
30
30
  const DEFAULT_RETRIES = 4;
31
+ // Per-request wall-clock ceiling. Without this a contended/wedged backend
32
+ // leaves `glash deploy` hanging forever after "source bundle X MB" with no
33
+ // output. On timeout we abort and retry instead of hanging. Uploads (FormData)
34
+ // stream a bundle and register a deployment, so they get a longer ceiling.
35
+ const DEFAULT_TIMEOUT_MS = 60_000;
36
+ const UPLOAD_TIMEOUT_MS = 180_000;
31
37
 
32
38
  function backoffMs(attempt, retryAfter) {
33
39
  if (retryAfter && Number.isFinite(retryAfter)) return Math.min(Math.max(retryAfter, 1000), 60000);
@@ -37,7 +43,7 @@ function backoffMs(attempt, retryAfter) {
37
43
  return Math.round(jitter);
38
44
  }
39
45
 
40
- export async function api(method, path, { body, token, query, retries = DEFAULT_RETRIES, signal } = {}) {
46
+ export async function api(method, path, { body, token, query, retries = DEFAULT_RETRIES, signal, timeoutMs } = {}) {
41
47
  const headers = {
42
48
  Accept: 'application/json',
43
49
  'User-Agent': `glash-cli/${VERSION}`,
@@ -62,25 +68,44 @@ export async function api(method, path, { body, token, query, retries = DEFAULT_
62
68
  // re-streamable — fetch reads them fresh on each attempt, which lets us
63
69
  // retry uploads without re-zipping.
64
70
  const requestBody = isForm ? body : (body === undefined ? undefined : JSON.stringify(body));
71
+ const effectiveTimeout = timeoutMs ?? (isForm ? UPLOAD_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
65
72
 
66
73
  let attempt = 0;
67
74
  // eslint-disable-next-line no-constant-condition
68
75
  while (true) {
76
+ // Each attempt gets a fresh AbortController so the timeout can fire
77
+ // independently. If the caller passed their own signal, chain it.
78
+ const ctrl = new AbortController();
79
+ let timedOut = false;
80
+ const timer = setTimeout(() => { timedOut = true; ctrl.abort(); }, effectiveTimeout);
81
+ const onCallerAbort = () => ctrl.abort();
82
+ if (signal) {
83
+ if (signal.aborted) ctrl.abort();
84
+ else signal.addEventListener('abort', onCallerAbort, { once: true });
85
+ }
86
+
69
87
  let res;
70
88
  try {
71
- res = await fetch(url, { method, headers, body: requestBody, signal });
89
+ res = await fetch(url, { method, headers, body: requestBody, signal: ctrl.signal });
72
90
  } catch (e) {
73
- if (signal?.aborted) throw e;
74
- const detail = e?.cause?.message || e?.message || String(e);
75
- // Network-level failure: the request did not complete, so retrying is
76
- // always safe. This covers DNS, TLS, connection-reset and timeout errors.
91
+ clearTimeout(timer);
92
+ signal?.removeEventListener?.('abort', onCallerAbort);
93
+ // A caller-initiated cancel (not our timeout) must not be retried.
94
+ if (signal?.aborted && !timedOut) throw e;
95
+ const detail = timedOut
96
+ ? `timed out after ${Math.round(effectiveTimeout / 1000)}s`
97
+ : (e?.cause?.message || e?.message || String(e));
98
+ // Network failure or timeout: the request did not complete, so retrying
99
+ // is safe. Covers DNS, TLS, connection-reset, and wedged-server hangs.
77
100
  if (attempt < retries) {
78
101
  await sleep(backoffMs(attempt));
79
102
  attempt += 1;
80
103
  continue;
81
104
  }
82
- throw new GlashApiError(0, `Network request failed while calling ${method} ${path}: ${detail}`);
105
+ throw new GlashApiError(0, `Request failed while calling ${method} ${path}: ${detail}`);
83
106
  }
107
+ clearTimeout(timer);
108
+ signal?.removeEventListener?.('abort', onCallerAbort);
84
109
 
85
110
  if (!res.ok) {
86
111
  const retryAfter = retryAfterMs(res.headers);
package/lib/version.mjs CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.21';
1
+ export const VERSION = '0.2.22';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glash/cli",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "description": "glashDB command-line interface — deploy projects, pull source + env, manage env vars.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://glashdb.com",