refactor(cli): define server connection boundary (#37133)
This commit is contained in:
parent
e8bd386973
commit
f5dd181443
14 changed files with 250 additions and 121 deletions
|
|
@ -118,6 +118,7 @@ const appBindingCommands = [
|
|||
"provider.connect",
|
||||
"opencode.status",
|
||||
"server.pair",
|
||||
"service.restart",
|
||||
"opencode.debug",
|
||||
"theme.switch",
|
||||
"theme.switch_mode",
|
||||
|
|
@ -138,8 +139,10 @@ const appBindingCommands = [
|
|||
export type TuiInput = {
|
||||
server: {
|
||||
endpoint: Service.Endpoint
|
||||
reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
|
||||
reload?: () => Promise<void>
|
||||
service?: {
|
||||
reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
|
||||
restart: () => Promise<void>
|
||||
}
|
||||
}
|
||||
args: Args
|
||||
config: Config.Interface
|
||||
|
|
@ -183,14 +186,15 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||
Effect.catch(() => Effect.tryPromise(() => api.location.get()).pipe(Effect.map((response) => response.directory))),
|
||||
)
|
||||
const handoff = input.terminalHandoff ? yield* Effect.promise(input.terminalHandoff) : undefined
|
||||
const reconnectEndpoint = input.server.reconnect
|
||||
const reconnect = reconnectEndpoint
|
||||
? async (onStatus: (status: Service.Status) => void, signal: AbortSignal) => {
|
||||
const endpoint = await reconnectEndpoint(onStatus, signal)
|
||||
const next = { baseUrl: endpoint.url, headers: Service.headers(endpoint) }
|
||||
return {
|
||||
api: OpenCode.make(next),
|
||||
}
|
||||
const managed = input.server.service
|
||||
const service = managed
|
||||
? {
|
||||
reconnect: async (onStatus: (status: Service.Status) => void, signal: AbortSignal) => {
|
||||
const endpoint = await managed.reconnect(onStatus, signal)
|
||||
const next = { baseUrl: endpoint.url, headers: Service.headers(endpoint) }
|
||||
return { api: OpenCode.make(next) }
|
||||
},
|
||||
restart: managed.restart,
|
||||
}
|
||||
: undefined
|
||||
const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
|
||||
|
|
@ -324,7 +328,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||
}
|
||||
>
|
||||
<PluginRuntimeProvider value={pluginRuntime}>
|
||||
<ClientProvider api={api} reconnect={reconnect} reload={input.server.reload}>
|
||||
<ClientProvider api={api} service={service}>
|
||||
<PermissionProvider>
|
||||
<DataProvider>
|
||||
<LocationProvider>
|
||||
|
|
@ -757,19 +761,21 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||
},
|
||||
category: "System",
|
||||
},
|
||||
...(client.reload
|
||||
...(client.restart
|
||||
? [
|
||||
{
|
||||
name: "server.reload",
|
||||
title: "Reload server",
|
||||
slash: { name: "reload" },
|
||||
name: "service.restart",
|
||||
title: "Restart service",
|
||||
slash: { name: "restart" },
|
||||
run: async () => {
|
||||
const restart = client.restart
|
||||
if (!restart) return
|
||||
dialog.clear()
|
||||
toast.show({ variant: "info", message: "Reloading server...", duration: 30000 })
|
||||
// reload resolves once the replacement service is healthy; the
|
||||
toast.show({ variant: "info", message: "Restarting service...", duration: 30000 })
|
||||
// restart resolves once the replacement service is healthy; the
|
||||
// event stream reattaches through the reconnect loop.
|
||||
await client.reload!()
|
||||
.then(() => toast.show({ variant: "success", message: "Server reloaded" }))
|
||||
await restart()
|
||||
.then(() => toast.show({ variant: "success", message: "Service restarted" }))
|
||||
.catch(toast.error)
|
||||
},
|
||||
category: "System",
|
||||
|
|
|
|||
|
|
@ -18,18 +18,18 @@ export type ClientConnectionEvent = {
|
|||
}
|
||||
}
|
||||
|
||||
type ManagedService = {
|
||||
reconnect: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<{ api: OpenCodeClient }>
|
||||
restart: () => Promise<void>
|
||||
}
|
||||
|
||||
type ClientEventMap = { [Type in OpenCodeEvent["type"]]: Extract<OpenCodeEvent, { type: Type }> }
|
||||
const connectTimeout = 2_000
|
||||
const connectionHistoryLimit = 50
|
||||
|
||||
export const { use: useClient, provider: ClientProvider } = createSimpleContext({
|
||||
name: "Client",
|
||||
init: (props: {
|
||||
api: OpenCodeClient
|
||||
reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<{ api: OpenCodeClient }>
|
||||
// Stops and starts the managed service; present only in service mode.
|
||||
reload?: () => Promise<void>
|
||||
}) => {
|
||||
init: (props: { api: OpenCodeClient; service?: ManagedService }) => {
|
||||
const log = useLog({ component: "client" })
|
||||
const abort = new AbortController()
|
||||
const history: ClientConnectionEvent[] = []
|
||||
|
|
@ -115,8 +115,8 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
|
|||
// Re-resolve the transport before retrying: the server may have
|
||||
// moved (service restarted on a new port) or need starting. Static
|
||||
// transports (--server, standalone) resolve to the same address.
|
||||
if (props.reconnect) {
|
||||
const next = await props.reconnect(setService, controller.signal).catch((error) => {
|
||||
if (props.service) {
|
||||
const next = await props.service.reconnect(setService, controller.signal).catch((error) => {
|
||||
if (!controller.signal.aborted)
|
||||
log.info("server resolution failed", {
|
||||
attempt,
|
||||
|
|
@ -168,7 +168,7 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
|
|||
},
|
||||
},
|
||||
},
|
||||
reload: props.reload,
|
||||
restart: props.service?.restart,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue