fix(tui): suppress transient reconnect overlay flashes (#34924)
This commit is contained in:
parent
f016392368
commit
7ec7413fdb
4 changed files with 98 additions and 2 deletions
|
|
@ -1116,6 +1116,34 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||||
return render({ params: route.data.data })
|
return render({ params: route.data.data })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Suppress the full-screen reconnecting overlay for transient disconnects (initial startup, host
|
||||||
|
// reload, sub-second event-stream blips). After the first successful connect, show it only once the
|
||||||
|
// connection has been lost for a full second; before the first connect give a longer grace period so
|
||||||
|
// startup never flashes it, but a server that dies before ever connecting still surfaces instead of
|
||||||
|
// leaving a silent empty app. Hide it immediately the moment status leaves "connecting".
|
||||||
|
const [showReconnecting, setShowReconnecting] = createSignal(false)
|
||||||
|
let reconnectTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
createEffect(() => {
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer)
|
||||||
|
reconnectTimer = undefined
|
||||||
|
}
|
||||||
|
if (sdk.connection.status() !== "connecting") {
|
||||||
|
setShowReconnecting(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reconnectTimer = setTimeout(
|
||||||
|
() => {
|
||||||
|
reconnectTimer = undefined
|
||||||
|
setShowReconnecting(true)
|
||||||
|
},
|
||||||
|
sdk.connection.connectedOnce() ? 1000 : 5000,
|
||||||
|
).unref()
|
||||||
|
})
|
||||||
|
onCleanup(() => {
|
||||||
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
width={dimensions().width}
|
width={dimensions().width}
|
||||||
|
|
@ -1161,7 +1189,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||||
<Show when={!startup.skipInitialLoading}>
|
<Show when={!startup.skipInitialLoading}>
|
||||||
<StartupLoading ready={ready} />
|
<StartupLoading ready={ready} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={sdk.connection.status() === "connecting"}>
|
<Show when={showReconnecting()}>
|
||||||
<Reconnecting attempt={sdk.connection.attempt()} error={sdk.connection.error()} />
|
<Reconnecting attempt={sdk.connection.attempt()} error={sdk.connection.error()} />
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -636,6 +636,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
error() {
|
error() {
|
||||||
return sdk.connection.error()
|
return sdk.connection.error()
|
||||||
},
|
},
|
||||||
|
connectedOnce() {
|
||||||
|
return sdk.connection.connectedOnce()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
list() {
|
list() {
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,11 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
status: SDKConnectionStatus
|
status: SDKConnectionStatus
|
||||||
attempt: number
|
attempt: number
|
||||||
error?: string
|
error?: string
|
||||||
|
connectedOnce: boolean
|
||||||
}>({
|
}>({
|
||||||
status: "connecting",
|
status: "connecting",
|
||||||
attempt: 0,
|
attempt: 0,
|
||||||
|
connectedOnce: false,
|
||||||
})
|
})
|
||||||
let stream: AbortController | undefined
|
let stream: AbortController | undefined
|
||||||
let pending: Promise<void> | undefined
|
let pending: Promise<void> | undefined
|
||||||
|
|
@ -68,7 +70,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
attempt = 0
|
attempt = 0
|
||||||
events.emit(first.value.type, first.value)
|
events.emit(first.value.type, first.value)
|
||||||
setConnection({ status: "connected", attempt: 0, error: undefined })
|
setConnection({ status: "connected", attempt: 0, error: undefined, connectedOnce: true })
|
||||||
connected()
|
connected()
|
||||||
while (!abort.signal.aborted && !controller.signal.aborted) {
|
while (!abort.signal.aborted && !controller.signal.aborted) {
|
||||||
const event = await iterator.next()
|
const event = await iterator.next()
|
||||||
|
|
@ -140,6 +142,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
error() {
|
error() {
|
||||||
return connection.error
|
return connection.error
|
||||||
},
|
},
|
||||||
|
connectedOnce() {
|
||||||
|
return connection.connectedOnce
|
||||||
|
},
|
||||||
},
|
},
|
||||||
reload,
|
reload,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,66 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("connectedOnce is false until first connect and persists across disconnect", async () => {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
let stream: ReadableStreamDefaultController<Uint8Array> | undefined
|
||||||
|
const eventResponse = () =>
|
||||||
|
new Response(
|
||||||
|
new ReadableStream<Uint8Array>({
|
||||||
|
start(controller) {
|
||||||
|
stream = controller
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ headers: { "content-type": "text/event-stream" } },
|
||||||
|
)
|
||||||
|
const connect = () =>
|
||||||
|
stream?.enqueue(
|
||||||
|
encoder.encode(`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n`),
|
||||||
|
)
|
||||||
|
const disconnect = () => {
|
||||||
|
stream?.close()
|
||||||
|
stream = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname === "/api/event") return eventResponse()
|
||||||
|
})
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
return <box />
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await testRender(() => (
|
||||||
|
<TestTuiContexts>
|
||||||
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
|
<ProjectProvider>
|
||||||
|
<DataProvider>
|
||||||
|
<Probe />
|
||||||
|
</DataProvider>
|
||||||
|
</ProjectProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</TestTuiContexts>
|
||||||
|
))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wait(() => stream !== undefined)
|
||||||
|
expect(data.connection.status()).toBe("connecting")
|
||||||
|
expect(data.connection.connectedOnce()).toBe(false)
|
||||||
|
|
||||||
|
connect()
|
||||||
|
await wait(() => data.connection.status() === "connected")
|
||||||
|
expect(data.connection.connectedOnce()).toBe(true)
|
||||||
|
|
||||||
|
disconnect()
|
||||||
|
await wait(() => data.connection.status() === "connecting")
|
||||||
|
expect(data.connection.connectedOnce()).toBe(true)
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("tracks session status from active sessions and execution events", async () => {
|
test("tracks session status from active sessions and execution events", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch((url) => {
|
const calls = createFetch((url) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue