{"version":3,"file":"storage-upload.mjs","names":[],"sources":["../src/storage-upload.ts"],"sourcesContent":["import { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\nimport { Data } from \"effect\";\n\nexport interface DistFile {\n  path: string;\n  bytes: Uint8Array;\n}\n\nexport interface BundleUploadResult {\n  stored: number;\n  totalBytes: number;\n  integrity: Record<string, string>;\n  storage?: \"s3\" | \"memory\";\n}\n\nasync function walkDist(dir: string, baseDir: string): Promise<DistFile[]> {\n  const entries = await readdir(dir, { withFileTypes: true });\n  const files: DistFile[] = [];\n  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n    const fullPath = join(dir, entry.name);\n    if (entry.isDirectory()) {\n      files.push(...(await walkDist(fullPath, baseDir)));\n      continue;\n    }\n    const info = await stat(fullPath);\n    if (!info.isFile()) continue;\n    files.push({\n      path: relative(baseDir, fullPath).split(sep).join(\"/\"),\n      bytes: new Uint8Array(await readFile(fullPath)),\n    });\n  }\n  return files;\n}\n\nexport async function collectDistFiles(distDir: string): Promise<DistFile[]> {\n  return walkDist(distDir, distDir);\n}\n\nconst STATUS_ATTEMPTS = 3;\nconst TRANSPORT_ATTEMPTS = 5;\nconst RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\nconst backoffMs = (attempt: number) => 1000 * attempt + Math.floor(Math.random() * 500);\n\nclass BundleUploadError extends Data.TaggedError(\"BundleUploadError\")<{\n  readonly status: number;\n  readonly detail: string;\n  readonly note?: string;\n}> {\n  constructor(status: number, detail: string, note?: string) {\n    super({ status, detail, note });\n  }\n\n  override get message() {\n    return `${this.detail}${this.note ?? \"\"}`;\n  }\n\n  withNote(note: string) {\n    return new BundleUploadError(this.status, this.detail, `${this.note ?? \"\"}${note}`);\n  }\n}\n\nfunction describeUploadFailure(status: number, detail: string): string {\n  const trimmed = detail.slice(0, 300);\n  if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) {\n    return `${trimmed} — the serving host timed out the upload; raise BOS_STORAGE_UPLOAD_TIMEOUT_MS there or upload smaller batches (BOS_MAX_BUNDLE_UPLOAD_BYTES)`;\n  }\n  try {\n    const parsed = JSON.parse(detail) as { code?: unknown; message?: unknown };\n    if (parsed?.code === \"INTERNAL_SERVER_ERROR\" && parsed?.message === \"Internal Server Error\") {\n      return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;\n    }\n  } catch {\n    // not a JSON error body\n  }\n  return trimmed;\n}\n\nexport async function uploadBundle(input: {\n  origin: string;\n  apiKey?: string;\n  account: string;\n  gateway: string;\n  workspace: string;\n  files: DistFile[];\n  fetchImpl?: typeof fetch;\n}): Promise<BundleUploadResult> {\n  const fetchImpl = input.fetchImpl ?? fetch;\n  const headers: Record<string, string> = { \"content-type\": \"application/json\" };\n  if (input.apiKey) headers[\"x-api-key\"] = input.apiKey;\n\n  const body = JSON.stringify({\n    account: input.account,\n    gateway: input.gateway,\n    workspace: input.workspace,\n    files: input.files.map((file) => ({\n      path: file.path,\n      contentBase64: Buffer.from(file.bytes).toString(\"base64\"),\n    })),\n  });\n\n  let lastError: BundleUploadError | Error | undefined;\n  let transportFailures = 0;\n  let statusFailures = 0;\n  for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt++) {\n    try {\n      const response = await fetchImpl(`${input.origin.replace(/\\/$/, \"\")}/api/storage/bundles`, {\n        method: \"POST\",\n        headers:\n          transportFailures > 0\n            ? // a transport failure killed the pooled keepalive socket — do not\n              // reuse it (the \"socket connection closed unexpectedly\" incident)\n              { ...headers, connection: \"close\" }\n            : headers,\n        body,\n      });\n\n      if (response.ok) {\n        return (await response.json()) as BundleUploadResult;\n      }\n\n      const detail = await response.text().catch(() => \"\");\n      lastError = new BundleUploadError(\n        response.status,\n        `[publish] bundle upload for ${input.workspace} failed: ${response.status} ${describeUploadFailure(response.status, detail)}`,\n      );\n      // Retries are safe: the storage route stores by account/gateway/\n      // workspace/path, so re-posting a batch overwrites idempotently.\n      if (!RETRYABLE_STATUSES.has(response.status)) break;\n      statusFailures += 1;\n      if (statusFailures >= STATUS_ATTEMPTS) break;\n    } catch (error) {\n      transportFailures += 1;\n      lastError = new BundleUploadError(\n        0,\n        `[publish] bundle upload for ${input.workspace} failed: ${error instanceof Error ? error.message : String(error)}`,\n      );\n    }\n\n    if (attempt < TRANSPORT_ATTEMPTS) await sleep(backoffMs(attempt));\n  }\n\n  if (lastError instanceof BundleUploadError && lastError.status === 0) {\n    lastError = lastError.withNote(\n      \" (Bun fetch note: re-run with fetch verbose diagnostics for detail)\",\n    );\n  }\n  throw lastError ?? new Error(`[publish] bundle upload for ${input.workspace} failed`);\n}\n\nexport async function uploadWorkspaceDist(input: {\n  origin: string;\n  apiKey?: string;\n  account: string;\n  gateway: string;\n  workspace: string;\n  files: DistFile[];\n  maxRequestBytes?: number;\n}): Promise<BundleUploadResult> {\n  const maxRequestBytes =\n    input.maxRequestBytes ?? (Number(process.env.BOS_MAX_BUNDLE_UPLOAD_BYTES) || 64 * 1024 * 1024);\n\n  const merged: BundleUploadResult = { stored: 0, totalBytes: 0, integrity: {} };\n  let batch: DistFile[] = [];\n  let batchBytes = 0;\n  const overheadPerFile = (file: DistFile): number =>\n    Math.ceil(file.bytes.byteLength / 3) * 4 + file.path.length + 64;\n\n  const flush = async () => {\n    if (batch.length === 0) return;\n    const result = await uploadBundle({\n      origin: input.origin,\n      apiKey: input.apiKey,\n      account: input.account,\n      gateway: input.gateway,\n      workspace: input.workspace,\n      files: batch,\n    });\n    merged.stored += result.stored;\n    merged.totalBytes += result.totalBytes;\n    Object.assign(merged.integrity, result.integrity);\n    merged.storage = result.storage;\n    batch = [];\n    batchBytes = 0;\n  };\n\n  for (const file of input.files) {\n    const cost = overheadPerFile(file);\n    if (cost > maxRequestBytes) {\n      throw new Error(\n        `[publish] ${input.workspace}/${file.path} (${file.bytes.byteLength} bytes) exceeds the per-request upload ceiling`,\n      );\n    }\n    if (batchBytes + cost > maxRequestBytes) {\n      await flush();\n    }\n    batch.push(file);\n    batchBytes += cost;\n  }\n  await flush();\n\n  return merged;\n}\n"],"mappings":"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"}