everything-dev 2.0.0-rc.4 → 2.0.0-rc.5

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 (59) hide show
  1. package/dist/api-contract.cjs +3 -6
  2. package/dist/api-contract.cjs.map +1 -1
  3. package/dist/api-contract.mjs +3 -6
  4. package/dist/api-contract.mjs.map +1 -1
  5. package/dist/apps/upload/src/client.cjs +188 -0
  6. package/dist/apps/upload/src/client.cjs.map +1 -0
  7. package/dist/apps/upload/src/client.mjs +180 -0
  8. package/dist/apps/upload/src/client.mjs.map +1 -0
  9. package/dist/cdn-deploy.cjs +29 -11
  10. package/dist/cdn-deploy.cjs.map +1 -1
  11. package/dist/cdn-deploy.mjs +29 -11
  12. package/dist/cdn-deploy.mjs.map +1 -1
  13. package/dist/cli/init.cjs +0 -2
  14. package/dist/cli/init.cjs.map +1 -1
  15. package/dist/cli/init.d.cts +1 -1
  16. package/dist/cli/init.d.mts +1 -1
  17. package/dist/cli/init.mjs +0 -2
  18. package/dist/cli/init.mjs.map +1 -1
  19. package/dist/cli/sync.cjs +0 -1
  20. package/dist/cli/sync.cjs.map +1 -1
  21. package/dist/cli/sync.mjs +0 -1
  22. package/dist/cli/sync.mjs.map +1 -1
  23. package/dist/commands/deploy.cjs +0 -2
  24. package/dist/commands/deploy.cjs.map +1 -1
  25. package/dist/commands/deploy.mjs +0 -2
  26. package/dist/commands/deploy.mjs.map +1 -1
  27. package/dist/commands/upgrade.cjs +30 -7
  28. package/dist/commands/upgrade.cjs.map +1 -1
  29. package/dist/commands/upgrade.mjs +30 -7
  30. package/dist/commands/upgrade.mjs.map +1 -1
  31. package/dist/config.cjs.map +1 -1
  32. package/dist/config.mjs.map +1 -1
  33. package/dist/contract.meta.cjs +1 -1
  34. package/dist/contract.meta.cjs.map +1 -1
  35. package/dist/contract.meta.d.cts +1 -1
  36. package/dist/contract.meta.d.mts +1 -1
  37. package/dist/contract.meta.mjs +1 -1
  38. package/dist/contract.meta.mjs.map +1 -1
  39. package/dist/dev-program.cjs +1 -0
  40. package/dist/dev-program.cjs.map +1 -1
  41. package/dist/dev-program.mjs +1 -1
  42. package/dist/dev-program.mjs.map +1 -1
  43. package/dist/image-deploy.cjs +38 -14
  44. package/dist/image-deploy.cjs.map +1 -1
  45. package/dist/image-deploy.mjs +39 -15
  46. package/dist/image-deploy.mjs.map +1 -1
  47. package/dist/storage-upload.cjs +7 -68
  48. package/dist/storage-upload.cjs.map +1 -1
  49. package/dist/storage-upload.mjs +7 -68
  50. package/dist/storage-upload.mjs.map +1 -1
  51. package/dist/ui/api.cjs +11 -69
  52. package/dist/ui/api.cjs.map +1 -1
  53. package/dist/ui/api.d.cts +2 -4
  54. package/dist/ui/api.d.mts +2 -4
  55. package/dist/ui/api.mjs +11 -69
  56. package/dist/ui/api.mjs.map +1 -1
  57. package/dist/ui/auth.d.cts +43 -43
  58. package/dist/ui/auth.d.mts +43 -43
  59. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"image-deploy.mjs","names":[],"sources":["../src/image-deploy.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { run } from \"./utils/run\";\nimport { colors, icons } from \"./utils/theme\";\n\nexport interface ResolvedImageRef {\n image: string;\n source: \"ci\" | \"env\" | \"repository\";\n}\n\n/**\n * Image reference resolution precedence (ADR 0021): `ci.image` in\n * bos.config.json → `BOS_IMAGE` env → derived from `repository`.\n */\nexport function resolveImageRef(input: {\n ciImage?: string;\n repository?: string;\n env?: Record<string, string | undefined>;\n}): ResolvedImageRef | undefined {\n const envImage = input.env?.BOS_IMAGE;\n if (input.ciImage) return { image: input.ciImage, source: \"ci\" };\n if (envImage) return { image: envImage, source: \"env\" };\n\n const repo = input.repository\n ?.replace(/^https?:\\/\\/(www\\.)?github\\.com\\//i, \"\")\n .replace(/\\.git$/i, \"\")\n .replace(/\\/+$/, \"\")\n .toLowerCase();\n if (!repo?.includes(\"/\") || repo.split(\"/\").length !== 2) return undefined;\n return { image: `ghcr.io/${repo}`, source: \"repository\" };\n}\n\nexport async function hasDocker(): Promise<boolean> {\n try {\n const result = await run(\"docker\", [\"info\", \"--format\", \"{{.ServerVersion}}\"], {\n capture: true,\n });\n return result?.exitCode === 0;\n } catch {\n return false;\n }\n}\n\nfunction parseDigest(pushOutput: string): string | undefined {\n const match = pushOutput.match(/digest:\\s*(sha256:[a-f0-9]+)/i);\n return match?.[1];\n}\n\nconst SEMVER_PATTERN = /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?$/;\n\n/**\n * The runtime framework version baked into the image is the workspace source's\n * `everything-dev` package version — the image builds from `configDir`, so that\n * file is the truth. Missing (child repos without the workspace) → undefined.\n */\nfunction readFrameworkVersion(configDir: string): string | undefined {\n try {\n const raw = readFileSync(\n join(configDir, \"packages\", \"everything-dev\", \"package.json\"),\n \"utf-8\",\n );\n const pkg = JSON.parse(raw) as { version?: unknown };\n return typeof pkg.version === \"string\" && SEMVER_PATTERN.test(pkg.version)\n ? pkg.version\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface ImageTags {\n /** Exact refs to build and push, in push order (latest excluded). */\n tags: string[];\n /** Push `:latest` after the pinned tags. */\n pushLatest: boolean;\n /** Human-readable reason when `latest` is held. */\n latestHoldReason?: string;\n /** Push `:rc` after the pinned tags — the floating prerelease-train ref. */\n pushRc: boolean;\n}\n\n/**\n * Tag scheme (ADR 0021): `sha-<short>` always; the exact `v<version>` tag; a\n * floating major tag (`v2`) on stable releases only. `latest` is held during\n * prereleases (npm's `rc` dist-tag analog) so existing deployments pinned to\n * `:latest` keep serving the last stable image — and `:rc` is the floating\n * ref for the prerelease train (npm's `rc` dist-tag mirror): it moves on\n * every prerelease deploy and freezes at the last rc-train build once the\n * workspace releases a stable version.\n */\nexport function computeImageTags(input: { shortSha: string; version?: string }): ImageTags {\n const tags = [`sha-${input.shortSha}`];\n const match = input.version?.trim().match(SEMVER_PATTERN);\n if (!match) {\n return { tags, pushLatest: true, pushRc: false };\n }\n const [, major, minor, patch, prerelease] = match;\n const exact = prerelease\n ? `v${major}.${minor}.${patch}-${prerelease}`\n : `v${major}.${minor}.${patch}`;\n if (prerelease) {\n return {\n tags: [...tags, exact],\n pushLatest: false,\n latestHoldReason: `${exact} is a prerelease — :latest still serves the last stable image`,\n pushRc: true,\n };\n }\n return { tags: [...tags, exact, `v${major}`], pushLatest: true, pushRc: false };\n}\n\nexport interface ImageDeployResult {\n image: string;\n /** Primary pinned tag (the sha tag). */\n tag: string;\n /** All tags pushed, including the primary. */\n tags: string[];\n latestPushed: boolean;\n /** Whether the floating `:rc` prerelease-train ref was pushed. */\n rcPushed: boolean;\n digest?: string;\n}\n\nexport type DeployImagePlan =\n | { kind: \"prebuilt\"; image: string; digest: string }\n | { kind: \"build\"; image: string }\n | { kind: \"skip\"; reason: string };\n\nconst DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;\n\n/** A Railway pin is `FROM image@<digest>` — anything else cannot resolve. */\nexport function isValidImageDigest(digest: string): boolean {\n return DIGEST_PATTERN.test(digest);\n}\n\n/**\n * Decide the image leg's source: a digest handed over from a pre-pushed image\n * (the CI image job) wins; otherwise the local docker build runs when the\n * toolchain and Dockerfile are present (ADR 0021, as amended).\n */\nexport function resolveDeployImagePlan(input: {\n imageRef: ResolvedImageRef | undefined;\n imageDigest?: string;\n hasDocker: boolean;\n hasDockerfile: boolean;\n}): DeployImagePlan {\n if (!input.imageRef) {\n return {\n kind: \"skip\",\n reason: \"set ci.image in bos.config.json (or BOS_IMAGE) to build and push the runtime image\",\n };\n }\n if (input.imageDigest) {\n return { kind: \"prebuilt\", image: input.imageRef.image, digest: input.imageDigest };\n }\n if (!input.hasDocker) {\n return { kind: \"skip\", reason: \"docker is not available\" };\n }\n if (!input.hasDockerfile) {\n return { kind: \"skip\", reason: \"no Dockerfile at the config root\" };\n }\n return { kind: \"build\", image: input.imageRef.image };\n}\n\n/**\n * Image leg (ADR 0021): build the runtime stage, tag `sha-<short>` + version\n * tags, push them, capture the digest, then push the floating refs — `:latest`\n * unless the version is a prerelease, and `:rc` when it is. Callers skip the\n * leg with a notice when no image is configured (child repos get\n * build+upload+publish only).\n */\nexport async function buildAndPushImage(input: {\n image: string;\n configDir: string;\n verbose?: boolean;\n}): Promise<ImageDeployResult> {\n const shaResult = await run(\"git\", [\"rev-parse\", \"--short=7\", \"HEAD\"], {\n cwd: input.configDir,\n capture: true,\n });\n const shortSha = shaResult?.stdout.trim();\n if (!shaResult || shaResult.exitCode !== 0 || !shortSha) {\n const detail = shaResult?.stderr.trim();\n throw new Error(\n `Failed to resolve the current git SHA in ${input.configDir} (exit code ${shaResult?.exitCode ?? \"unknown\"}${detail ? `: ${detail}` : \"\"})`,\n );\n }\n\n const version = readFrameworkVersion(input.configDir);\n const { tags, pushLatest, latestHoldReason, pushRc } = computeImageTags({\n shortSha,\n version,\n });\n const refs = tags.map((tag) => `${input.image}:${tag}`);\n const latestRef = `${input.image}:latest`;\n const rcRef = `${input.image}:rc`;\n const buildRefs = pushLatest ? [...refs, latestRef] : [...refs];\n if (pushRc) buildRefs.push(rcRef);\n\n console.log(\n ` Building runtime image ${colors.cyan(buildRefs.join(\", \"))}${pushLatest ? \"\" : colors.yellow(\" (latest held)\")}`,\n );\n await run(\n \"docker\",\n [\"build\", \"--target\", \"runtime\", ...buildRefs.flatMap((ref) => [\"-t\", ref]), \".\"],\n { cwd: input.configDir },\n );\n\n console.log(` Pushing ${colors.cyan(refs.join(\", \"))}...`);\n let digest: string | undefined;\n for (const ref of refs) {\n const push = await run(\"docker\", [\"push\", ref], {\n cwd: input.configDir,\n capture: true,\n });\n if (!push || push.exitCode !== 0) {\n throw new Error(`docker push ${ref} failed with exit code ${push?.exitCode ?? \"unknown\"}`);\n }\n digest ??= parseDigest(push.stdout);\n if (input.verbose && push.stdout.trim()) {\n console.log(colors.dim(push.stdout.trim()));\n }\n }\n\n const pushFloating = async (ref: string): Promise<boolean> => {\n const push = await run(\"docker\", [\"push\", ref], {\n cwd: input.configDir,\n capture: true,\n });\n if (!push || push.exitCode !== 0) {\n throw new Error(`docker push ${ref} failed with exit code ${push?.exitCode ?? \"unknown\"}`);\n }\n digest ??= parseDigest(push.stdout);\n if (input.verbose && push.stdout.trim()) {\n console.log(colors.dim(push.stdout.trim()));\n }\n return true;\n };\n\n let latestPushed = false;\n if (pushLatest) {\n latestPushed = await pushFloating(latestRef);\n } else {\n console.log(colors.yellow(` latest held: ${latestHoldReason}`));\n }\n const rcPushed = pushRc ? await pushFloating(rcRef) : false;\n\n if (!digest) {\n console.log(colors.yellow(\" Could not capture the image digest from push output\"));\n }\n\n return { image: input.image, tag: tags[0]!, tags, latestPushed, rcPushed, digest };\n}\n\n/**\n * Railway leg (ADR 0021): pull-only deploy of the pushed image digest — a\n * thin `FROM image@digest` Dockerfile pins the digest so Railway never\n * rebuilds; `RAILWAY_DOCKERFILE_PATH` points at the generated file.\n */\nexport async function deployImageToRailway(input: {\n image: string;\n digest: string;\n service: string;\n configDir: string;\n}): Promise<void> {\n const deployDir = join(input.configDir, \".bos\", \"deploy\");\n mkdirSync(deployDir, { recursive: true });\n const dockerfilePath = join(deployDir, \"Dockerfile\");\n writeFileSync(dockerfilePath, `FROM ${input.image}@${input.digest}\\n`);\n\n console.log(` Deploying to Railway service ${colors.cyan(input.service)} (pull-only)...`);\n const result = await run(\"railway\", [\"up\", \"--service\", input.service, \"--ci\"], {\n cwd: input.configDir,\n capture: true,\n env: { RAILWAY_DOCKERFILE_PATH: \".bos/deploy/Dockerfile\" },\n });\n if (result?.stdout) {\n for (const line of result.stdout.split(\"\\n\")) {\n if (line.trim()) console.log(` ${colors.dim(line.trim())}`);\n }\n }\n if (!result || result.exitCode !== 0) {\n throw new Error(\n result?.stderr.trim()\n ? `railway up failed: ${result.stderr.trim().split(\"\\n\").slice(-3).join(\" \")}`\n : `railway up failed with exit code ${result?.exitCode ?? \"unknown\"}`,\n );\n }\n console.log(colors.green(` ${icons.ok} Railway deploy complete`));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image-deploy.mjs","names":[],"sources":["../src/image-deploy.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { run } from \"./utils/run\";\nimport { colors, icons } from \"./utils/theme\";\n\nexport interface ResolvedImageRef {\n image: string;\n source: \"ci\" | \"env\" | \"repository\";\n}\n\n/**\n * Image reference resolution precedence (ADR 0021): `ci.image` in\n * bos.config.json → `BOS_IMAGE` env → derived from `repository`.\n */\nexport function resolveImageRef(input: {\n ciImage?: string;\n repository?: string;\n env?: Record<string, string | undefined>;\n}): ResolvedImageRef | undefined {\n const envImage = input.env?.BOS_IMAGE;\n if (input.ciImage) return { image: input.ciImage, source: \"ci\" };\n if (envImage) return { image: envImage, source: \"env\" };\n\n const repo = input.repository\n ?.replace(/^https?:\\/\\/(www\\.)?github\\.com\\//i, \"\")\n .replace(/\\.git$/i, \"\")\n .replace(/\\/+$/, \"\")\n .toLowerCase();\n if (!repo?.includes(\"/\") || repo.split(\"/\").length !== 2) return undefined;\n return { image: `ghcr.io/${repo}`, source: \"repository\" };\n}\n\nexport async function hasDocker(): Promise<boolean> {\n try {\n const result = await run(\"docker\", [\"info\", \"--format\", \"{{.ServerVersion}}\"], {\n capture: true,\n });\n return result?.exitCode === 0;\n } catch {\n return false;\n }\n}\n\nfunction parseDigest(pushOutput: string): string | undefined {\n const match = pushOutput.match(/digest:\\s*(sha256:[a-f0-9]+)/i);\n return match?.[1];\n}\n\nconst SEMVER_PATTERN = /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?$/;\n\n/**\n * The runtime framework version baked into the image is the workspace source's\n * `everything-dev` package version — the image builds from `configDir`, so that\n * file is the truth. Missing (child repos without the workspace) → undefined.\n */\nfunction readFrameworkVersion(configDir: string): string | undefined {\n try {\n const raw = readFileSync(\n join(configDir, \"packages\", \"everything-dev\", \"package.json\"),\n \"utf-8\",\n );\n const pkg = JSON.parse(raw) as { version?: unknown };\n return typeof pkg.version === \"string\" && SEMVER_PATTERN.test(pkg.version)\n ? pkg.version\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface ImageTags {\n /** Exact refs to build and push, in push order (latest excluded). */\n tags: string[];\n /** Push `:latest` after the pinned tags. */\n pushLatest: boolean;\n /** Human-readable reason when `latest` is held. */\n latestHoldReason?: string;\n /** Push `:rc` after the pinned tags — the floating prerelease-train ref. */\n pushRc: boolean;\n}\n\n/**\n * Tag scheme (ADR 0021): `sha-<short>` always; the exact `v<version>` tag; a\n * floating major tag (`v2`) on stable releases only. `latest` is held during\n * prereleases (npm's `rc` dist-tag analog) so existing deployments pinned to\n * `:latest` keep serving the last stable image — and `:rc` is the floating\n * ref for the prerelease train (npm's `rc` dist-tag mirror): it moves on\n * every prerelease deploy and freezes at the last rc-train build once the\n * workspace releases a stable version.\n */\nexport function computeImageTags(input: { shortSha: string; version?: string }): ImageTags {\n const tags = [`sha-${input.shortSha}`];\n const match = input.version?.trim().match(SEMVER_PATTERN);\n if (!match) {\n return { tags, pushLatest: true, pushRc: false };\n }\n const [, major, minor, patch, prerelease] = match;\n const exact = prerelease\n ? `v${major}.${minor}.${patch}-${prerelease}`\n : `v${major}.${minor}.${patch}`;\n if (prerelease) {\n return {\n tags: [...tags, exact],\n pushLatest: false,\n latestHoldReason: `${exact} is a prerelease — :latest still serves the last stable image`,\n pushRc: true,\n };\n }\n return { tags: [...tags, exact, `v${major}`], pushLatest: true, pushRc: false };\n}\n\nexport interface ImageDeployResult {\n image: string;\n /** Primary pinned tag (the sha tag). */\n tag: string;\n /** All tags pushed, including the primary. */\n tags: string[];\n latestPushed: boolean;\n /** Whether the floating `:rc` prerelease-train ref was pushed. */\n rcPushed: boolean;\n digest?: string;\n}\n\nexport type DeployImagePlan =\n | { kind: \"prebuilt\"; image: string; digest: string }\n | { kind: \"build\"; image: string }\n | { kind: \"skip\"; reason: string };\n\nconst DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;\n\n/** A Railway pin is `FROM image@<digest>` — anything else cannot resolve. */\nexport function isValidImageDigest(digest: string): boolean {\n return DIGEST_PATTERN.test(digest);\n}\n\n/**\n * Decide the image leg's source: a digest handed over from a pre-pushed image\n * (the CI image job) wins; otherwise the local docker build runs when the\n * toolchain and Dockerfile are present (ADR 0021, as amended).\n */\nexport function resolveDeployImagePlan(input: {\n imageRef: ResolvedImageRef | undefined;\n imageDigest?: string;\n hasDocker: boolean;\n hasDockerfile: boolean;\n}): DeployImagePlan {\n if (!input.imageRef) {\n return {\n kind: \"skip\",\n reason: \"set ci.image in bos.config.json (or BOS_IMAGE) to build and push the runtime image\",\n };\n }\n if (input.imageDigest) {\n return { kind: \"prebuilt\", image: input.imageRef.image, digest: input.imageDigest };\n }\n if (!input.hasDocker) {\n return { kind: \"skip\", reason: \"docker is not available\" };\n }\n if (!input.hasDockerfile) {\n return { kind: \"skip\", reason: \"no Dockerfile at the config root\" };\n }\n return { kind: \"build\", image: input.imageRef.image };\n}\n\n/**\n * Image leg (ADR 0021): build the runtime stage, tag `sha-<short>` + version\n * tags, push them, capture the digest, then push the floating refs — `:latest`\n * unless the version is a prerelease, and `:rc` when it is. Callers skip the\n * leg with a notice when no image is configured (child repos get\n * build+upload+publish only).\n */\nexport async function buildAndPushImage(input: {\n image: string;\n configDir: string;\n verbose?: boolean;\n}): Promise<ImageDeployResult> {\n const shaResult = await run(\"git\", [\"rev-parse\", \"--short=7\", \"HEAD\"], {\n cwd: input.configDir,\n capture: true,\n });\n const shortSha = shaResult?.stdout.trim();\n if (!shaResult || shaResult.exitCode !== 0 || !shortSha) {\n const detail = shaResult?.stderr.trim();\n throw new Error(\n `Failed to resolve the current git SHA in ${input.configDir} (exit code ${shaResult?.exitCode ?? \"unknown\"}${detail ? `: ${detail}` : \"\"})`,\n );\n }\n\n const version = readFrameworkVersion(input.configDir);\n const { tags, pushLatest, latestHoldReason, pushRc } = computeImageTags({\n shortSha,\n version,\n });\n const refs = tags.map((tag) => `${input.image}:${tag}`);\n const latestRef = `${input.image}:latest`;\n const rcRef = `${input.image}:rc`;\n const buildRefs = pushLatest ? [...refs, latestRef] : [...refs];\n if (pushRc) buildRefs.push(rcRef);\n\n console.log(\n ` Building runtime image ${colors.cyan(buildRefs.join(\", \"))}${pushLatest ? \"\" : colors.yellow(\" (latest held)\")}`,\n );\n await run(\n \"docker\",\n [\"build\", \"--target\", \"runtime\", ...buildRefs.flatMap((ref) => [\"-t\", ref]), \".\"],\n { cwd: input.configDir },\n );\n\n console.log(` Pushing ${colors.cyan(refs.join(\", \"))}...`);\n let digest: string | undefined;\n for (const ref of refs) {\n const push = await run(\"docker\", [\"push\", ref], {\n cwd: input.configDir,\n capture: true,\n });\n if (!push || push.exitCode !== 0) {\n throw new Error(`docker push ${ref} failed with exit code ${push?.exitCode ?? \"unknown\"}`);\n }\n digest ??= parseDigest(push.stdout);\n if (input.verbose && push.stdout.trim()) {\n console.log(colors.dim(push.stdout.trim()));\n }\n }\n\n const pushFloating = async (ref: string): Promise<boolean> => {\n const push = await run(\"docker\", [\"push\", ref], {\n cwd: input.configDir,\n capture: true,\n });\n if (!push || push.exitCode !== 0) {\n throw new Error(`docker push ${ref} failed with exit code ${push?.exitCode ?? \"unknown\"}`);\n }\n digest ??= parseDigest(push.stdout);\n if (input.verbose && push.stdout.trim()) {\n console.log(colors.dim(push.stdout.trim()));\n }\n return true;\n };\n\n let latestPushed = false;\n if (pushLatest) {\n latestPushed = await pushFloating(latestRef);\n } else {\n console.log(colors.yellow(` latest held: ${latestHoldReason}`));\n }\n const rcPushed = pushRc ? await pushFloating(rcRef) : false;\n\n if (!digest) {\n console.log(colors.yellow(\" Could not capture the image digest from push output\"));\n }\n\n return { image: input.image, tag: tags[0]!, tags, latestPushed, rcPushed, digest };\n}\n\n/**\n * Railway leg (ADR 0021, amended): image-backed deploy. The service's source\n * is a Docker image ref (connected once in the dashboard — `:rc` during the\n * prerelease train), and `redeploy --from-source` pulls and deploys whatever\n * that ref points at. The image leg pushes `:rc` (and the `sha-<short>` /\n * version tags) immediately before this step, so the pulled bytes are exactly\n * what this train validated. Railway skips the build entirely for\n * Docker-image sources — nothing is uploaded, nothing is built on Railway,\n * and the digest-pin Dockerfile mechanism this replaces is gone (the CLI v5\n * dropped RAILWAY_DOCKERFILE_PATH, its snapshot respects .gitignore so\n * generated Dockerfiles never shipped, and its Metal builder rejected the\n * repo Dockerfile's cache-mount ids).\n */\nexport async function deployImageToRailway(input: {\n service: string;\n configDir: string;\n}): Promise<void> {\n console.log(` Deploying to Railway service ${colors.cyan(input.service)} (image-backed)...`);\n // The probe is authoritative: runners don't ship the CLI, and a spawn\n // failure resolves (reject:false) as exitCode 1 with empty output — the\n // 2026-10-13 deploy surfaced exactly that as a silent instant failure.\n let version: string | undefined;\n try {\n version = (await run(\"railway\", [\"--version\"], { cwd: input.configDir, capture: true }))\n ?.stdout;\n } catch {\n version = undefined;\n }\n if (!version?.trim()) {\n throw new Error(\"Railway CLI not found. Install it: npm i -g @railway/cli\");\n }\n console.log(` ${colors.dim(`railway ${version.trim()}`)}`);\n\n // Stream the CLI's output live — a silent exit (empty stdout+stderr, seen\n // on the 2026-10-13 deploy) must still be diagnosable from the workflow log.\n const collected = { stdout: \"\", stderr: \"\" };\n const result = await run(\n \"railway\",\n [\"redeploy\", \"--service\", input.service, \"--from-source\", \"--yes\"],\n {\n cwd: input.configDir,\n capture: true,\n onChunk: (stream, chunk) => {\n collected[stream] += chunk.toString(\"utf-8\");\n for (const line of chunk.toString(\"utf-8\").split(\"\\n\")) {\n if (line.trim()) console.log(` ${colors.dim(line.trim())}`);\n }\n },\n },\n );\n if (!result || result.exitCode !== 0) {\n const exitCode = result?.exitCode ?? \"unknown\";\n const tail = (stream: string, text: string) =>\n text.trim()\n ? `\\nrailway ${stream} (tail):\\n${text.trim().split(\"\\n\").slice(-20).join(\"\\n\")}`\n : \"\";\n throw new Error(\n `railway redeploy failed with exit code ${exitCode}` +\n tail(\"stdout\", collected.stdout) +\n tail(\"stderr\", collected.stderr),\n );\n }\n console.log(colors.green(` ${icons.ok} Railway deploy complete`));\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAgB,gBAAgB,OAIC;CAC/B,MAAM,WAAW,MAAM,KAAK;CAC5B,IAAI,MAAM,SAAS,OAAO;EAAE,OAAO,MAAM;EAAS,QAAQ;CAAK;CAC/D,IAAI,UAAU,OAAO;EAAE,OAAO;EAAU,QAAQ;CAAM;CAEtD,MAAM,OAAO,MAAM,YACf,QAAQ,sCAAsC,EAAE,CAAC,CAClD,QAAQ,WAAW,EAAE,CAAC,CACtB,QAAQ,QAAQ,EAAE,CAAC,CACnB,YAAY;CACf,IAAI,CAAC,MAAM,SAAS,GAAG,KAAK,KAAK,MAAM,GAAG,CAAC,CAAC,WAAW,GAAG,OAAO;CACjE,OAAO;EAAE,OAAO,WAAW;EAAQ,QAAQ;CAAa;AAC1D;AAEA,eAAsB,YAA8B;CAClD,IAAI;EAIF,QAAO,MAHc,IAAI,UAAU;GAAC;GAAQ;GAAY;EAAoB,GAAG,EAC7E,SAAS,KACX,CAAC,EACY,EAAE,aAAa;CAC9B,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,YAAY,YAAwC;CAE3D,OADc,WAAW,MAAM,+BACpB,CAAC,GAAG;AACjB;AAEA,MAAM,iBAAiB;;;;;;AAOvB,SAAS,qBAAqB,WAAuC;CACnE,IAAI;EACF,MAAM,MAAM,aACV,KAAK,WAAW,YAAY,kBAAkB,cAAc,GAC5D,OACF;EACA,MAAM,MAAM,KAAK,MAAM,GAAG;EAC1B,OAAO,OAAO,IAAI,YAAY,YAAY,eAAe,KAAK,IAAI,OAAO,IACrE,IAAI,UACJ;CACN,QAAQ;EACN;CACF;AACF;;;;;;;;;;AAsBA,SAAgB,iBAAiB,OAA0D;CACzF,MAAM,OAAO,CAAC,OAAO,MAAM,UAAU;CACrC,MAAM,QAAQ,MAAM,SAAS,KAAK,CAAC,CAAC,MAAM,cAAc;CACxD,IAAI,CAAC,OACH,OAAO;EAAE;EAAM,YAAY;EAAM,QAAQ;CAAM;CAEjD,MAAM,GAAG,OAAO,OAAO,OAAO,cAAc;CAC5C,MAAM,QAAQ,aACV,IAAI,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,eAC/B,IAAI,MAAM,GAAG,MAAM,GAAG;CAC1B,IAAI,YACF,OAAO;EACL,MAAM,CAAC,GAAG,MAAM,KAAK;EACrB,YAAY;EACZ,kBAAkB,GAAG,MAAM;EAC3B,QAAQ;CACV;CAEF,OAAO;EAAE,MAAM;GAAC,GAAG;GAAM;GAAO,IAAI;EAAO;EAAG,YAAY;EAAM,QAAQ;CAAM;AAChF;AAmBA,MAAM,iBAAiB;;AAGvB,SAAgB,mBAAmB,QAAyB;CAC1D,OAAO,eAAe,KAAK,MAAM;AACnC;;;;;;AAOA,SAAgB,uBAAuB,OAKnB;CAClB,IAAI,CAAC,MAAM,UACT,OAAO;EACL,MAAM;EACN,QAAQ;CACV;CAEF,IAAI,MAAM,aACR,OAAO;EAAE,MAAM;EAAY,OAAO,MAAM,SAAS;EAAO,QAAQ,MAAM;CAAY;CAEpF,IAAI,CAAC,MAAM,WACT,OAAO;EAAE,MAAM;EAAQ,QAAQ;CAA0B;CAE3D,IAAI,CAAC,MAAM,eACT,OAAO;EAAE,MAAM;EAAQ,QAAQ;CAAmC;CAEpE,OAAO;EAAE,MAAM;EAAS,OAAO,MAAM,SAAS;CAAM;AACtD;;;;;;;;AASA,eAAsB,kBAAkB,OAIT;CAC7B,MAAM,YAAY,MAAM,IAAI,OAAO;EAAC;EAAa;EAAa;CAAM,GAAG;EACrE,KAAK,MAAM;EACX,SAAS;CACX,CAAC;CACD,MAAM,WAAW,WAAW,OAAO,KAAK;CACxC,IAAI,CAAC,aAAa,UAAU,aAAa,KAAK,CAAC,UAAU;EACvD,MAAM,SAAS,WAAW,OAAO,KAAK;EACtC,MAAM,IAAI,MACR,4CAA4C,MAAM,UAAU,cAAc,WAAW,YAAY,YAAY,SAAS,KAAK,WAAW,GAAG,EAC3I;CACF;CAGA,MAAM,EAAE,MAAM,YAAY,kBAAkB,WAAW,iBAAiB;EACtE;EACA,SAHc,qBAAqB,MAAM,SAGnC;CACR,CAAC;CACD,MAAM,OAAO,KAAK,KAAK,QAAQ,GAAG,MAAM,MAAM,GAAG,KAAK;CACtD,MAAM,YAAY,GAAG,MAAM,MAAM;CACjC,MAAM,QAAQ,GAAG,MAAM,MAAM;CAC7B,MAAM,YAAY,aAAa,CAAC,GAAG,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI;CAC9D,IAAI,QAAQ,UAAU,KAAK,KAAK;CAEhC,QAAQ,IACN,4BAA4B,OAAO,KAAK,UAAU,KAAK,IAAI,CAAC,IAAI,aAAa,KAAK,OAAO,OAAO,gBAAgB,GAClH;CACA,MAAM,IACJ,UACA;EAAC;EAAS;EAAY;EAAW,GAAG,UAAU,SAAS,QAAQ,CAAC,MAAM,GAAG,CAAC;EAAG;CAAG,GAChF,EAAE,KAAK,MAAM,UAAU,CACzB;CAEA,QAAQ,IAAI,aAAa,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC,EAAE,IAAI;CAC1D,IAAI;CACJ,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,OAAO,MAAM,IAAI,UAAU,CAAC,QAAQ,GAAG,GAAG;GAC9C,KAAK,MAAM;GACX,SAAS;EACX,CAAC;EACD,IAAI,CAAC,QAAQ,KAAK,aAAa,GAC7B,MAAM,IAAI,MAAM,eAAe,IAAI,yBAAyB,MAAM,YAAY,WAAW;EAE3F,WAAW,YAAY,KAAK,MAAM;EAClC,IAAI,MAAM,WAAW,KAAK,OAAO,KAAK,GACpC,QAAQ,IAAI,OAAO,IAAI,KAAK,OAAO,KAAK,CAAC,CAAC;CAE9C;CAEA,MAAM,eAAe,OAAO,QAAkC;EAC5D,MAAM,OAAO,MAAM,IAAI,UAAU,CAAC,QAAQ,GAAG,GAAG;GAC9C,KAAK,MAAM;GACX,SAAS;EACX,CAAC;EACD,IAAI,CAAC,QAAQ,KAAK,aAAa,GAC7B,MAAM,IAAI,MAAM,eAAe,IAAI,yBAAyB,MAAM,YAAY,WAAW;EAE3F,WAAW,YAAY,KAAK,MAAM;EAClC,IAAI,MAAM,WAAW,KAAK,OAAO,KAAK,GACpC,QAAQ,IAAI,OAAO,IAAI,KAAK,OAAO,KAAK,CAAC,CAAC;EAE5C,OAAO;CACT;CAEA,IAAI,eAAe;CACnB,IAAI,YACF,eAAe,MAAM,aAAa,SAAS;MAE3C,QAAQ,IAAI,OAAO,OAAO,kBAAkB,kBAAkB,CAAC;CAEjE,MAAM,WAAW,SAAS,MAAM,aAAa,KAAK,IAAI;CAEtD,IAAI,CAAC,QACH,QAAQ,IAAI,OAAO,OAAO,uDAAuD,CAAC;CAGpF,OAAO;EAAE,OAAO,MAAM;EAAO,KAAK,KAAK;EAAK;EAAM;EAAc;EAAU;CAAO;AACnF;;;;;;;;;;;;;;AAeA,eAAsB,qBAAqB,OAGzB;CAChB,QAAQ,IAAI,kCAAkC,OAAO,KAAK,MAAM,OAAO,EAAE,mBAAmB;CAI5F,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,IAAI,WAAW,CAAC,WAAW,GAAG;GAAE,KAAK,MAAM;GAAW,SAAS;EAAK,CAAC,EAAC,EACnF;CACN,QAAQ;EACN,UAAU;CACZ;CACA,IAAI,CAAC,SAAS,KAAK,GACjB,MAAM,IAAI,MAAM,0DAA0D;CAE5E,QAAQ,IAAI,KAAK,OAAO,IAAI,WAAW,QAAQ,KAAK,GAAG,GAAG;CAI1D,MAAM,YAAY;EAAE,QAAQ;EAAI,QAAQ;CAAG;CAC3C,MAAM,SAAS,MAAM,IACnB,WACA;EAAC;EAAY;EAAa,MAAM;EAAS;EAAiB;CAAO,GACjE;EACE,KAAK,MAAM;EACX,SAAS;EACT,UAAU,QAAQ,UAAU;GAC1B,UAAU,WAAW,MAAM,SAAS,OAAO;GAC3C,KAAK,MAAM,QAAQ,MAAM,SAAS,OAAO,CAAC,CAAC,MAAM,IAAI,GACnD,IAAI,KAAK,KAAK,GAAG,QAAQ,IAAI,KAAK,OAAO,IAAI,KAAK,KAAK,CAAC,GAAG;EAE/D;CACF,CACF;CACA,IAAI,CAAC,UAAU,OAAO,aAAa,GAAG;EACpC,MAAM,WAAW,QAAQ,YAAY;EACrC,MAAM,QAAQ,QAAgB,SAC5B,KAAK,KAAK,IACN,aAAa,OAAO,YAAY,KAAK,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,MAC5E;EACN,MAAM,IAAI,MACR,0CAA0C,aACxC,KAAK,UAAU,UAAU,MAAM,IAC/B,KAAK,UAAU,UAAU,MAAM,CACnC;CACF;CACA,QAAQ,IAAI,OAAO,MAAM,KAAK,MAAM,GAAG,yBAAyB,CAAC;AACnE"}
@@ -1,5 +1,5 @@
1
+ const require_client = require('./apps/upload/src/client.cjs');
1
2
  let node_path = require("node:path");
2
- let effect = require("effect");
3
3
  let node_fs_promises = require("node:fs/promises");
4
4
 
5
5
  //#region src/storage-upload.ts
@@ -23,47 +23,12 @@ async function walkDist(dir, baseDir) {
23
23
  async function collectDistFiles(distDir) {
24
24
  return walkDist(distDir, distDir);
25
25
  }
26
- const STATUS_ATTEMPTS = 3;
27
- const TRANSPORT_ATTEMPTS = 5;
28
- const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
29
- 408,
30
- 429,
31
- 500,
32
- 502,
33
- 503,
34
- 504
35
- ]);
36
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37
- const backoffMs = (attempt) => 1e3 * attempt + Math.floor(Math.random() * 500);
38
- var BundleUploadError = class BundleUploadError extends effect.Data.TaggedError("BundleUploadError") {
39
- constructor(status, detail, note) {
40
- super({
41
- status,
42
- detail,
43
- note
44
- });
45
- }
46
- get message() {
47
- return `${this.detail}${this.note ?? ""}`;
48
- }
49
- withNote(note) {
50
- return new BundleUploadError(this.status, this.detail, `${this.note ?? ""}${note}`);
51
- }
52
- };
53
- function describeUploadFailure(status, detail) {
54
- const trimmed = detail.slice(0, 300);
55
- if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) 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)`;
56
- try {
57
- const parsed = JSON.parse(detail);
58
- if (parsed?.code === "INTERNAL_SERVER_ERROR" && parsed?.message === "Internal Server Error") return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;
59
- } catch {}
60
- return trimmed;
61
- }
62
26
  async function uploadBundle(input) {
63
- const fetchImpl = input.fetchImpl ?? fetch;
64
- const headers = { "content-type": "application/json" };
65
- if (input.apiKey) headers["x-api-key"] = input.apiKey;
66
- const body = JSON.stringify({
27
+ return await require_client.createUploadClient({
28
+ origin: input.origin,
29
+ apiKey: input.apiKey,
30
+ fetchImpl: input.fetchImpl
31
+ }).uploadBundle({
67
32
  account: input.account,
68
33
  gateway: input.gateway,
69
34
  workspace: input.workspace,
@@ -72,33 +37,6 @@ async function uploadBundle(input) {
72
37
  contentBase64: Buffer.from(file.bytes).toString("base64")
73
38
  }))
74
39
  });
75
- let lastError;
76
- let transportFailures = 0;
77
- let statusFailures = 0;
78
- for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt++) {
79
- try {
80
- const response = await fetchImpl(`${input.origin.replace(/\/$/, "")}/api/storage/bundles`, {
81
- method: "POST",
82
- headers: transportFailures > 0 ? {
83
- ...headers,
84
- connection: "close"
85
- } : headers,
86
- body
87
- });
88
- if (response.ok) return await response.json();
89
- const detail = await response.text().catch(() => "");
90
- lastError = new BundleUploadError(response.status, `[publish] bundle upload for ${input.workspace} failed: ${response.status} ${describeUploadFailure(response.status, detail)}`);
91
- if (!RETRYABLE_STATUSES.has(response.status)) break;
92
- statusFailures += 1;
93
- if (statusFailures >= STATUS_ATTEMPTS) break;
94
- } catch (error) {
95
- transportFailures += 1;
96
- lastError = new BundleUploadError(0, `[publish] bundle upload for ${input.workspace} failed: ${error instanceof Error ? error.message : String(error)}`);
97
- }
98
- if (attempt < TRANSPORT_ATTEMPTS) await sleep(backoffMs(attempt));
99
- }
100
- if (lastError instanceof BundleUploadError && lastError.status === 0) lastError = lastError.withNote(" (Bun fetch note: re-run with fetch verbose diagnostics for detail)");
101
- throw lastError ?? /* @__PURE__ */ new Error(`[publish] bundle upload for ${input.workspace} failed`);
102
40
  }
103
41
  async function uploadWorkspaceDist(input) {
104
42
  const maxRequestBytes = input.maxRequestBytes ?? (Number(process.env.BOS_MAX_BUNDLE_UPLOAD_BYTES) || 67108864);
@@ -122,6 +60,7 @@ async function uploadWorkspaceDist(input) {
122
60
  });
123
61
  merged.stored += result.stored;
124
62
  merged.totalBytes += result.totalBytes;
63
+ merged.unchanged = (merged.unchanged ?? 0) + (result.unchanged ?? 0);
125
64
  Object.assign(merged.integrity, result.integrity);
126
65
  merged.storage = result.storage;
127
66
  batch = [];
@@ -1 +1 @@
1
- {"version":3,"file":"storage-upload.cjs","names":["readdir","join","stat","relative","sep","readFile","Data"],"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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"}
1
+ {"version":3,"file":"storage-upload.cjs","names":["readdir","join","stat","relative","sep","readFile","createUploadClient"],"sources":["../src/storage-upload.ts"],"sourcesContent":["import { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\nimport { createUploadClient } from \"@everything-dev/upload-app/client\";\n\nexport interface DistFile {\n path: string;\n bytes: Uint8Array;\n}\n\nexport interface BundleUploadResult {\n stored: number;\n unchanged?: 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\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 client = createUploadClient({\n origin: input.origin,\n apiKey: input.apiKey,\n fetchImpl: input.fetchImpl,\n });\n const result = await client.uploadBundle({\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 return result;\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 merged.unchanged = (merged.unchanged ?? 0) + (result.unchanged ?? 0);\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":";;;;;AAiBA,eAAe,SAAS,KAAa,SAAsC;CACzE,MAAM,UAAU,UAAMA,0BAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;CAC1D,MAAM,QAAoB,CAAC;CAC3B,KAAK,MAAM,SAAS,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,GAAG;EACxE,MAAM,eAAWC,gBAAK,KAAK,MAAM,IAAI;EACrC,IAAI,MAAM,YAAY,GAAG;GACvB,MAAM,KAAK,GAAI,MAAM,SAAS,UAAU,OAAO,CAAE;GACjD;EACF;EAEA,IAAI,EAAC,UADcC,uBAAK,QAAQ,EACvB,CAAC,OAAO,GAAG;EACpB,MAAM,KAAK;GACT,UAAMC,oBAAS,SAAS,QAAQ,CAAC,CAAC,MAAMC,aAAG,CAAC,CAAC,KAAK,GAAG;GACrD,OAAO,IAAI,WAAW,UAAMC,2BAAS,QAAQ,CAAC;EAChD,CAAC;CACH;CACA,OAAO;AACT;AAEA,eAAsB,iBAAiB,SAAsC;CAC3E,OAAO,SAAS,SAAS,OAAO;AAClC;AAEA,eAAsB,aAAa,OAQH;CAe9B,OAAO,MAdQC,kCAAmB;EAChC,QAAQ,MAAM;EACd,QAAQ,MAAM;EACd,WAAW,MAAM;CACnB,CAC0B,CAAC,CAAC,aAAa;EACvC,SAAS,MAAM;EACf,SAAS,MAAM;EACf,WAAW,MAAM;EACjB,OAAO,MAAM,MAAM,KAAK,UAAU;GAChC,MAAM,KAAK;GACX,eAAe,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC,SAAS,QAAQ;EAC1D,EAAE;CACJ,CAAC;AAEH;AAEA,eAAsB,oBAAoB,OAQV;CAC9B,MAAM,kBACJ,MAAM,oBAAoB,OAAO,QAAQ,IAAI,2BAA2B,KAAK;CAE/E,MAAM,SAA6B;EAAE,QAAQ;EAAG,YAAY;EAAG,WAAW,CAAC;CAAE;CAC7E,IAAI,QAAoB,CAAC;CACzB,IAAI,aAAa;CACjB,MAAM,mBAAmB,SACvB,KAAK,KAAK,KAAK,MAAM,aAAa,CAAC,IAAI,IAAI,KAAK,KAAK,SAAS;CAEhE,MAAM,QAAQ,YAAY;EACxB,IAAI,MAAM,WAAW,GAAG;EACxB,MAAM,SAAS,MAAM,aAAa;GAChC,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,SAAS,MAAM;GACf,WAAW,MAAM;GACjB,OAAO;EACT,CAAC;EACD,OAAO,UAAU,OAAO;EACxB,OAAO,cAAc,OAAO;EAC5B,OAAO,aAAa,OAAO,aAAa,MAAM,OAAO,aAAa;EAClE,OAAO,OAAO,OAAO,WAAW,OAAO,SAAS;EAChD,OAAO,UAAU,OAAO;EACxB,QAAQ,CAAC;EACT,aAAa;CACf;CAEA,KAAK,MAAM,QAAQ,MAAM,OAAO;EAC9B,MAAM,OAAO,gBAAgB,IAAI;EACjC,IAAI,OAAO,iBACT,MAAM,IAAI,MACR,aAAa,MAAM,UAAU,GAAG,KAAK,KAAK,IAAI,KAAK,MAAM,WAAW,+CACtE;EAEF,IAAI,aAAa,OAAO,iBACtB,MAAM,MAAM;EAEd,MAAM,KAAK,IAAI;EACf,cAAc;CAChB;CACA,MAAM,MAAM;CAEZ,OAAO;AACT"}
@@ -1,5 +1,5 @@
1
+ import { createUploadClient } from "./apps/upload/src/client.mjs";
1
2
  import { join, relative, sep } from "node:path";
2
- import { Data } from "effect";
3
3
  import { readFile, readdir, stat } from "node:fs/promises";
4
4
 
5
5
  //#region src/storage-upload.ts
@@ -23,47 +23,12 @@ async function walkDist(dir, baseDir) {
23
23
  async function collectDistFiles(distDir) {
24
24
  return walkDist(distDir, distDir);
25
25
  }
26
- const STATUS_ATTEMPTS = 3;
27
- const TRANSPORT_ATTEMPTS = 5;
28
- const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
29
- 408,
30
- 429,
31
- 500,
32
- 502,
33
- 503,
34
- 504
35
- ]);
36
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37
- const backoffMs = (attempt) => 1e3 * attempt + Math.floor(Math.random() * 500);
38
- var BundleUploadError = class BundleUploadError extends Data.TaggedError("BundleUploadError") {
39
- constructor(status, detail, note) {
40
- super({
41
- status,
42
- detail,
43
- note
44
- });
45
- }
46
- get message() {
47
- return `${this.detail}${this.note ?? ""}`;
48
- }
49
- withNote(note) {
50
- return new BundleUploadError(this.status, this.detail, `${this.note ?? ""}${note}`);
51
- }
52
- };
53
- function describeUploadFailure(status, detail) {
54
- const trimmed = detail.slice(0, 300);
55
- if (status === 408 || /request timeout|bundle upload timed out/i.test(trimmed)) 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)`;
56
- try {
57
- const parsed = JSON.parse(detail);
58
- if (parsed?.code === "INTERNAL_SERVER_ERROR" && parsed?.message === "Internal Server Error") return `${trimmed} — the host hid the underlying error; check the host logs for the cause`;
59
- } catch {}
60
- return trimmed;
61
- }
62
26
  async function uploadBundle(input) {
63
- const fetchImpl = input.fetchImpl ?? fetch;
64
- const headers = { "content-type": "application/json" };
65
- if (input.apiKey) headers["x-api-key"] = input.apiKey;
66
- const body = JSON.stringify({
27
+ return await createUploadClient({
28
+ origin: input.origin,
29
+ apiKey: input.apiKey,
30
+ fetchImpl: input.fetchImpl
31
+ }).uploadBundle({
67
32
  account: input.account,
68
33
  gateway: input.gateway,
69
34
  workspace: input.workspace,
@@ -72,33 +37,6 @@ async function uploadBundle(input) {
72
37
  contentBase64: Buffer.from(file.bytes).toString("base64")
73
38
  }))
74
39
  });
75
- let lastError;
76
- let transportFailures = 0;
77
- let statusFailures = 0;
78
- for (let attempt = 1; attempt <= TRANSPORT_ATTEMPTS; attempt++) {
79
- try {
80
- const response = await fetchImpl(`${input.origin.replace(/\/$/, "")}/api/storage/bundles`, {
81
- method: "POST",
82
- headers: transportFailures > 0 ? {
83
- ...headers,
84
- connection: "close"
85
- } : headers,
86
- body
87
- });
88
- if (response.ok) return await response.json();
89
- const detail = await response.text().catch(() => "");
90
- lastError = new BundleUploadError(response.status, `[publish] bundle upload for ${input.workspace} failed: ${response.status} ${describeUploadFailure(response.status, detail)}`);
91
- if (!RETRYABLE_STATUSES.has(response.status)) break;
92
- statusFailures += 1;
93
- if (statusFailures >= STATUS_ATTEMPTS) break;
94
- } catch (error) {
95
- transportFailures += 1;
96
- lastError = new BundleUploadError(0, `[publish] bundle upload for ${input.workspace} failed: ${error instanceof Error ? error.message : String(error)}`);
97
- }
98
- if (attempt < TRANSPORT_ATTEMPTS) await sleep(backoffMs(attempt));
99
- }
100
- if (lastError instanceof BundleUploadError && lastError.status === 0) lastError = lastError.withNote(" (Bun fetch note: re-run with fetch verbose diagnostics for detail)");
101
- throw lastError ?? /* @__PURE__ */ new Error(`[publish] bundle upload for ${input.workspace} failed`);
102
40
  }
103
41
  async function uploadWorkspaceDist(input) {
104
42
  const maxRequestBytes = input.maxRequestBytes ?? (Number(process.env.BOS_MAX_BUNDLE_UPLOAD_BYTES) || 67108864);
@@ -122,6 +60,7 @@ async function uploadWorkspaceDist(input) {
122
60
  });
123
61
  merged.stored += result.stored;
124
62
  merged.totalBytes += result.totalBytes;
63
+ merged.unchanged = (merged.unchanged ?? 0) + (result.unchanged ?? 0);
125
64
  Object.assign(merged.integrity, result.integrity);
126
65
  merged.storage = result.storage;
127
66
  batch = [];
@@ -1 +1 @@
1
- {"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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"}
1
+ {"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 { createUploadClient } from \"@everything-dev/upload-app/client\";\n\nexport interface DistFile {\n path: string;\n bytes: Uint8Array;\n}\n\nexport interface BundleUploadResult {\n stored: number;\n unchanged?: 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\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 client = createUploadClient({\n origin: input.origin,\n apiKey: input.apiKey,\n fetchImpl: input.fetchImpl,\n });\n const result = await client.uploadBundle({\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 return result;\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 merged.unchanged = (merged.unchanged ?? 0) + (result.unchanged ?? 0);\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":";;;;;AAiBA,eAAe,SAAS,KAAa,SAAsC;CACzE,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;CAC1D,MAAM,QAAoB,CAAC;CAC3B,KAAK,MAAM,SAAS,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,GAAG;EACxE,MAAM,WAAW,KAAK,KAAK,MAAM,IAAI;EACrC,IAAI,MAAM,YAAY,GAAG;GACvB,MAAM,KAAK,GAAI,MAAM,SAAS,UAAU,OAAO,CAAE;GACjD;EACF;EAEA,IAAI,EAAC,MADc,KAAK,QAAQ,EACvB,CAAC,OAAO,GAAG;EACpB,MAAM,KAAK;GACT,MAAM,SAAS,SAAS,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG;GACrD,OAAO,IAAI,WAAW,MAAM,SAAS,QAAQ,CAAC;EAChD,CAAC;CACH;CACA,OAAO;AACT;AAEA,eAAsB,iBAAiB,SAAsC;CAC3E,OAAO,SAAS,SAAS,OAAO;AAClC;AAEA,eAAsB,aAAa,OAQH;CAe9B,OAAO,MAdQ,mBAAmB;EAChC,QAAQ,MAAM;EACd,QAAQ,MAAM;EACd,WAAW,MAAM;CACnB,CAC0B,CAAC,CAAC,aAAa;EACvC,SAAS,MAAM;EACf,SAAS,MAAM;EACf,WAAW,MAAM;EACjB,OAAO,MAAM,MAAM,KAAK,UAAU;GAChC,MAAM,KAAK;GACX,eAAe,OAAO,KAAK,KAAK,KAAK,CAAC,CAAC,SAAS,QAAQ;EAC1D,EAAE;CACJ,CAAC;AAEH;AAEA,eAAsB,oBAAoB,OAQV;CAC9B,MAAM,kBACJ,MAAM,oBAAoB,OAAO,QAAQ,IAAI,2BAA2B,KAAK;CAE/E,MAAM,SAA6B;EAAE,QAAQ;EAAG,YAAY;EAAG,WAAW,CAAC;CAAE;CAC7E,IAAI,QAAoB,CAAC;CACzB,IAAI,aAAa;CACjB,MAAM,mBAAmB,SACvB,KAAK,KAAK,KAAK,MAAM,aAAa,CAAC,IAAI,IAAI,KAAK,KAAK,SAAS;CAEhE,MAAM,QAAQ,YAAY;EACxB,IAAI,MAAM,WAAW,GAAG;EACxB,MAAM,SAAS,MAAM,aAAa;GAChC,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,SAAS,MAAM;GACf,WAAW,MAAM;GACjB,OAAO;EACT,CAAC;EACD,OAAO,UAAU,OAAO;EACxB,OAAO,cAAc,OAAO;EAC5B,OAAO,aAAa,OAAO,aAAa,MAAM,OAAO,aAAa;EAClE,OAAO,OAAO,OAAO,WAAW,OAAO,SAAS;EAChD,OAAO,UAAU,OAAO;EACxB,QAAQ,CAAC;EACT,aAAa;CACf;CAEA,KAAK,MAAM,QAAQ,MAAM,OAAO;EAC9B,MAAM,OAAO,gBAAgB,IAAI;EACjC,IAAI,OAAO,iBACT,MAAM,IAAI,MACR,aAAa,MAAM,UAAU,GAAG,KAAK,KAAK,IAAI,KAAK,MAAM,WAAW,+CACtE;EAEF,IAAI,aAAa,OAAO,iBACtB,MAAM,MAAM;EAEd,MAAM,KAAK,IAAI;EACf,cAAc;CAChB;CACA,MAAM,MAAM;CAEZ,OAAO;AACT"}
package/dist/ui/api.cjs CHANGED
@@ -1,80 +1,22 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- let _orpc_client = require("@orpc/client");
3
- let _orpc_client_fetch = require("@orpc/client/fetch");
4
2
  let _orpc_tanstack_query = require("@orpc/tanstack-query");
5
3
  let _tanstack_react_router = require("@tanstack/react-router");
4
+ let every_plugin_client = require("every-plugin/client");
6
5
 
7
6
  //#region src/ui/api.ts
8
- function createRpcLink(config, url, headers) {
9
- return new _orpc_client_fetch.RPCLink({
10
- origin: config.hostUrl,
11
- url,
12
- interceptors: [(0, _orpc_client.onError)((error) => {
13
- if (typeof window === "undefined") return;
14
- if (error && typeof error === "object" && "message" in error) {
15
- const message = String(error.message).toLowerCase();
16
- if (message.includes("fetch") || message.includes("network") || message.includes("failed to fetch")) import("sonner").then(({ toast }) => {
17
- const message = config.connectionError?.() ?? {
18
- title: "Unable to connect to API",
19
- description: "The API is currently unavailable. Please try again later."
20
- };
21
- toast.error(message.title, {
22
- id: "api-connection-error",
23
- description: message.description
24
- });
25
- }).catch(() => {});
26
- }
27
- })],
28
- fetch(fetchUrl, options) {
29
- return fetchWithRateLimitRetry(fetchUrl, {
30
- ...options,
31
- credentials: "include",
32
- headers: headers ? {
33
- ...Object.fromEntries(headers),
34
- ...options?.headers
35
- } : options?.headers
36
- });
37
- }
38
- });
39
- }
40
- const RATE_LIMIT_MAX_RETRIES = 3;
41
- const RATE_LIMIT_RETRY_CAP_MS = 2e3;
42
- /**
43
- * A 429 from the host's edge limiter is transient saturation, not an
44
- * application failure — retry within the server's `Retry-After` (capped) so
45
- * queries and the session check self-heal instead of surfacing errors.
46
- */
47
- async function fetchWithRateLimitRetry(fetchUrl, options) {
48
- let response = await fetch(fetchUrl, options);
49
- for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES && response.status === 429; attempt++) {
50
- const retryAfterSeconds = Number(response.headers.get("retry-after"));
51
- const delay = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? Math.min(retryAfterSeconds * 1e3, RATE_LIMIT_RETRY_CAP_MS) : 100 * attempt;
52
- await new Promise((resolve) => setTimeout(resolve, delay));
53
- response = await fetch(fetchUrl, options);
54
- }
55
- return response;
56
- }
57
- function buildClient(config, url, headers) {
58
- if (!config.hostUrl) throw new Error("Missing runtime host URL");
59
- return (0, _orpc_client.createORPCClient)(createRpcLink(config, url, headers));
60
- }
61
- const browserClients = /* @__PURE__ */ new Map();
62
- function memoizedClient(config, url, headers) {
63
- if (typeof window !== "undefined" && !headers) {
64
- const key = `${config.hostUrl}${url}`;
65
- const memoized = browserClients.get(key);
66
- if (memoized) return memoized;
67
- const client = buildClient(config, url, headers);
68
- browserClients.set(key, client);
69
- return client;
70
- }
71
- return buildClient(config, url, headers);
72
- }
73
7
  function createApiClient(config, headers) {
74
- return memoizedClient(config, config.rpcBase, headers);
8
+ return (0, every_plugin_client.createRpcClient)({
9
+ hostUrl: config.hostUrl,
10
+ path: config.rpcBase,
11
+ connectionError: config.connectionError
12
+ }, headers);
75
13
  }
76
14
  function createPluginApiClient(pluginKey, config, headers) {
77
- return memoizedClient(config, `${config.rpcBase}/${pluginKey}`, headers);
15
+ return (0, every_plugin_client.createRpcClient)({
16
+ hostUrl: config.hostUrl,
17
+ path: `${config.rpcBase}/${pluginKey}`,
18
+ connectionError: config.connectionError
19
+ }, headers);
78
20
  }
79
21
  function createServiceClients(config, keys, headers) {
80
22
  const clients = {};
@@ -1 +1 @@
1
- {"version":3,"file":"api.cjs","names":["RPCLink","onError","createORPCClient","useRouter","createTanstackQueryUtils"],"sources":["../../src/ui/api.ts"],"sourcesContent":["import { createORPCClient, onError } from \"@orpc/client\";\nimport { RPCLink } from \"@orpc/client/fetch\";\nimport type { ContractRouterClient, RouterContract } from \"@orpc/contract\";\nimport { createTanstackQueryUtils } from \"@orpc/tanstack-query\";\nimport { useRouter } from \"@tanstack/react-router\";\n\nexport type { RouterContract };\n\n/**\n * Localized copy for the API connection-failure toast — the app's authored\n * override seam (ADR 0023). Called at error time, so the implementation may\n * resolve the active locale itself.\n */\nexport type ApiConnectionErrorCopy = () => { title: string; description: string };\n\n/** typed client for the app's default router contract (router-context default) */\nexport type ApiClient<T extends RouterContract = RouterContract> = ContractRouterClient<T>;\n\nexport interface ClientServiceConfig {\n hostUrl: string;\n rpcBase: `/${string}`;\n connectionError?: ApiConnectionErrorCopy;\n}\n\ntype ClientRouterContext = {\n apiClient?: unknown;\n pluginClients?: Record<string, unknown>;\n};\n\nexport type { ClientRouterContext };\n\nfunction createRpcLink(config: ClientServiceConfig, url: `/${string}`, headers?: Headers) {\n return new RPCLink({\n origin: config.hostUrl,\n url,\n interceptors: [\n onError((error: unknown) => {\n if (typeof window === \"undefined\") {\n return;\n }\n\n if (error && typeof error === \"object\" && \"message\" in error) {\n const message = String(error.message).toLowerCase();\n if (\n message.includes(\"fetch\") ||\n message.includes(\"network\") ||\n message.includes(\"failed to fetch\")\n ) {\n void import(\"sonner\")\n .then(({ toast }) => {\n const message = config.connectionError?.() ?? {\n title: \"Unable to connect to API\",\n description: \"The API is currently unavailable. Please try again later.\",\n };\n toast.error(message.title, {\n id: \"api-connection-error\",\n description: message.description,\n });\n })\n .catch(() => {});\n }\n }\n }),\n ],\n fetch(fetchUrl: RequestInfo | URL, options?: RequestInit) {\n return fetchWithRateLimitRetry(fetchUrl, {\n ...options,\n credentials: \"include\",\n headers: headers\n ? { ...Object.fromEntries(headers), ...(options?.headers as Record<string, string>) }\n : options?.headers,\n });\n },\n });\n}\n\nconst RATE_LIMIT_MAX_RETRIES = 3;\nconst RATE_LIMIT_RETRY_CAP_MS = 2_000;\n\n/**\n * A 429 from the host's edge limiter is transient saturation, not an\n * application failure — retry within the server's `Retry-After` (capped) so\n * queries and the session check self-heal instead of surfacing errors.\n */\nasync function fetchWithRateLimitRetry(\n fetchUrl: RequestInfo | URL,\n options?: RequestInit,\n): Promise<Response> {\n let response = await fetch(fetchUrl, options);\n for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES && response.status === 429; attempt++) {\n const retryAfterSeconds = Number(response.headers.get(\"retry-after\"));\n const delay =\n Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0\n ? Math.min(retryAfterSeconds * 1000, RATE_LIMIT_RETRY_CAP_MS)\n : 100 * attempt;\n await new Promise((resolve) => setTimeout(resolve, delay));\n response = await fetch(fetchUrl, options);\n }\n return response;\n}\n\nfunction buildClient<T extends RouterContract>(\n config: ClientServiceConfig,\n url: `/${string}`,\n headers?: Headers,\n): ContractRouterClient<T> {\n if (!config.hostUrl) {\n throw new Error(\"Missing runtime host URL\");\n }\n return createORPCClient(createRpcLink(config, url, headers)) as ContractRouterClient<T>;\n}\n\nconst browserClients = new Map<string, unknown>();\n\nfunction memoizedClient<T extends RouterContract>(\n config: ClientServiceConfig,\n url: `/${string}`,\n headers?: Headers,\n): ContractRouterClient<T> {\n if (typeof window !== \"undefined\" && !headers) {\n const key = `${config.hostUrl}${url}`;\n const memoized = browserClients.get(key);\n if (memoized) {\n return memoized as ContractRouterClient<T>;\n }\n const client = buildClient<T>(config, url, headers);\n browserClients.set(key, client);\n return client;\n }\n\n return buildClient<T>(config, url, headers);\n}\n\nexport function createApiClient<T extends RouterContract = RouterContract>(\n config: ClientServiceConfig,\n headers?: Headers,\n): ContractRouterClient<T> {\n return memoizedClient<T>(config, config.rpcBase, headers);\n}\n\nexport function createPluginApiClient<T extends RouterContract = RouterContract>(\n pluginKey: string,\n config: ClientServiceConfig,\n headers?: Headers,\n): ContractRouterClient<T> {\n return memoizedClient<T>(config, `${config.rpcBase}/${pluginKey}`, headers);\n}\n\nexport function createServiceClients<\n C extends Record<string, RouterContract>,\n K extends keyof C & string = keyof C & string,\n>(\n config: ClientServiceConfig,\n keys: readonly K[],\n headers?: Headers,\n): Pick<{ [P in keyof C]: ContractRouterClient<C[P]> }, K> {\n const clients: Record<string, unknown> = {};\n for (const key of keys) {\n if (key === \"api\") {\n clients[key] = createApiClient<C[typeof key]>(config, headers);\n } else {\n clients[key] = createPluginApiClient<C[typeof key]>(key, config, headers);\n }\n }\n return clients as Pick<{ [P in keyof C]: ContractRouterClient<C[P]> }, K>;\n}\n\nfunction useClientContext(): ClientRouterContext {\n return useRouter().options.context as ClientRouterContext;\n}\n\nexport function useApiClient<T extends RouterContract = RouterContract>(): ContractRouterClient<T> {\n return useClientContext().apiClient as ContractRouterClient<T>;\n}\n\nexport function useOrpc<T extends RouterContract = RouterContract>() {\n return createTanstackQueryUtils(useApiClient<T>());\n}\n\nexport function usePluginClients<C extends Record<string, unknown> = Record<string, unknown>>(): C {\n return (useClientContext().pluginClients ?? {}) as C;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"api.cjs","names":["createRpcClient","useRouter","createTanstackQueryUtils"],"sources":["../../src/ui/api.ts"],"sourcesContent":["import type { ContractRouterClient, RouterContract } from \"@orpc/contract\";\nimport { createTanstackQueryUtils } from \"@orpc/tanstack-query\";\nimport { useRouter } from \"@tanstack/react-router\";\nimport { createRpcClient, type RpcConnectionErrorCopy } from \"every-plugin/client\";\n\nexport type { RouterContract };\n\n/**\n * Localized copy for the API connection-failure toast — the app's authored\n * override seam (ADR 0023). Called at error time, so the implementation may\n * resolve the active locale itself.\n */\nexport type ApiConnectionErrorCopy = RpcConnectionErrorCopy;\n\n/** typed client for the app's default router contract (router-context default) */\nexport type ApiClient<T extends RouterContract = RouterContract> = ContractRouterClient<T>;\n\nexport interface ClientServiceConfig {\n hostUrl: string;\n rpcBase: `/${string}`;\n connectionError?: ApiConnectionErrorCopy;\n}\n\ntype ClientRouterContext = {\n apiClient?: unknown;\n pluginClients?: Record<string, unknown>;\n};\n\nexport type { ClientRouterContext };\n\nexport function createApiClient<T extends RouterContract = RouterContract>(\n config: ClientServiceConfig,\n headers?: Headers,\n): ContractRouterClient<T> {\n return createRpcClient<T>(\n {\n hostUrl: config.hostUrl,\n path: config.rpcBase,\n connectionError: config.connectionError,\n },\n headers,\n );\n}\n\nexport function createPluginApiClient<T extends RouterContract = RouterContract>(\n pluginKey: string,\n config: ClientServiceConfig,\n headers?: Headers,\n): ContractRouterClient<T> {\n return createRpcClient<T>(\n {\n hostUrl: config.hostUrl,\n path: `${config.rpcBase}/${pluginKey}`,\n connectionError: config.connectionError,\n },\n headers,\n );\n}\n\nexport function createServiceClients<\n C extends Record<string, RouterContract>,\n K extends keyof C & string = keyof C & string,\n>(\n config: ClientServiceConfig,\n keys: readonly K[],\n headers?: Headers,\n): Pick<{ [P in keyof C]: ContractRouterClient<C[P]> }, K> {\n const clients: Record<string, unknown> = {};\n for (const key of keys) {\n if (key === \"api\") {\n clients[key] = createApiClient<C[typeof key]>(config, headers);\n } else {\n clients[key] = createPluginApiClient<C[typeof key]>(key, config, headers);\n }\n }\n return clients as Pick<{ [P in keyof C]: ContractRouterClient<C[P]> }, K>;\n}\n\nfunction useClientContext(): ClientRouterContext {\n return useRouter().options.context as ClientRouterContext;\n}\n\nexport function useApiClient<T extends RouterContract = RouterContract>(): ContractRouterClient<T> {\n return useClientContext().apiClient as ContractRouterClient<T>;\n}\n\nexport function useOrpc<T extends RouterContract = RouterContract>() {\n return createTanstackQueryUtils(useApiClient<T>());\n}\n\nexport function usePluginClients<C extends Record<string, unknown> = Record<string, unknown>>(): C {\n return (useClientContext().pluginClients ?? {}) as C;\n}\n"],"mappings":";;;;;;AA8BA,SAAgB,gBACd,QACA,SACyB;CACzB,WAAOA,qCACL;EACE,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,iBAAiB,OAAO;CAC1B,GACA,OACF;AACF;AAEA,SAAgB,sBACd,WACA,QACA,SACyB;CACzB,WAAOA,qCACL;EACE,SAAS,OAAO;EAChB,MAAM,GAAG,OAAO,QAAQ,GAAG;EAC3B,iBAAiB,OAAO;CAC1B,GACA,OACF;AACF;AAEA,SAAgB,qBAId,QACA,MACA,SACyD;CACzD,MAAM,UAAmC,CAAC;CAC1C,KAAK,MAAM,OAAO,MAChB,IAAI,QAAQ,OACV,QAAQ,OAAO,gBAA+B,QAAQ,OAAO;MAE7D,QAAQ,OAAO,sBAAqC,KAAK,QAAQ,OAAO;CAG5E,OAAO;AACT;AAEA,SAAS,mBAAwC;CAC/C,WAAOC,kCAAU,CAAC,CAAC,QAAQ;AAC7B;AAEA,SAAgB,eAAmF;CACjG,OAAO,iBAAiB,CAAC,CAAC;AAC5B;AAEA,SAAgB,UAAqD;CACnE,WAAOC,+CAAyB,aAAgB,CAAC;AACnD;AAEA,SAAgB,mBAAmF;CACjG,OAAQ,iBAAiB,CAAC,CAAC,iBAAiB,CAAC;AAC/C"}
package/dist/ui/api.d.cts CHANGED
@@ -1,14 +1,12 @@
1
1
  import { ContractRouterClient, RouterContract } from "@orpc/contract";
2
+ import { RpcConnectionErrorCopy } from "every-plugin/client";
2
3
  //#region src/ui/api.d.ts
3
4
  /**
4
5
  * Localized copy for the API connection-failure toast — the app's authored
5
6
  * override seam (ADR 0023). Called at error time, so the implementation may
6
7
  * resolve the active locale itself.
7
8
  */
8
- type ApiConnectionErrorCopy = () => {
9
- title: string;
10
- description: string;
11
- };
9
+ type ApiConnectionErrorCopy = RpcConnectionErrorCopy;
12
10
  /** typed client for the app's default router contract (router-context default) */
13
11
  type ApiClient<T extends RouterContract = RouterContract> = ContractRouterClient<T>;
14
12
  interface ClientServiceConfig {
package/dist/ui/api.d.mts CHANGED
@@ -1,14 +1,12 @@
1
1
  import { ContractRouterClient, RouterContract } from "@orpc/contract";
2
+ import { RpcConnectionErrorCopy } from "every-plugin/client";
2
3
  //#region src/ui/api.d.ts
3
4
  /**
4
5
  * Localized copy for the API connection-failure toast — the app's authored
5
6
  * override seam (ADR 0023). Called at error time, so the implementation may
6
7
  * resolve the active locale itself.
7
8
  */
8
- type ApiConnectionErrorCopy = () => {
9
- title: string;
10
- description: string;
11
- };
9
+ type ApiConnectionErrorCopy = RpcConnectionErrorCopy;
12
10
  /** typed client for the app's default router contract (router-context default) */
13
11
  type ApiClient<T extends RouterContract = RouterContract> = ContractRouterClient<T>;
14
12
  interface ClientServiceConfig {
package/dist/ui/api.mjs CHANGED
@@ -1,79 +1,21 @@
1
- import { createORPCClient, onError } from "@orpc/client";
2
- import { RPCLink } from "@orpc/client/fetch";
3
1
  import { createTanstackQueryUtils } from "@orpc/tanstack-query";
4
2
  import { useRouter } from "@tanstack/react-router";
3
+ import { createRpcClient } from "every-plugin/client";
5
4
 
6
5
  //#region src/ui/api.ts
7
- function createRpcLink(config, url, headers) {
8
- return new RPCLink({
9
- origin: config.hostUrl,
10
- url,
11
- interceptors: [onError((error) => {
12
- if (typeof window === "undefined") return;
13
- if (error && typeof error === "object" && "message" in error) {
14
- const message = String(error.message).toLowerCase();
15
- if (message.includes("fetch") || message.includes("network") || message.includes("failed to fetch")) import("sonner").then(({ toast }) => {
16
- const message = config.connectionError?.() ?? {
17
- title: "Unable to connect to API",
18
- description: "The API is currently unavailable. Please try again later."
19
- };
20
- toast.error(message.title, {
21
- id: "api-connection-error",
22
- description: message.description
23
- });
24
- }).catch(() => {});
25
- }
26
- })],
27
- fetch(fetchUrl, options) {
28
- return fetchWithRateLimitRetry(fetchUrl, {
29
- ...options,
30
- credentials: "include",
31
- headers: headers ? {
32
- ...Object.fromEntries(headers),
33
- ...options?.headers
34
- } : options?.headers
35
- });
36
- }
37
- });
38
- }
39
- const RATE_LIMIT_MAX_RETRIES = 3;
40
- const RATE_LIMIT_RETRY_CAP_MS = 2e3;
41
- /**
42
- * A 429 from the host's edge limiter is transient saturation, not an
43
- * application failure — retry within the server's `Retry-After` (capped) so
44
- * queries and the session check self-heal instead of surfacing errors.
45
- */
46
- async function fetchWithRateLimitRetry(fetchUrl, options) {
47
- let response = await fetch(fetchUrl, options);
48
- for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES && response.status === 429; attempt++) {
49
- const retryAfterSeconds = Number(response.headers.get("retry-after"));
50
- const delay = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? Math.min(retryAfterSeconds * 1e3, RATE_LIMIT_RETRY_CAP_MS) : 100 * attempt;
51
- await new Promise((resolve) => setTimeout(resolve, delay));
52
- response = await fetch(fetchUrl, options);
53
- }
54
- return response;
55
- }
56
- function buildClient(config, url, headers) {
57
- if (!config.hostUrl) throw new Error("Missing runtime host URL");
58
- return createORPCClient(createRpcLink(config, url, headers));
59
- }
60
- const browserClients = /* @__PURE__ */ new Map();
61
- function memoizedClient(config, url, headers) {
62
- if (typeof window !== "undefined" && !headers) {
63
- const key = `${config.hostUrl}${url}`;
64
- const memoized = browserClients.get(key);
65
- if (memoized) return memoized;
66
- const client = buildClient(config, url, headers);
67
- browserClients.set(key, client);
68
- return client;
69
- }
70
- return buildClient(config, url, headers);
71
- }
72
6
  function createApiClient(config, headers) {
73
- return memoizedClient(config, config.rpcBase, headers);
7
+ return createRpcClient({
8
+ hostUrl: config.hostUrl,
9
+ path: config.rpcBase,
10
+ connectionError: config.connectionError
11
+ }, headers);
74
12
  }
75
13
  function createPluginApiClient(pluginKey, config, headers) {
76
- return memoizedClient(config, `${config.rpcBase}/${pluginKey}`, headers);
14
+ return createRpcClient({
15
+ hostUrl: config.hostUrl,
16
+ path: `${config.rpcBase}/${pluginKey}`,
17
+ connectionError: config.connectionError
18
+ }, headers);
77
19
  }
78
20
  function createServiceClients(config, keys, headers) {
79
21
  const clients = {};