Added websocket reconnect with backoff

This commit is contained in:
Viswamedha Nalabotu 2026-03-18 10:20:57 +00:00
parent 64f2fa012e
commit d19c50cf77
3 changed files with 107 additions and 18 deletions

View file

@ -0,0 +1,3 @@
export const BACKOFF_BASE_MS = 1000
export const BACKOFF_MAX_MS = 30000
export const BACKOFF_MAX_ATTEMPTS = 6

View file

@ -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

View file

@ -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