@tapi-dev/sdk 0.1.29 → 0.1.30

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/cli.d.ts CHANGED
@@ -125,7 +125,9 @@ export declare function chooseStudioPort(preferred?: number, options?: {
125
125
  portAvailable?: StudioPortAvailable;
126
126
  reapBlockedPorts?: StudioPortReaper;
127
127
  }): Promise<number>;
128
- export declare function downloadAndVerify(url: string, destination: string, expectedSha256: string, label?: string): Promise<void>;
128
+ export declare function downloadAndVerify(url: string, destination: string, expectedSha256: string, label?: string, options?: {
129
+ stallTimeoutMs?: number;
130
+ }): Promise<void>;
129
131
  export declare function studioInstanceRecordPathForOptions(options: StudioCliOptions): string;
130
132
  export declare function findPortableStudioServerExe(releaseDir: string, manifest: StudioReleaseManifest): string | undefined;
131
133
  export declare function estimateRequiredExtractionBytes(zipBytes: number): number;
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { homedir } from "node:os";
9
9
  import { basename, dirname, join, resolve } from "node:path";
10
10
  import { performance } from "node:perf_hooks";
11
11
  import { emitKeypressEvents } from "node:readline";
12
- import { Readable } from "node:stream";
12
+ import { Readable, Transform } from "node:stream";
13
13
  import { pipeline } from "node:stream/promises";
14
14
  import { fileURLToPath, pathToFileURL } from "node:url";
15
15
  import { TapiClient } from "./index.js";
@@ -23,6 +23,8 @@ const SUPPORTED_STUDIO_PLATFORM = "windows-x86_64";
23
23
  const DEFAULT_INSTALL_AUTH_TIMEOUT_MS = 120_000;
24
24
  const PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS = 30_000;
25
25
  const PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS = 250;
26
+ const DOWNLOAD_STALL_TIMEOUT_MS = 30_000;
27
+ const DOWNLOAD_PROGRESS_LOG_BYTES = 50 * 1024 * 1024;
26
28
  const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
27
29
  const MAX_NONCURRENT_MANAGED_DOWNLOADS = 0;
28
30
  const MAX_NONCURRENT_MANAGED_RELEASES = 0;
@@ -2617,11 +2619,11 @@ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, f
2617
2619
  expiresAt: typeof payload?.expiresAt === "string" ? payload.expiresAt : undefined,
2618
2620
  };
2619
2621
  }
2620
- export async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer") {
2622
+ export async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer", options = {}) {
2621
2623
  const partialPath = `${destination}.partial`;
2622
2624
  await unlinkIfExists(partialPath);
2623
2625
  try {
2624
- await downloadFile(url, partialPath, label);
2626
+ await downloadFile(url, partialPath, label, options);
2625
2627
  const actualSha256 = await sha256File(partialPath);
2626
2628
  if (actualSha256 !== expectedSha256.toLowerCase()) {
2627
2629
  throw new Error(`${label} checksum mismatch. Expected ${expectedSha256}, got ${actualSha256}.`);
@@ -2634,15 +2636,97 @@ export async function downloadAndVerify(url, destination, expectedSha256, label
2634
2636
  throw error;
2635
2637
  }
2636
2638
  }
2637
- async function downloadFile(url, destination, label = "download") {
2638
- const response = await fetch(url);
2639
- if (!response.ok) {
2640
- throw new Error(`Failed to download ${label}: HTTP ${response.status}`);
2639
+ async function downloadFile(url, destination, label = "download", options = {}) {
2640
+ const stallTimeoutMs = normalizeDownloadTimeout(options.stallTimeoutMs);
2641
+ const controller = new AbortController();
2642
+ let stallTimer;
2643
+ let stallError;
2644
+ let source;
2645
+ let progressStream;
2646
+ let destinationStream;
2647
+ let receivedBytes = 0;
2648
+ let nextProgressLogBytes = DOWNLOAD_PROGRESS_LOG_BYTES;
2649
+ const failIfStalled = (phase) => {
2650
+ if (stallTimer) {
2651
+ clearTimeout(stallTimer);
2652
+ }
2653
+ stallTimer = setTimeout(() => {
2654
+ stallError = formatDownloadStallError(label, stallTimeoutMs, phase, receivedBytes);
2655
+ controller.abort(stallError);
2656
+ source?.destroy(stallError);
2657
+ progressStream?.destroy(stallError);
2658
+ destinationStream?.destroy(stallError);
2659
+ }, stallTimeoutMs);
2660
+ };
2661
+ try {
2662
+ console.log(`Downloading ${label}...`);
2663
+ failIfStalled("connecting");
2664
+ const response = await fetch(url, { signal: controller.signal });
2665
+ if (!response.ok) {
2666
+ throw new Error(`Failed to download ${label}: HTTP ${response.status}`);
2667
+ }
2668
+ if (!response.body) {
2669
+ throw new Error(`${label} response did not include a body.`);
2670
+ }
2671
+ failIfStalled("receiving data");
2672
+ source = Readable.fromWeb(response.body);
2673
+ progressStream = new Transform({
2674
+ transform(chunk, _encoding, callback) {
2675
+ receivedBytes += downloadChunkByteLength(chunk);
2676
+ failIfStalled("receiving data");
2677
+ if (receivedBytes >= nextProgressLogBytes) {
2678
+ console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}...`);
2679
+ while (receivedBytes >= nextProgressLogBytes) {
2680
+ nextProgressLogBytes += DOWNLOAD_PROGRESS_LOG_BYTES;
2681
+ }
2682
+ }
2683
+ callback(null, chunk);
2684
+ },
2685
+ });
2686
+ destinationStream = createWriteStream(destination);
2687
+ await pipeline(source, progressStream, destinationStream);
2688
+ console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}.`);
2689
+ }
2690
+ catch (error) {
2691
+ if (stallError) {
2692
+ throw stallError;
2693
+ }
2694
+ throw error;
2641
2695
  }
2642
- if (!response.body) {
2643
- throw new Error(`${label} response did not include a body.`);
2696
+ finally {
2697
+ if (stallTimer) {
2698
+ clearTimeout(stallTimer);
2699
+ }
2700
+ }
2701
+ }
2702
+ function downloadChunkByteLength(chunk) {
2703
+ if (typeof chunk === "string") {
2704
+ return Buffer.byteLength(chunk);
2705
+ }
2706
+ if (Buffer.isBuffer(chunk)) {
2707
+ return chunk.length;
2708
+ }
2709
+ if (chunk instanceof Uint8Array) {
2710
+ return chunk.byteLength;
2711
+ }
2712
+ return 0;
2713
+ }
2714
+ function normalizeDownloadTimeout(timeoutMs) {
2715
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
2716
+ return DOWNLOAD_STALL_TIMEOUT_MS;
2717
+ }
2718
+ return Math.max(1, Math.floor(timeoutMs));
2719
+ }
2720
+ function formatDownloadStallError(label, timeoutMs, phase, receivedBytes) {
2721
+ const progress = receivedBytes > 0 ? ` after ${formatBytes(receivedBytes)}` : "";
2722
+ return new Error(`${label} download stalled${progress}: no data received for ${formatDuration(timeoutMs)} while ${phase}. ` +
2723
+ "Check your network connection, VPN, or install authorization and retry.");
2724
+ }
2725
+ function formatDuration(ms) {
2726
+ if (ms < 1000) {
2727
+ return `${ms}ms`;
2644
2728
  }
2645
- await pipeline(Readable.fromWeb(response.body), createWriteStream(destination));
2729
+ return `${Math.ceil(ms / 1000)}s`;
2646
2730
  }
2647
2731
  async function browserGoogleSignIn(timeoutMs = DEFAULT_INSTALL_AUTH_TIMEOUT_MS) {
2648
2732
  ensureWindowsHost();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",