diff --git a/packages/cli/src/utils/publishProject.test.ts b/packages/cli/src/utils/publishProject.test.ts index b5a3b8117..4764e0409 100644 --- a/packages/cli/src/utils/publishProject.test.ts +++ b/packages/cli/src/utils/publishProject.test.ts @@ -7,6 +7,7 @@ import { createPublishArchive, getPublishApiBaseUrl, publishProjectArchive, + uploadTimeoutMs, } from "./publishProject.js"; function makeProjectDir(): string { @@ -35,6 +36,22 @@ describe("createPublishArchive", () => { }); }); +describe("uploadTimeoutMs", () => { + it("returns the minimum timeout for small files", () => { + expect(uploadTimeoutMs(0)).toBe(120_000); + expect(uploadTimeoutMs(50 * 1024 * 1024)).toBe(120_000); + }); + + it("scales above the floor for large files", () => { + expect(uploadTimeoutMs(64 * 1024 * 1024)).toBeGreaterThan(120_000); + expect(uploadTimeoutMs(500 * 1024 * 1024)).toBeGreaterThan(900_000); + }); + + it("returns an integer", () => { + expect(Number.isInteger(uploadTimeoutMs(123_456))).toBe(true); + }); +}); + describe("publishProjectArchive", () => { beforeEach(() => { vi.stubEnv("HYPERFRAMES_PUBLISHED_PROJECTS_API_URL", ""); diff --git a/packages/cli/src/utils/publishProject.ts b/packages/cli/src/utils/publishProject.ts index 3b4d62855..9a317b619 100644 --- a/packages/cli/src/utils/publishProject.ts +++ b/packages/cli/src/utils/publishProject.ts @@ -5,7 +5,11 @@ import AdmZip from "adm-zip"; const IGNORED_DIRS = new Set([".git", "node_modules", "dist", ".next", "coverage"]); const IGNORED_FILES = new Set([".DS_Store", "Thumbs.db"]); const PUBLISH_CONTENT_TYPE = "application/zip"; -const PUBLISH_REQUEST_TIMEOUT_MS = 30_000; +const PUBLISH_METADATA_TIMEOUT_MS = 30_000; +const PUBLISH_UPLOAD_MIN_TIMEOUT_MS = 120_000; +// Conservative floor — most connections are faster, but this prevents +// premature aborts on slow/unstable networks (hotel wifi, tethering). +const PUBLISH_UPLOAD_BYTES_PER_SECOND = 500_000; export interface PublishArchiveResult { buffer: Buffer; @@ -25,6 +29,7 @@ interface StagedUploadResponse { uploadKey: string; contentType: string; uploadHeaders: Record; + expiresInSeconds: number; } type JsonRecord = Record; @@ -73,11 +78,14 @@ function parseStagedUploadResponse( const uploadKey = stringField(data, "upload_key"); const contentType = stringField(data, "content_type") || PUBLISH_CONTENT_TYPE; if (!uploadUrl || !uploadKey) return null; + const rawExpires = data["expires_in_seconds"]; + const expiresInSeconds = typeof rawExpires === "number" && rawExpires > 0 ? rawExpires : 1800; return { uploadUrl, uploadKey, contentType, uploadHeaders: getUploadHeaders(data, uploadUrl, contentType, archiveByteLength), + expiresInSeconds, }; } @@ -142,6 +150,13 @@ async function readErrorMessage(response: Response, fallback: string): Promise