fix: WebSocket race conditions, Vue error handler, remove sudo podman, add container health checks
- F1: Guard connectWebSocket against concurrent calls with isWsConnecting flag - F2: Serialize mesh send operations with sendQueue to prevent fetchMessages races - F3: Add global Vue error handler with toast notification - S1: Replace sudo podman with podman across all scripts (rootless Podman) - S2: Add health-cmd to all 40 container run commands in first-boot-containers.sh Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
let isWsSubscribed = false
|
||||
let isWsConnecting = false
|
||||
let sessionValidated = false
|
||||
|
||||
// Computed
|
||||
@@ -86,10 +87,14 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
|
||||
async function connectWebSocket(): Promise<void> {
|
||||
// Prevent concurrent connection attempts
|
||||
if (isWsConnecting) return
|
||||
isWsConnecting = true
|
||||
|
||||
try {
|
||||
if (import.meta.env.DEV) console.log('[Store] Connecting WebSocket...')
|
||||
isReconnecting.value = true
|
||||
|
||||
|
||||
// Don't create multiple subscriptions - check if already subscribed
|
||||
if (!isWsSubscribed) {
|
||||
// Subscribe to updates BEFORE connecting (so we catch initial data)
|
||||
@@ -159,6 +164,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
isConnected.value = false
|
||||
// Don't throw - allow app to work without real-time updates
|
||||
// The WebSocket will reconnect in the background
|
||||
} finally {
|
||||
isWsConnecting = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,9 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
const error = ref<string | null>(null)
|
||||
const sending = ref(false)
|
||||
|
||||
// Serialize send operations to prevent concurrent fetchMessages() races
|
||||
let sendQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
// Node position tracking for map view (contact_id -> position)
|
||||
const nodePositions = ref<Map<number, NodePosition>>(new Map())
|
||||
|
||||
@@ -247,24 +250,30 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
}
|
||||
|
||||
async function sendMessage(contactId: number, message: string) {
|
||||
try {
|
||||
sending.value = true
|
||||
error.value = null
|
||||
const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({
|
||||
method: 'mesh.send',
|
||||
params: { contact_id: contactId, message: message.trim() },
|
||||
})
|
||||
// Refresh messages after sending
|
||||
if (res.sent) {
|
||||
await fetchMessages()
|
||||
const doSend = async () => {
|
||||
try {
|
||||
sending.value = true
|
||||
error.value = null
|
||||
const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({
|
||||
method: 'mesh.send',
|
||||
params: { contact_id: contactId, message: message.trim() },
|
||||
})
|
||||
// Refresh messages after sending
|
||||
if (res.sent) {
|
||||
await fetchMessages()
|
||||
}
|
||||
return res
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to send mesh message'
|
||||
throw err
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
return res
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to send mesh message'
|
||||
throw err
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
// Chain onto send queue to prevent concurrent fetchMessages() calls
|
||||
const result = sendQueue.then(doSend, doSend)
|
||||
sendQueue = result.then(() => {}, () => {})
|
||||
return result
|
||||
}
|
||||
|
||||
async function broadcastIdentity() {
|
||||
|
||||
Reference in New Issue
Block a user