hostedge 0.1.1 → 0.1.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.
Files changed (2) hide show
  1. package/dist/hostedge.js +83 -18
  2. package/package.json +1 -1
package/dist/hostedge.js CHANGED
@@ -1320,19 +1320,21 @@ function exitCodeFor(status, code) {
1320
1320
  return EXIT.failure;
1321
1321
  }
1322
1322
  var ApiError = class extends CliError {
1323
- constructor(message, status, code) {
1323
+ constructor(message, status, code, retryAfter = null) {
1324
1324
  super(message, exitCodeFor(status, code));
1325
1325
  this.status = status;
1326
1326
  this.code = code;
1327
+ this.retryAfter = retryAfter;
1327
1328
  }
1328
1329
  status;
1329
1330
  code;
1331
+ retryAfter;
1330
1332
  };
1331
1333
  function createApi(io2, apiUrl, token) {
1332
1334
  async function raw(path, init2 = {}) {
1333
1335
  const headers = new Headers(init2.headers);
1334
1336
  headers.set("authorization", `Bearer ${token}`);
1335
- headers.set("user-agent", "hostedge-cli/0.1.1");
1337
+ headers.set("user-agent", "hostedge-cli/0.1.2");
1336
1338
  let response;
1337
1339
  try {
1338
1340
  response = await io2.fetch(`${apiUrl}${path}`, { ...init2, headers });
@@ -1343,7 +1345,13 @@ function createApi(io2, apiUrl, token) {
1343
1345
  const body2 = await response.json().catch(() => ({}));
1344
1346
  const fields = body2.error?.fields ? ` ${Object.entries(body2.error.fields).map(([k, v]) => `${k}: ${v}`).join("; ")}` : "";
1345
1347
  const message = response.status === 401 ? `${body2.error?.message ?? "Not signed in."} Run \`hostedge login\`.` : `${body2.error?.message ?? `HostEdge answered ${response.status}.`}${fields}`;
1346
- throw new ApiError(message, response.status, body2.error?.code ?? null);
1348
+ const wait = Number(response.headers.get("retry-after"));
1349
+ throw new ApiError(
1350
+ message,
1351
+ response.status,
1352
+ body2.error?.code ?? null,
1353
+ Number.isFinite(wait) && wait > 0 ? wait : null
1354
+ );
1347
1355
  }
1348
1356
  return response;
1349
1357
  }
@@ -1566,7 +1574,8 @@ async function withRetries(work, retryable, attempts = 4, delayMs = 500) {
1566
1574
  return await work();
1567
1575
  } catch (error) {
1568
1576
  if (attempt >= attempts || !retryable(error)) throw error;
1569
- await new Promise((resolve6) => setTimeout(resolve6, delayMs * 2 ** (attempt - 1)));
1577
+ const wait = typeof delayMs === "function" ? delayMs(error, attempt) : delayMs * 2 ** (attempt - 1);
1578
+ await new Promise((resolve6) => setTimeout(resolve6, wait));
1570
1579
  }
1571
1580
  }
1572
1581
  }
@@ -1639,8 +1648,31 @@ async function* readSse(body2) {
1639
1648
 
1640
1649
  // src/commands/deploy.ts
1641
1650
  var FINISHED = /* @__PURE__ */ new Set(["ready", "failed", "canceled"]);
1642
- var UPLOAD_CONCURRENCY = 16;
1651
+ var UPLOAD_CONCURRENCY = 6;
1652
+ var BATCH_BYTES = 3 * 1024 * 1024;
1653
+ var BATCH_FILES = 100;
1643
1654
  var UPLOAD_TIMEOUT_MS = 3e4;
1655
+ function batchesOf(files) {
1656
+ const batches = [];
1657
+ let current = [];
1658
+ let bytes = 0;
1659
+ for (const file of files) {
1660
+ if (current.length > 0 && (bytes + file.size > BATCH_BYTES || current.length >= BATCH_FILES)) {
1661
+ batches.push(current);
1662
+ current = [];
1663
+ bytes = 0;
1664
+ }
1665
+ current.push(file);
1666
+ bytes += file.size;
1667
+ }
1668
+ if (current.length > 0) batches.push(current);
1669
+ return batches;
1670
+ }
1671
+ function retryDelay(error, attempt) {
1672
+ if (error instanceof ApiError && error.retryAfter !== null)
1673
+ return Math.min(6e4, (error.retryAfter + 1) * 1e3);
1674
+ return Math.min(6e4, 500 * 2 ** (attempt - 1));
1675
+ }
1644
1676
  function temporary(error) {
1645
1677
  if (error instanceof ApiError) return error.status === 429 || error.status >= 500;
1646
1678
  return error instanceof CliError;
@@ -1721,23 +1753,56 @@ async function deploy(r, options) {
1721
1753
  let uploaded = 0;
1722
1754
  let reported = 0;
1723
1755
  const step = Math.max(1, Math.ceil(toUpload.length / 10));
1724
- await pool(toUpload, UPLOAD_CONCURRENCY, async (file) => {
1725
- const bytes = await readFile4(file.absolute);
1726
- await withRetries(
1727
- () => api.raw(`/api/v1/uploads/${file.hash}`, {
1728
- method: "PUT",
1729
- headers: { "content-type": "application/octet-stream" },
1730
- body: bytes,
1731
- signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
1732
- }),
1733
- temporary
1734
- );
1735
- uploaded++;
1756
+ const progress = (count2) => {
1757
+ uploaded += count2;
1736
1758
  if (toUpload.length > 20 && uploaded - reported >= step && uploaded < toUpload.length) {
1737
1759
  reported = uploaded;
1738
1760
  r.say(`${s.dim(` ${uploaded}/${toUpload.length} uploaded`)}
1739
1761
  `);
1740
1762
  }
1763
+ };
1764
+ const send = (path, init2, bytes) => withRetries(
1765
+ () => api.raw(path, {
1766
+ ...init2,
1767
+ // 30 s plus a few more for every MB, so a big file on a slow line can finish.
1768
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS + Math.ceil(bytes / 1048576) * 4e3)
1769
+ }),
1770
+ temporary,
1771
+ 6,
1772
+ retryDelay
1773
+ );
1774
+ let batching = true;
1775
+ await pool(batchesOf(toUpload), UPLOAD_CONCURRENCY, async (batch) => {
1776
+ if (batching && (batch.length > 1 || (batch[0]?.size ?? 0) <= BATCH_BYTES)) {
1777
+ const form = new FormData();
1778
+ for (const file of batch)
1779
+ form.append(file.hash, new Blob([await readFile4(file.absolute)]), file.hash);
1780
+ try {
1781
+ await send(
1782
+ "/api/v1/uploads/batch",
1783
+ { method: "POST", body: form },
1784
+ batch.reduce((sum, file) => sum + file.size, 0)
1785
+ );
1786
+ progress(batch.length);
1787
+ return;
1788
+ } catch (error) {
1789
+ if (!(error instanceof ApiError) || error.status !== 404 && error.status !== 405)
1790
+ throw error;
1791
+ batching = false;
1792
+ }
1793
+ }
1794
+ for (const file of batch) {
1795
+ await send(
1796
+ `/api/v1/uploads/${file.hash}`,
1797
+ {
1798
+ method: "PUT",
1799
+ headers: { "content-type": "application/octet-stream" },
1800
+ body: await readFile4(file.absolute)
1801
+ },
1802
+ file.size
1803
+ );
1804
+ progress(1);
1805
+ }
1741
1806
  });
1742
1807
  let project;
1743
1808
  if (!link) {
@@ -2969,7 +3034,7 @@ var Reporter = class {
2969
3034
  };
2970
3035
 
2971
3036
  // src/cli.ts
2972
- var VERSION = "0.1.1";
3037
+ var VERSION = "0.1.2";
2973
3038
  var HELP = `hostedge \u2014 deploy to HostEdge from your terminal
2974
3039
 
2975
3040
  Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostedge",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Deploy to HostEdge from your terminal",
5
5
  "keywords": [
6
6
  "hostedge",