mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/wsclient: reconnect automatically if disconnected
This commit is contained in:
parent
7d6bbe77b9
commit
894fcb3fa0
6 changed files with 112 additions and 20 deletions
|
@ -41,15 +41,41 @@ function App() {
|
|||
}, [client])
|
||||
useEffect(() => client.start(), [client])
|
||||
|
||||
if (connState?.error) {
|
||||
return <div>
|
||||
{`${connState.error} \u{1F63F}`}
|
||||
const afterConnectError = Boolean(connState?.error && connState.reconnecting && clientState?.is_verified)
|
||||
useEffect(() => {
|
||||
if (afterConnectError) {
|
||||
const cancelKeys = (evt: KeyboardEvent | MouseEvent) => evt.stopPropagation()
|
||||
document.body.addEventListener("keydown", cancelKeys, { capture: true })
|
||||
document.body.addEventListener("keyup", cancelKeys, { capture: true })
|
||||
document.body.addEventListener("click", cancelKeys, { capture: true })
|
||||
return () => {
|
||||
document.body.removeEventListener("keydown", cancelKeys, { capture: true })
|
||||
document.body.removeEventListener("keyup", cancelKeys, { capture: true })
|
||||
document.body.removeEventListener("click", cancelKeys, { capture: true })
|
||||
}
|
||||
}
|
||||
}, [afterConnectError])
|
||||
const errorOverlay = connState?.error ? <div
|
||||
className={`connection-error-wrapper ${afterConnectError ? "post-connect" : ""}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="connection-error-inner">
|
||||
<div>{connState.error} 😿</div>
|
||||
{connState.reconnecting && <div>
|
||||
<ScaleLoader width="2rem" height="2rem" color="var(--primary-color)"/>
|
||||
Reconnecting to backend...
|
||||
{connState.nextAttempt ? <div><small>(next attempt at {connState.nextAttempt})</small></div> : null}
|
||||
</div>}
|
||||
</div>
|
||||
} else if (!connState?.connected || !clientState) {
|
||||
</div> : null
|
||||
|
||||
if (connState?.error && !afterConnectError) {
|
||||
return errorOverlay
|
||||
} else if ((!connState?.connected && !afterConnectError) || !clientState) {
|
||||
const msg = connState?.connected ?
|
||||
"Waiting for client state..." : "Connecting to backend..."
|
||||
return <div>
|
||||
<ScaleLoader/>
|
||||
return <div className="pre-connect">
|
||||
<ScaleLoader width="2rem" height="2rem" color="var(--primary-color)"/>
|
||||
{msg}
|
||||
</div>
|
||||
} else if (!clientState.is_logged_in) {
|
||||
|
@ -61,6 +87,7 @@ function App() {
|
|||
<LightboxWrapper>
|
||||
<MainScreen/>
|
||||
</LightboxWrapper>
|
||||
{errorOverlay}
|
||||
</ClientContext>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,16 +52,16 @@ export default class Client {
|
|||
method: "POST",
|
||||
signal,
|
||||
})
|
||||
if (!resp.ok) {
|
||||
if (!resp.ok && !signal.aborted) {
|
||||
this.rpc.connect.emit({
|
||||
connected: false,
|
||||
error: new Error(`Authentication failed: ${resp.statusText}`),
|
||||
reconnecting: false,
|
||||
error: `Authentication failed: ${resp.statusText}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(`${err}`)
|
||||
this.rpc.connect.emit({ connected: false, error })
|
||||
this.rpc.connect.emit({ connected: false, reconnecting: false, error: `Authentication failed: ${err}` })
|
||||
}
|
||||
if (signal.aborted) {
|
||||
return
|
||||
|
|
|
@ -42,7 +42,9 @@ import type {
|
|||
|
||||
export interface ConnectionEvent {
|
||||
connected: boolean
|
||||
error: Error | null
|
||||
reconnecting: boolean
|
||||
error: string | null
|
||||
nextAttempt?: string
|
||||
}
|
||||
|
||||
export class ErrorResponse extends Error {
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class WailsClient extends RPCClient {
|
|||
this.event.emit(evt.data[0])
|
||||
})
|
||||
this.#wails.Call.ByName("main.CommandHandler.Init")
|
||||
this.connect.emit({ connected: true, error: null })
|
||||
this.connect.emit({ connected: true, reconnecting: false, error: null })
|
||||
}
|
||||
|
||||
async stop() {}
|
||||
|
|
|
@ -25,6 +25,9 @@ export default class WSClient extends RPCClient {
|
|||
#pingInterval: number | null = null
|
||||
#lastReceivedEvt: number = 0
|
||||
#resumeRunID: string = ""
|
||||
#stopped = false
|
||||
#reconnectTimeout: number | null = null
|
||||
#connectFailures: number = 0
|
||||
|
||||
constructor(readonly addr: string) {
|
||||
super()
|
||||
|
@ -32,6 +35,7 @@ export default class WSClient extends RPCClient {
|
|||
|
||||
start() {
|
||||
try {
|
||||
this.#stopped = false
|
||||
this.#lastMessage = Date.now()
|
||||
const params = new URLSearchParams({
|
||||
run_id: this.#resumeRunID,
|
||||
|
@ -44,9 +48,8 @@ export default class WSClient extends RPCClient {
|
|||
this.#conn.onopen = this.#onOpen
|
||||
this.#conn.onerror = this.#onError
|
||||
this.#conn.onclose = this.#onClose
|
||||
this.#pingInterval = setInterval(this.#pingLoop, PING_INTERVAL)
|
||||
} catch (err) {
|
||||
this.#dispatchConnectionStatus(false, err as Error)
|
||||
this.#dispatchConnectionStatus(false, false, `Failed to create websocket: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,8 +69,10 @@ export default class WSClient extends RPCClient {
|
|||
}
|
||||
|
||||
stop() {
|
||||
this.#stopped = true
|
||||
if (this.#pingInterval !== null) {
|
||||
clearInterval(this.#pingInterval)
|
||||
this.#pingInterval = null
|
||||
}
|
||||
this.#conn?.close(1000, "Client closed")
|
||||
}
|
||||
|
@ -105,13 +110,20 @@ export default class WSClient extends RPCClient {
|
|||
this.onCommand(parsed)
|
||||
}
|
||||
|
||||
#dispatchConnectionStatus(connected: boolean, error: Error | null) {
|
||||
this.connect.emit({ connected, error })
|
||||
#dispatchConnectionStatus(connected: boolean, reconnecting: boolean, error: string | null, nextAttempt?: number) {
|
||||
this.connect.emit({
|
||||
connected,
|
||||
reconnecting,
|
||||
error,
|
||||
nextAttempt: nextAttempt ? new Date(nextAttempt).toLocaleTimeString() : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
#onOpen = () => {
|
||||
console.info("Websocket opened")
|
||||
this.#dispatchConnectionStatus(true, null)
|
||||
this.#dispatchConnectionStatus(true, false, null)
|
||||
this.#connectFailures = 0
|
||||
this.#pingInterval = setInterval(this.#pingLoop, PING_INTERVAL)
|
||||
}
|
||||
|
||||
#clearPending = () => {
|
||||
|
@ -123,13 +135,33 @@ export default class WSClient extends RPCClient {
|
|||
|
||||
#onError = (ev: Event) => {
|
||||
console.error("Websocket error:", ev)
|
||||
this.#dispatchConnectionStatus(false, new Error("Websocket error"))
|
||||
this.#clearPending()
|
||||
}
|
||||
|
||||
#onClose = (ev: CloseEvent) => {
|
||||
this.#connectFailures++
|
||||
console.warn("Websocket closed:", ev)
|
||||
this.#dispatchConnectionStatus(false, new Error(`Websocket closed: ${ev.code} ${ev.reason}`))
|
||||
this.#clearPending()
|
||||
if (this.#pingInterval !== null) {
|
||||
clearInterval(this.#pingInterval)
|
||||
this.#pingInterval = null
|
||||
}
|
||||
const willReconnect = !this.#stopped && !this.#reconnectTimeout
|
||||
const backoff = Math.min(2 ** (this.#connectFailures - 4), 10) * 1000
|
||||
this.#dispatchConnectionStatus(
|
||||
false,
|
||||
willReconnect,
|
||||
`Websocket closed: ${ev.code} ${ev.reason}`,
|
||||
Date.now() + backoff,
|
||||
)
|
||||
if (willReconnect) {
|
||||
console.log("Attempting to reconnect in", backoff, "ms")
|
||||
this.#reconnectTimeout = setTimeout(() => {
|
||||
console.log("Reconnecting now")
|
||||
this.#reconnectTimeout = null
|
||||
this.start()
|
||||
}, backoff)
|
||||
} else {
|
||||
console.log(`Not reconnecting (stopped=${this.#stopped}, reconnectTimeout=${this.#reconnectTimeout})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -206,6 +206,37 @@ button, a.button, span.button {
|
|||
}
|
||||
}
|
||||
|
||||
div.connection-error-wrapper {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
&.post-connect {
|
||||
background-color: var(--dimmed-overlay-background-color);
|
||||
}
|
||||
|
||||
> div.connection-error-inner {
|
||||
background-color: var(--background-color);
|
||||
border: 2px solid var(--error-color);
|
||||
border-radius: .5rem;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
div.pre-connect {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--link-text-color);
|
||||
|
|
Loading…
Add table
Reference in a new issue