mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33: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])
|
}, [client])
|
||||||
useEffect(() => client.start(), [client])
|
useEffect(() => client.start(), [client])
|
||||||
|
|
||||||
if (connState?.error) {
|
const afterConnectError = Boolean(connState?.error && connState.reconnecting && clientState?.is_verified)
|
||||||
return <div>
|
useEffect(() => {
|
||||||
{`${connState.error} \u{1F63F}`}
|
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>
|
</div>
|
||||||
} else if (!connState?.connected || !clientState) {
|
</div> : null
|
||||||
|
|
||||||
|
if (connState?.error && !afterConnectError) {
|
||||||
|
return errorOverlay
|
||||||
|
} else if ((!connState?.connected && !afterConnectError) || !clientState) {
|
||||||
const msg = connState?.connected ?
|
const msg = connState?.connected ?
|
||||||
"Waiting for client state..." : "Connecting to backend..."
|
"Waiting for client state..." : "Connecting to backend..."
|
||||||
return <div>
|
return <div className="pre-connect">
|
||||||
<ScaleLoader/>
|
<ScaleLoader width="2rem" height="2rem" color="var(--primary-color)"/>
|
||||||
{msg}
|
{msg}
|
||||||
</div>
|
</div>
|
||||||
} else if (!clientState.is_logged_in) {
|
} else if (!clientState.is_logged_in) {
|
||||||
|
@ -61,6 +87,7 @@ function App() {
|
||||||
<LightboxWrapper>
|
<LightboxWrapper>
|
||||||
<MainScreen/>
|
<MainScreen/>
|
||||||
</LightboxWrapper>
|
</LightboxWrapper>
|
||||||
|
{errorOverlay}
|
||||||
</ClientContext>
|
</ClientContext>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,16 +52,16 @@ export default class Client {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
if (!resp.ok) {
|
if (!resp.ok && !signal.aborted) {
|
||||||
this.rpc.connect.emit({
|
this.rpc.connect.emit({
|
||||||
connected: false,
|
connected: false,
|
||||||
error: new Error(`Authentication failed: ${resp.statusText}`),
|
reconnecting: false,
|
||||||
|
error: `Authentication failed: ${resp.statusText}`,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err instanceof Error ? err : new Error(`${err}`)
|
this.rpc.connect.emit({ connected: false, reconnecting: false, error: `Authentication failed: ${err}` })
|
||||||
this.rpc.connect.emit({ connected: false, error })
|
|
||||||
}
|
}
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -42,7 +42,9 @@ import type {
|
||||||
|
|
||||||
export interface ConnectionEvent {
|
export interface ConnectionEvent {
|
||||||
connected: boolean
|
connected: boolean
|
||||||
error: Error | null
|
reconnecting: boolean
|
||||||
|
error: string | null
|
||||||
|
nextAttempt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ErrorResponse extends Error {
|
export class ErrorResponse extends Error {
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default class WailsClient extends RPCClient {
|
||||||
this.event.emit(evt.data[0])
|
this.event.emit(evt.data[0])
|
||||||
})
|
})
|
||||||
this.#wails.Call.ByName("main.CommandHandler.Init")
|
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() {}
|
async stop() {}
|
||||||
|
|
|
@ -25,6 +25,9 @@ export default class WSClient extends RPCClient {
|
||||||
#pingInterval: number | null = null
|
#pingInterval: number | null = null
|
||||||
#lastReceivedEvt: number = 0
|
#lastReceivedEvt: number = 0
|
||||||
#resumeRunID: string = ""
|
#resumeRunID: string = ""
|
||||||
|
#stopped = false
|
||||||
|
#reconnectTimeout: number | null = null
|
||||||
|
#connectFailures: number = 0
|
||||||
|
|
||||||
constructor(readonly addr: string) {
|
constructor(readonly addr: string) {
|
||||||
super()
|
super()
|
||||||
|
@ -32,6 +35,7 @@ export default class WSClient extends RPCClient {
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
try {
|
try {
|
||||||
|
this.#stopped = false
|
||||||
this.#lastMessage = Date.now()
|
this.#lastMessage = Date.now()
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
run_id: this.#resumeRunID,
|
run_id: this.#resumeRunID,
|
||||||
|
@ -44,9 +48,8 @@ export default class WSClient extends RPCClient {
|
||||||
this.#conn.onopen = this.#onOpen
|
this.#conn.onopen = this.#onOpen
|
||||||
this.#conn.onerror = this.#onError
|
this.#conn.onerror = this.#onError
|
||||||
this.#conn.onclose = this.#onClose
|
this.#conn.onclose = this.#onClose
|
||||||
this.#pingInterval = setInterval(this.#pingLoop, PING_INTERVAL)
|
|
||||||
} catch (err) {
|
} 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() {
|
stop() {
|
||||||
|
this.#stopped = true
|
||||||
if (this.#pingInterval !== null) {
|
if (this.#pingInterval !== null) {
|
||||||
clearInterval(this.#pingInterval)
|
clearInterval(this.#pingInterval)
|
||||||
|
this.#pingInterval = null
|
||||||
}
|
}
|
||||||
this.#conn?.close(1000, "Client closed")
|
this.#conn?.close(1000, "Client closed")
|
||||||
}
|
}
|
||||||
|
@ -105,13 +110,20 @@ export default class WSClient extends RPCClient {
|
||||||
this.onCommand(parsed)
|
this.onCommand(parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
#dispatchConnectionStatus(connected: boolean, error: Error | null) {
|
#dispatchConnectionStatus(connected: boolean, reconnecting: boolean, error: string | null, nextAttempt?: number) {
|
||||||
this.connect.emit({ connected, error })
|
this.connect.emit({
|
||||||
|
connected,
|
||||||
|
reconnecting,
|
||||||
|
error,
|
||||||
|
nextAttempt: nextAttempt ? new Date(nextAttempt).toLocaleTimeString() : undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#onOpen = () => {
|
#onOpen = () => {
|
||||||
console.info("Websocket opened")
|
console.info("Websocket opened")
|
||||||
this.#dispatchConnectionStatus(true, null)
|
this.#dispatchConnectionStatus(true, false, null)
|
||||||
|
this.#connectFailures = 0
|
||||||
|
this.#pingInterval = setInterval(this.#pingLoop, PING_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
#clearPending = () => {
|
#clearPending = () => {
|
||||||
|
@ -123,13 +135,33 @@ export default class WSClient extends RPCClient {
|
||||||
|
|
||||||
#onError = (ev: Event) => {
|
#onError = (ev: Event) => {
|
||||||
console.error("Websocket error:", ev)
|
console.error("Websocket error:", ev)
|
||||||
this.#dispatchConnectionStatus(false, new Error("Websocket error"))
|
|
||||||
this.#clearPending()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#onClose = (ev: CloseEvent) => {
|
#onClose = (ev: CloseEvent) => {
|
||||||
|
this.#connectFailures++
|
||||||
console.warn("Websocket closed:", ev)
|
console.warn("Websocket closed:", ev)
|
||||||
this.#dispatchConnectionStatus(false, new Error(`Websocket closed: ${ev.code} ${ev.reason}`))
|
|
||||||
this.#clearPending()
|
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 {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--link-text-color);
|
color: var(--link-text-color);
|
||||||
|
|
Loading…
Add table
Reference in a new issue