{"version":3,"file":"index.mjs","names":[],"sources":["../src/runtime-constants.ts","../src/runtime-control.ts","../src/ping-project.ts","../src/ping-project-target.ts","../src/deploy-status.ts"],"sourcesContent":["/** Timeout for a single project runtime `/health` probe. */\nexport const RUNTIME_PING_TIMEOUT_MS = 15_000;\n","import { PROJECT_SERVER_PORT } from \"./constants\";\nimport { RUNTIME_PING_TIMEOUT_MS } from \"./runtime-constants\";\n\nexport const PROJECT_SERVER_CONTROL = {\n  load: \"/control/projects/load\",\n  promote: \"/control/projects/promote\",\n  unload: \"/control/projects/unload\",\n  port: PROJECT_SERVER_PORT,\n} as const;\n\nexport type LoadProjectOnRuntimeInput = {\n  projectId: string;\n  artifactId: string;\n  artifactVersion: number;\n  storageKey: string;\n  activate?: boolean;\n};\n\nexport type RuntimeControlOptions = {\n  fetchImpl?: typeof fetch;\n  timeoutMs?: number;\n  /** Bearer token — typically the org's WORKER_INTERNAL_TOKEN. */\n  workerToken?: string;\n};\n\nexport class RuntimeControlUnavailableError extends Error {\n  readonly status: number | undefined;\n\n  constructor(message: string, options: { status?: number; cause?: unknown } = {}) {\n    super(message, { cause: options.cause });\n    this.name = \"RuntimeControlUnavailableError\";\n    this.status = options.status;\n  }\n}\n\nasync function postControl<T>(\n  baseUrl: string,\n  path: string,\n  body: unknown,\n  options: RuntimeControlOptions = {},\n): Promise<T> {\n  const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n  const headers: Record<string, string> = {\n    \"content-type\": \"application/json\",\n  };\n  if (options.workerToken) {\n    headers.authorization = `Bearer ${options.workerToken}`;\n  }\n\n  let response: Response;\n  try {\n    response = await (options.fetchImpl ?? fetch)(new URL(path, baseUrl), {\n      method: \"POST\",\n      headers,\n      body: JSON.stringify(body),\n      signal: AbortSignal.timeout(timeoutMs),\n    });\n  } catch (cause) {\n    throw new RuntimeControlUnavailableError(`Runtime control ${path} unavailable`, { cause });\n  }\n\n  const data = (await response.json().catch(() => ({}))) as T & { ok?: boolean; error?: string };\n  const message =\n    typeof data.error === \"string\"\n      ? data.error\n      : `Runtime control ${path} failed (${response.status})`;\n  if (!response.ok || data.ok === false) {\n    if (response.status === 404 || response.status >= 500) {\n      throw new RuntimeControlUnavailableError(message, { status: response.status });\n    }\n    throw new Error(message);\n  }\n\n  return data;\n}\n\nexport async function loadProjectOnRuntime(\n  baseUrl: string,\n  input: LoadProjectOnRuntimeInput,\n  options: RuntimeControlOptions = {},\n): Promise<void> {\n  await postControl(baseUrl, PROJECT_SERVER_CONTROL.load, input, options);\n}\n\nexport async function promoteProjectOnRuntime(\n  baseUrl: string,\n  input: { projectId: string; artifactId: string },\n  options: RuntimeControlOptions = {},\n): Promise<void> {\n  await postControl(baseUrl, PROJECT_SERVER_CONTROL.promote, input, options);\n}\n\nexport async function unloadProjectOnRuntime(\n  baseUrl: string,\n  input: { projectId: string; artifactId: string; force?: boolean },\n  options: RuntimeControlOptions = {},\n): Promise<void> {\n  await postControl(baseUrl, PROJECT_SERVER_CONTROL.unload, input, options);\n}\n\n/** True when the org runtime answers /health. */\nexport async function pingRuntimeHealth(\n  baseUrl: string,\n  options: RuntimeControlOptions = {},\n): Promise<boolean> {\n  const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n  try {\n    const response = await (options.fetchImpl ?? fetch)(new URL(\"/health\", baseUrl), {\n      signal: AbortSignal.timeout(timeoutMs),\n    });\n    return response.ok;\n  } catch {\n    return false;\n  }\n}\n","import { PROJECT_SERVER_HEALTH } from \"./constants\";\nimport type { HostingPlugin } from \"./plugin\";\nimport { RUNTIME_PING_TIMEOUT_MS } from \"./runtime-constants\";\n\nexport type PingProjectOptions = {\n  runtimeId?: string | null;\n  fetchImpl?: typeof fetch;\n  timeoutMs?: number;\n  plugin?: Pick<HostingPlugin, \"pingRequestHeaders\">;\n};\n\nexport async function pingProject(\n  baseUrl: string,\n  options: PingProjectOptions = {},\n): Promise<boolean> {\n  const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n  const headers = options.plugin?.pingRequestHeaders?.(options.runtimeId ?? null) ?? {};\n\n  try {\n    const response = await (options.fetchImpl ?? fetch)(\n      new URL(PROJECT_SERVER_HEALTH.path, baseUrl),\n      {\n        signal: AbortSignal.timeout(timeoutMs),\n        headers,\n      },\n    );\n\n    return response.ok;\n  } catch {\n    return false;\n  }\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport type { ProjectPingTarget } from \"./runtime\";\nimport { pingProject, type PingProjectOptions } from \"./ping-project\";\n\nexport function canPingProjectTarget(\n  target: ProjectPingTarget,\n  plugin?: Pick<HostingPlugin, \"canPingTarget\">,\n): target is { baseUrl: string; runtimeId: string | null } {\n  if (plugin?.canPingTarget) {\n    return plugin.canPingTarget(target);\n  }\n\n  return !!target.baseUrl;\n}\n\nexport async function pingProjectTarget(\n  target: ProjectPingTarget,\n  options: PingProjectOptions & {\n    plugin?: Pick<HostingPlugin, \"canPingTarget\" | \"pingRequestHeaders\">;\n  } = {},\n): Promise<boolean> {\n  if (!canPingProjectTarget(target, options.plugin)) {\n    return false;\n  }\n\n  return pingProject(target.baseUrl, {\n    ...options,\n    runtimeId: target.runtimeId,\n  });\n}\n","import { PROJECT_SERVER_DEPLOY_STATUS } from \"./constants\";\nimport { RUNTIME_PING_TIMEOUT_MS } from \"./runtime-constants\";\n\nconst DEFAULT_WAIT_TIMEOUT_MS = 120_000;\nconst DEFAULT_WAIT_INTERVAL_MS = 500;\nconst REQUEST_TIMEOUT_MS = 2_000;\n\n/** Per-project bootstrap outcome reported by an org runtime machine. */\nexport type ProjectDeployStatus = {\n  projectId: string;\n  ok: boolean;\n  error?: string;\n};\n\nexport type VersionDeployStatus = {\n  projectId: string;\n  artifactId: string;\n  state: \"active\" | \"resident\" | \"indexed\";\n  ok: boolean;\n  activeJobCount?: number;\n  error?: string;\n};\n\nexport type DeployStatusResponse = {\n  projects: ProjectDeployStatus[];\n  versions?: VersionDeployStatus[];\n};\n\nexport type FetchDeployStatusOptions = {\n  fetchImpl?: typeof fetch;\n  timeoutMs?: number;\n};\n\nexport type WaitForDeployStatusOptions = {\n  fetchImpl?: typeof fetch;\n  timeoutMs?: number;\n  intervalMs?: number;\n};\n\n/**\n * Ask a running org machine which projects bootstrapped and which failed.\n * Returns undefined when the endpoint is unreachable, non-OK, times out, or\n * returns an unusable payload — callers must treat that as non-affirmative\n * health for every pending project (never promote on silence).\n */\nexport async function fetchDeployStatus(\n  baseUrl: string,\n  options: FetchDeployStatusOptions = {},\n): Promise<DeployStatusResponse | undefined> {\n  const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n\n  try {\n    const response = await (options.fetchImpl ?? fetch)(\n      new URL(PROJECT_SERVER_DEPLOY_STATUS.path, baseUrl),\n      { signal: AbortSignal.timeout(timeoutMs) },\n    );\n\n    if (!response.ok) {\n      return undefined;\n    }\n\n    const data = (await response.json()) as DeployStatusResponse;\n    if (!data || !Array.isArray(data.projects)) {\n      return undefined;\n    }\n\n    return data;\n  } catch (error) {\n    console.warn(`[deploy-status] failed to fetch from ${baseUrl}:`, error);\n    return undefined;\n  }\n}\n\n/**\n * Poll `/deploy-status` until the real worker returns a usable payload or the\n * deadline expires. Retries only transient unavailability (network errors,\n * non-2xx including early-health 404, timeouts, malformed bodies). Any valid\n * `{ projects: [...] }` response — including empty or explicit failures — is\n * terminal so genuine bootstrap errors surface immediately.\n */\nexport async function waitForDeployStatus(\n  baseUrl: string,\n  options: WaitForDeployStatusOptions = {},\n): Promise<DeployStatusResponse | undefined> {\n  const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;\n  const intervalMs = options.intervalMs ?? DEFAULT_WAIT_INTERVAL_MS;\n  const deadline = Date.now() + timeoutMs;\n\n  while (Date.now() < deadline) {\n    const remainingMs = deadline - Date.now();\n    const requestTimeoutMs = Math.min(REQUEST_TIMEOUT_MS, Math.max(1, remainingMs));\n    const status = await fetchDeployStatus(baseUrl, {\n      fetchImpl: options.fetchImpl,\n      timeoutMs: requestTimeoutMs,\n    });\n    if (status) {\n      return status;\n    }\n\n    if (Date.now() + intervalMs >= deadline) {\n      break;\n    }\n    await sleep(intervalMs);\n  }\n\n  return undefined;\n}\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => {\n    setTimeout(resolve, ms);\n  });\n}\n\nexport type DeployHealthVerdict = { ok: true } | { ok: false; error: string };\n\n/**\n * Require an explicit `{ projectId, ok: true }` entry for each candidate.\n * Missing entries, `ok: false`, empty `projects`, or an unavailable status\n * response all fail closed for that project.\n */\nexport function classifyDeployHealth(input: {\n  projectIds: string[];\n  deployStatus: DeployStatusResponse | undefined;\n}): Map<string, DeployHealthVerdict> {\n  const verdicts = new Map<string, DeployHealthVerdict>();\n\n  if (!input.deployStatus) {\n    for (const projectId of input.projectIds) {\n      verdicts.set(projectId, {\n        ok: false,\n        error: \"Deploy health status unavailable\",\n      });\n    }\n    return verdicts;\n  }\n\n  const byId = new Map(\n    input.deployStatus.projects\n      .filter((entry) => typeof entry?.projectId === \"string\" && entry.projectId.length > 0)\n      .map((entry) => [entry.projectId, entry]),\n  );\n\n  for (const projectId of input.projectIds) {\n    const entry = byId.get(projectId);\n    if (!entry) {\n      verdicts.set(projectId, {\n        ok: false,\n        error: \"Deploy health status omitted project\",\n      });\n      continue;\n    }\n    if (entry.ok !== true) {\n      verdicts.set(projectId, {\n        ok: false,\n        error: entry.error?.trim() || \"Project failed to start\",\n      });\n      continue;\n    }\n    verdicts.set(projectId, { ok: true });\n  }\n\n  return verdicts;\n}\n"],"mappings":"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"}