2026-03-15 12:35:17 +00:00
---
name : polish-websocket
description : Improve Archipelago WebSocket reliability, reconnection UX, heartbeat monitoring, session timeout detection, and connection status indicators. Use when user says "polish websocket", "fix reconnection", "websocket reliability", or "connection status".
---
2026-03-08 08:06:52 +00:00
# Skill: Polish WebSocket & Real-Time
Improve WebSocket reliability, reconnection UX, heartbeat, session timeout detection, and connection status indicators.
## 1. Connection Status Indicator
### Create or update connection status display
- **Location**: App.vue header or create ConnectionStatus.vue component
- **States**: Connected (green), Reconnecting (amber pulse), Disconnected (red)
- **Data source**: `wsClient.isConnected()` from websocket.ts
- **Style**: Use existing design tokens, small dot + text in header area
```vue
< div class = "flex items-center gap-1.5" >
< div : class = "[
'w-2 h-2 rounded-full',
isConnected ? 'bg-green-400' : isReconnecting ? 'bg-amber-400 animate-pulse' : 'bg-red-400'
]" />
< span class = "text-xs text-white/40" >
{{ isConnected ? '' : isReconnecting ? 'Reconnecting...' : 'Offline' }}
</ span >
</ div >
```
### Fix OnlineStatusPill.vue
- Connect to actual WebSocket state instead of hardcoded "Online"
- Use the app store's connection state
## 2. Reconnection UX
### Don't silently give up
File: `api/websocket.ts`
After max reconnect attempts (currently 10), instead of silently stopping:
- Set a `permanentlyDisconnected` flag
- Emit event that App.vue listens to
- Show persistent banner: "Connection lost. Click to retry." or "Refresh page to reconnect."
```typescript
if ( this . reconnectAttempts >= this . maxReconnectAttempts ) {
this . shouldReconnect = false
this . notifyConnectionState ( false )
// Emit permanent disconnect event
this . onPermanentDisconnect ? .()
}
```
### Allow manual reconnect
Add a `forceReconnect()` method that resets attempt counter and tries again:
```typescript
forceReconnect() {
this . reconnectAttempts = 0
this . shouldReconnect = true
this . connect ()
}
```
## 3. Heartbeat Improvement
File: `api/websocket.ts`
Current: 60-second stale detection (passive).
Target: 30-second active ping with 5-second pong timeout.
```typescript
private startHeartbeat() {
this . heartbeatInterval = setInterval (() => {
if ( this . ws ? . readyState === WebSocket . OPEN ) {
this . ws . send ( JSON . stringify ({ type : 'ping' }))
this . pongTimeout = setTimeout (() => {
// No pong received — connection is dead
this . ws ? . close ()
this . handleReconnect ()
}, 5000 )
}
}, 30000 )
}
// In message handler:
if ( data . type === 'pong' ) {
clearTimeout ( this . pongTimeout )
return
}
```
Note: Backend must respond to `ping` with `pong` . Check handler.rs WebSocket handler.
## 4. Session Timeout Detection
File: `api/rpc-client.ts`
When RPC returns 401 or 403:
```typescript
if ( response . status === 401 || response . status === 403 ) {
// Session expired — redirect to login
window . location . href = '/login'
return
}
```
This should be in the base `call()` method so it applies to all RPC calls.
## 5. Fix Race Condition on Reconnect
File: `stores/app.ts` or `api/websocket.ts`
Problem: `isWsSubscribed` flag doesn't prevent duplicate listeners on rapid reconnect.
Fix: Use listener deduplication:
```typescript
private listeners = new Map < string , Set < Function >>()
subscribe ( event : string , callback : Function ) {
if ( ! this . listeners . has ( event )) {
this . listeners . set ( event , new Set ())
}
this . listeners . get ( event ) ! . add ( callback )
}
```
Or simpler: remove all listeners before reconnect, then re-add:
```typescript
onReconnect() {
// Clear old subscriptions
this . removeAllListeners ()
// Re-subscribe
this . setupSubscriptions ()
}
```
## 6. Message Queuing During Disconnect
When WebSocket is down, queue subscription requests:
```typescript
private pendingSubscriptions : Array < () => void > = []
subscribe ( event : string , callback : Function ) {
if ( this . ws ? . readyState !== WebSocket . OPEN ) {
this . pendingSubscriptions . push (() => this . subscribe ( event , callback ))
return
}
// Normal subscribe logic
}
onReconnected() {
// Replay pending subscriptions
const pending = [... this . pendingSubscriptions ]
this . pendingSubscriptions = []
pending . forEach ( fn => fn ())
}
```
## Verification
1. **Kill backend** → frontend shows "Disconnected" → restart backend → frontend reconnects and shows "Connected"
2. **Toggle wifi** → status indicator updates → wifi back → auto-reconnect
3. **Wait for session timeout** → next RPC call redirects to login
4. **Rapid reconnect** → no duplicate event handlers (check with DevTools)
5. **Leave tab in background** → come back → status is accurate
## Deploy
```bash
./scripts/deploy-to-target.sh --live
```
Test with browser DevTools Network tab to observe WebSocket frames.