fix(cli): restart stale clients after updates
This commit is contained in:
parent
f9d1d3b259
commit
910af9a122
13 changed files with 246 additions and 25 deletions
|
|
@ -30,7 +30,7 @@ import { PluginRouteMissing } from "./component/plugin-route-missing"
|
|||
import { ProjectProvider, useProject } from "./context/project"
|
||||
import { EditorContextProvider } from "./context/editor"
|
||||
import { useEvent } from "./context/event"
|
||||
import { SDKProvider, useSDK } from "./context/sdk"
|
||||
import { SDKProvider, useSDK, type SDKDiscovery } from "./context/sdk"
|
||||
import { StartupLoading } from "./component/startup-loading"
|
||||
import { Reconnecting } from "./component/reconnecting"
|
||||
import { SyncProvider, useSync } from "./context/sync"
|
||||
|
|
@ -138,7 +138,8 @@ const appBindingCommands = [
|
|||
export type TuiInput = {
|
||||
client: OpencodeClient
|
||||
api: OpenCodeClient
|
||||
discover?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
|
||||
discover?: () => Promise<SDKDiscovery>
|
||||
restart?: () => void
|
||||
reload?: () => Promise<void>
|
||||
args: Args
|
||||
config: TuiConfig.Resolved
|
||||
|
|
@ -302,7 +303,16 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||
>
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<PluginRuntimeProvider value={pluginRuntime}>
|
||||
<SDKProvider client={input.client} api={input.api} discover={input.discover} reload={input.reload}>
|
||||
<SDKProvider
|
||||
client={input.client}
|
||||
api={input.api}
|
||||
discover={input.discover}
|
||||
restart={() => {
|
||||
input.restart?.()
|
||||
destroyRenderer(renderer)
|
||||
}}
|
||||
reload={input.reload}
|
||||
>
|
||||
<PermissionProvider>
|
||||
<ProjectProvider>
|
||||
<SyncProvider>
|
||||
|
|
@ -812,8 +822,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
|||
toast.show({ variant: "info", message: "Reloading server...", duration: 30000 })
|
||||
// reload resolves once the replacement service is healthy; the
|
||||
// event stream reattaches through the reconnect loop.
|
||||
await sdk
|
||||
.reload!()
|
||||
await sdk.reload!()
|
||||
.then(() => toast.show({ variant: "success", message: "Server reloaded" }))
|
||||
.catch(toast.error)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,12 +10,15 @@ export type SDKConnectionStatus = "connected" | "connecting"
|
|||
type SDKEventMap = { [Type in V2Event["type"]]: Extract<V2Event, { type: Type }> }
|
||||
const connectTimeout = 2_000
|
||||
|
||||
export type SDKDiscovery = { client: OpencodeClient; api: OpenCodeClient } | { restart: true }
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: {
|
||||
client: OpencodeClient
|
||||
api: OpenCodeClient
|
||||
discover?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
|
||||
discover?: () => Promise<SDKDiscovery>
|
||||
restart?: () => void
|
||||
// Stops and starts the managed service; present only in service mode.
|
||||
reload?: () => Promise<void>
|
||||
}) => {
|
||||
|
|
@ -66,7 +69,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
return connection.signal.reason instanceof Error
|
||||
? connection.signal.reason
|
||||
: new Error("Event stream disconnected")
|
||||
if (first.value.type !== "server.connected") return new Error("Event stream did not start with server.connected")
|
||||
if (first.value.type !== "server.connected")
|
||||
return new Error("Event stream did not start with server.connected")
|
||||
clearTimeout(timeout)
|
||||
attempt = 0
|
||||
events.emit(first.value.type, first.value)
|
||||
|
|
@ -93,6 +97,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||
const next = await props.discover().catch(() => undefined)
|
||||
if (abort.signal.aborted || controller.signal.aborted) return
|
||||
if (next) {
|
||||
if ("restart" in next) {
|
||||
props.restart?.()
|
||||
return
|
||||
}
|
||||
client = next.client
|
||||
api = next.api
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { OpenCodeClient } from "@opencode-ai/client/promise"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { onMount } from "solid-js"
|
||||
import { ProjectProvider, useProject } from "../../../src/context/project"
|
||||
import { SDKProvider, useSDK } from "../../../src/context/sdk"
|
||||
import { SDKProvider, useSDK, type SDKDiscovery } from "../../../src/context/sdk"
|
||||
import { useEvent } from "../../../src/context/event"
|
||||
import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
|
@ -49,7 +48,7 @@ function update(version: string): V2Event {
|
|||
}
|
||||
}
|
||||
|
||||
async function mount(discover?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>) {
|
||||
async function mount(discover?: () => Promise<SDKDiscovery>, restart?: () => void) {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(undefined, events)
|
||||
const seen: V2Event[] = []
|
||||
|
|
@ -63,7 +62,12 @@ async function mount(discover?: () => Promise<{ client: OpencodeClient; api: Ope
|
|||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)} discover={discover}>
|
||||
<SDKProvider
|
||||
client={createClient(calls.fetch)}
|
||||
api={createApi(calls.fetch)}
|
||||
discover={discover}
|
||||
restart={restart}
|
||||
>
|
||||
<ProjectProvider>
|
||||
<Probe
|
||||
onReady={async (ctx) => {
|
||||
|
|
@ -198,4 +202,25 @@ describe("useEvent", () => {
|
|||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("requests a restart when discovery finds a newer service", async () => {
|
||||
let restarts = 0
|
||||
const { app, events, sdk } = await mount(
|
||||
async () => ({ restart: true }),
|
||||
() => {
|
||||
restarts += 1
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
await wait(() => sdk.connection.status() === "connected")
|
||||
events.disconnect()
|
||||
await wait(() => restarts === 1)
|
||||
await Bun.sleep(300)
|
||||
|
||||
expect(restarts).toBe(1)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue