Added websocket reconnect with backoff
This commit is contained in:
parent
64f2fa012e
commit
d19c50cf77
3 changed files with 107 additions and 18 deletions
3
site/src/stores/agentBackoff.ts
Normal file
3
site/src/stores/agentBackoff.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const BACKOFF_BASE_MS = 1000
|
||||||
|
export const BACKOFF_MAX_MS = 30000
|
||||||
|
export const BACKOFF_MAX_ATTEMPTS = 6
|
||||||
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
AgentSocketEventPayload,
|
AgentSocketEventPayload,
|
||||||
AgentStartPayload,
|
AgentStartPayload,
|
||||||
} from '../types/agent'
|
} from '../types/agent'
|
||||||
|
import { BACKOFF_BASE_MS, BACKOFF_MAX_MS, BACKOFF_MAX_ATTEMPTS } from './agentBackoff'
|
||||||
|
|
||||||
export const useAgentStore = defineStore('agent', () => {
|
export const useAgentStore = defineStore('agent', () => {
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
|
|
@ -14,6 +15,11 @@ export const useAgentStore = defineStore('agent', () => {
|
||||||
const lastExecutionId = ref<string | null>(null)
|
const lastExecutionId = ref<string | null>(null)
|
||||||
const socket = ref<WebSocket | null>(null)
|
const socket = ref<WebSocket | null>(null)
|
||||||
|
|
||||||
|
let currentUrl = ''
|
||||||
|
let reconnectAttempts = 0
|
||||||
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let intentionalClose = false
|
||||||
|
|
||||||
const pushEvent = (evt: AgentSocketEventPayload) => {
|
const pushEvent = (evt: AgentSocketEventPayload) => {
|
||||||
eventLog.value.unshift({
|
eventLog.value.unshift({
|
||||||
type: evt.type,
|
type: evt.type,
|
||||||
|
|
@ -23,17 +29,32 @@ export const useAgentStore = defineStore('agent', () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const connect = (id: string) => {
|
const clearReconnectTimer = () => {
|
||||||
if (socket.value) {
|
if (reconnectTimer !== null) {
|
||||||
socket.value.close()
|
clearTimeout(reconnectTimer)
|
||||||
socket.value = null
|
reconnectTimer = null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
const scheduleReconnect = () => {
|
||||||
const wsUrl = `${wsProtocol}://${window.location.host}/ws/onboarding/chat/${id}/`
|
if (reconnectAttempts >= BACKOFF_MAX_ATTEMPTS) {
|
||||||
socket.value = new WebSocket(wsUrl)
|
pushEvent({ type: 'error', message: 'Connection lost. Please refresh the page.' })
|
||||||
|
executionStatus.value = 'idle'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const delay = Math.min(BACKOFF_BASE_MS * 2 ** reconnectAttempts, BACKOFF_MAX_MS)
|
||||||
|
reconnectAttempts++
|
||||||
|
pushEvent({ type: 'status', message: `Reconnecting in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempts}/${BACKOFF_MAX_ATTEMPTS})...` })
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
if (!intentionalClose) openSocket(currentUrl)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSocket = (url: string) => {
|
||||||
|
socket.value = new WebSocket(url)
|
||||||
|
|
||||||
socket.value.onopen = () => {
|
socket.value.onopen = () => {
|
||||||
|
reconnectAttempts = 0
|
||||||
isConnected.value = true
|
isConnected.value = true
|
||||||
pushEvent({ type: 'status', message: 'Connected to Orchestrator' })
|
pushEvent({ type: 'status', message: 'Connected to Orchestrator' })
|
||||||
}
|
}
|
||||||
|
|
@ -81,13 +102,35 @@ export const useAgentStore = defineStore('agent', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.value.onclose = () => {
|
socket.value.onclose = (event) => {
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
executionStatus.value = 'idle'
|
// code 1000 = clean close (server finished normally); don't reconnect
|
||||||
|
if (!intentionalClose && event.code !== 1000) {
|
||||||
|
scheduleReconnect()
|
||||||
|
} else {
|
||||||
|
executionStatus.value = 'idle'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connect = (id: string) => {
|
||||||
|
intentionalClose = false
|
||||||
|
clearReconnectTimer()
|
||||||
|
reconnectAttempts = 0
|
||||||
|
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.close()
|
||||||
|
socket.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
currentUrl = `${wsProtocol}://${window.location.host}/ws/onboarding/chat/${id}/`
|
||||||
|
openSocket(currentUrl)
|
||||||
|
}
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
|
intentionalClose = true
|
||||||
|
clearReconnectTimer()
|
||||||
if (socket.value) {
|
if (socket.value) {
|
||||||
socket.value.close()
|
socket.value.close()
|
||||||
socket.value = null
|
socket.value = null
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
AgentSocketEventPayload,
|
AgentSocketEventPayload,
|
||||||
AgentStartPayload,
|
AgentStartPayload,
|
||||||
} from '../types/agent'
|
} from '../types/agent'
|
||||||
|
import { BACKOFF_BASE_MS, BACKOFF_MAX_MS, BACKOFF_MAX_ATTEMPTS } from './agentBackoff'
|
||||||
|
|
||||||
export const useOnboardingAgentStore = defineStore('onboarding-agent', () => {
|
export const useOnboardingAgentStore = defineStore('onboarding-agent', () => {
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
|
|
@ -14,6 +15,11 @@ export const useOnboardingAgentStore = defineStore('onboarding-agent', () => {
|
||||||
const lastExecutionId = ref<string | null>(null)
|
const lastExecutionId = ref<string | null>(null)
|
||||||
const socket = ref<WebSocket | null>(null)
|
const socket = ref<WebSocket | null>(null)
|
||||||
|
|
||||||
|
let currentUrl = ''
|
||||||
|
let reconnectAttempts = 0
|
||||||
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let intentionalClose = false
|
||||||
|
|
||||||
const pushEvent = (evt: AgentSocketEventPayload) => {
|
const pushEvent = (evt: AgentSocketEventPayload) => {
|
||||||
eventLog.value.unshift({
|
eventLog.value.unshift({
|
||||||
type: evt.type,
|
type: evt.type,
|
||||||
|
|
@ -23,17 +29,32 @@ export const useOnboardingAgentStore = defineStore('onboarding-agent', () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const connect = (id: string) => {
|
const clearReconnectTimer = () => {
|
||||||
if (socket.value) {
|
if (reconnectTimer !== null) {
|
||||||
socket.value.close()
|
clearTimeout(reconnectTimer)
|
||||||
socket.value = null
|
reconnectTimer = null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
const scheduleReconnect = () => {
|
||||||
const wsUrl = `${wsProtocol}://${window.location.host}/ws/onboarding/generate/${id}/`
|
if (reconnectAttempts >= BACKOFF_MAX_ATTEMPTS) {
|
||||||
socket.value = new WebSocket(wsUrl)
|
pushEvent({ type: 'error', message: 'Connection lost. Please refresh the page.' })
|
||||||
|
executionStatus.value = 'idle'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const delay = Math.min(BACKOFF_BASE_MS * 2 ** reconnectAttempts, BACKOFF_MAX_MS)
|
||||||
|
reconnectAttempts++
|
||||||
|
pushEvent({ type: 'status', message: `Reconnecting in ${Math.round(delay / 1000)}s (attempt ${reconnectAttempts}/${BACKOFF_MAX_ATTEMPTS})...` })
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
if (!intentionalClose) openSocket(currentUrl)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSocket = (url: string) => {
|
||||||
|
socket.value = new WebSocket(url)
|
||||||
|
|
||||||
socket.value.onopen = () => {
|
socket.value.onopen = () => {
|
||||||
|
reconnectAttempts = 0
|
||||||
isConnected.value = true
|
isConnected.value = true
|
||||||
pushEvent({ type: 'status', message: 'Connected to Orchestrator' })
|
pushEvent({ type: 'status', message: 'Connected to Orchestrator' })
|
||||||
}
|
}
|
||||||
|
|
@ -81,13 +102,35 @@ export const useOnboardingAgentStore = defineStore('onboarding-agent', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.value.onclose = () => {
|
socket.value.onclose = (event) => {
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
executionStatus.value = 'idle'
|
// code 1000 = clean close (server finished normally); don't reconnect
|
||||||
|
if (!intentionalClose && event.code !== 1000) {
|
||||||
|
scheduleReconnect()
|
||||||
|
} else {
|
||||||
|
executionStatus.value = 'idle'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connect = (id: string) => {
|
||||||
|
intentionalClose = false
|
||||||
|
clearReconnectTimer()
|
||||||
|
reconnectAttempts = 0
|
||||||
|
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.close()
|
||||||
|
socket.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
currentUrl = `${wsProtocol}://${window.location.host}/ws/onboarding/generate/${id}/`
|
||||||
|
openSocket(currentUrl)
|
||||||
|
}
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
|
intentionalClose = true
|
||||||
|
clearReconnectTimer()
|
||||||
if (socket.value) {
|
if (socket.value) {
|
||||||
socket.value.close()
|
socket.value.close()
|
||||||
socket.value = null
|
socket.value = null
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue