packages/client/src/service-probe.ts
Rama dev · réplica de lectura del repositorio original.
import { homedir } from "node:os"
import { join } from "node:path"
import type { DiscoverOptions, Endpoint, Info } from "./service.js"
import { defaultEnsureTiming } from "./service-timing.js"
import { matchesVersion } from "./service-version.js"
// Shared by the Effect and Promise clients. Keep this module free of Effect so the
// Promise client never loads the Effect runtime.
export type LocalService = {
readonly info: Info
readonly endpoint: Endpoint
readonly version?: string
readonly state: "ready" | "waiting" | "failed"
readonly compatible: boolean
}
/**
* What `ensure()` does with a registered service that answered: return it, keep polling while
* it starts, replace it because its version does not meet the requirement, or fail.
*/
export function decide(service: LocalService, options: DiscoverOptions) {
if (!matchesVersion(service.version, options))
return { _tag: "replace", pty: service.state === "ready" ? "handoff" : "clear" } as const
if (!service.compatible)
return fail(
"Background service uses an incompatible health protocol. Update this client or explicitly restart the service.",
)
if (service.state === "ready") return { _tag: "reuse" } as const
if (service.state === "failed") return fail("Background service failed to start")
return { _tag: "wait" } as const
}
function fail(message: string) {
return { _tag: "fail" as const, error: new Error(message) }
}
/** The default registration file. */
export function fallback() {
return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json")
}
/** Whether two registrations describe the same service instance. */
export function same(left: Info, right: Info) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
}
/** Ask a registered owner for its health and classify the answer. */
export async function probeResult(info: Info, timeout = defaultEnsureTiming.requestTimeout) {
const endpoint = {
url: info.url,
auth: info.password === undefined ? undefined : { type: "basic", username: "opencode", password: info.password },
} satisfies Endpoint
const signal = AbortSignal.timeout(timeout)
const result = await fetch(new URL("/api/info", info.url), { headers: headers(endpoint), signal })
.then(async (response) => ({
response,
body: response.status === 404 ? undefined : ((await response.json()) as unknown),
}))
.catch(() => undefined)
if (result === undefined) return { service: undefined, timedOut: signal.aborted }
const response = result.response
// A missing health endpoint identifies protocol incompatibility, not an older
// version. Only an unmet version requirement lets ensure replace this owner.
if (response.status === 404)
return {
service: {
info,
endpoint,
version: info.version,
state: "ready",
compatible: false,
} satisfies LocalService,
timedOut: false,
}
const serverInfo = decodeInfo(result.body)
if (serverInfo === undefined) return { service: undefined, timedOut: false }
if (serverInfo.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && serverInfo.version !== info.version) return { service: undefined, timedOut: false }
return {
service: {
info,
endpoint,
version: serverInfo.version,
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
compatible: true,
} satisfies LocalService,
timedOut: false,
}
}
/** Create HTTP authentication headers for a service endpoint. */
export function headers(endpoint: Endpoint) {
if (endpoint.auth === undefined) return undefined
return {
authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64"),
}
}
function decodeInfo(input: unknown) {
if (typeof input !== "object" || input === null) return
if (!("version" in input) || typeof input.version !== "string") return
if (!("pid" in input) || typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid < 0) return
return { version: input.version, pid: input.pid }
}