This document describes the automatic reconnection logic implemented in the WebSocket middleware to handle connection drops gracefully, with special support for servers running on processor hardware.
The WebSocket middleware uses a simple approach: auto-reconnect for everything except custom application codes.
| Close Code | Scenario | Behavior |
|---|---|---|
| 4100 | Client-initiated cleanup | No reconnection, clean shutdown |
| 4000 | User code changed | No reconnection, show error, clear state |
| 4002 | Room combination changed | No reconnection, manual reconnect required |
| 4001 (with touchpanel key) | Connection loss | Auto-reconnect after 5s |
| 4001 (no touchpanel, no proc) | Processor shutdown | No reconnection, manual reconnect required |
| 4001 (no touchpanel, proc HW) | Connection loss | Auto-reconnect after 5s |
| All other codes | Any disconnect/error | Auto-reconnect after 5s |
This includes:
- 1000 (Normal Closure) - Typical processor hardware disconnect
- Any network errors, unexpected disconnects, etc.
Code 4001 Logic:
- If touchpanel key exists → Always auto-reconnect (regardless of processor hardware flag)
- If no touchpanel key AND not on processor hardware → Manual reconnect required
- If no touchpanel key BUT on processor hardware → Auto-reconnect
The middleware checks the serverIsRunningOnProcessorHardware flag from Redux state to determine reconnection behavior:
For most disconnects (code 1000, network errors, unexpected closes, etc.):
- Show user-friendly message: "Connection lost. Attempting to reconnect..."
- Clear room/device state but preserve configuration
- Wait 5 seconds, then attempt reconnection
- Repeat continuously every 5 seconds until successful
- Once connected, stop the reconnection loop
Continuous Reconnection Loop:
The middleware maintains a reconnection timer that:
- Starts when a reconnectable disconnect occurs
- Attempts to reconnect every 5 seconds
- Continues indefinitely until connection succeeds
- Is automatically stopped when connection is established
- Is also stopped for terminal errors (codes 4000, 4002, 4100, or 4001 on non-processor hardware)
Code:
const startReconnectionLoop = (dispatch: Dispatch) => {
if (state.reconnectTimer) {
clearTimeout(state.reconnectTimer);
state.reconnectTimer = null;
}
console.log('WebSocket middleware: Starting reconnection loop...');
state.reconnectTimer = setTimeout(() => {
state.waitingToReconnect = false;
state.reconnectTimer = null;
console.log('WebSocket middleware: Attempting automatic reconnection...');
dispatch(wsConnect());
}, 5000);
};
// On disconnect:
console.log('WebSocket middleware: Clearing state on disconnect');
dispatch(
uiActions.setErrorMessage('Connection lost. Attempting to reconnect...')
);
dispatch(runtimeConfigActions.setWebsocketIsConnected(false));
dispatch(devicesActions.clearDevices());
dispatch(roomsActions.clearRooms());
dispatch(uiActions.clearAllModals());
dispatch(uiActions.clearSyncState());
// Start continuous reconnection attempts
startReconnectionLoop(dispatch);
// On successful connection:
newWs.onopen = (ev: Event) => {
console.log('WebSocket middleware: Connected');
state.waitingToReconnect = false;
stopReconnectionLoop(); // Stop trying to reconnect
dispatch(runtimeConfigActions.setWebsocketIsConnected(true));
};Scenario: Code 4001 behavior depends on touchpanel key presence and processor hardware flag
Priority Logic:
- Touchpanel key exists → Always auto-reconnect (best indicator of active touchpanel)
- No touchpanel key + not on processor hardware → Manual reconnect required
- No touchpanel key + on processor hardware → Auto-reconnect
Code:
// Handle code 4001 based on touchpanel key presence
if (closeEvent.code === 4001) {
const currentState = getState() as LocalRootState;
const hasTouchpanelKey = !!currentState.runtimeConfig?.touchpanelKey;
if (hasTouchpanelKey) {
console.log(
'WebSocket middleware: Code 4001 received with touchpanel key present, will auto-reconnect'
);
// Will fall through to auto-reconnect logic below
} else if (!serverIsRunningOnProcessorHardware) {
console.log(
'WebSocket middleware: Processor disconnected (no touchpanel key, not on processor hardware)'
);
stopReconnectionLoop();
dispatch(
uiActions.setErrorMessage('Processor has disconnected. Click Reconnect')
);
clearStateDataOnDisconnect(dispatch);
return;
} else {
console.log(
'WebSocket middleware: Code 4001 on processor hardware (no touchpanel key), will auto-reconnect'
);
// Will fall through to auto-reconnect logic below
}
}Touchpanel Key Priority: The presence of a touchpanel key indicates an active touchpanel connection that should be maintained, so it takes priority over the processor hardware flag for reconnection decisions.
Scenario: Server is on a computer/VM that might be shut down
- Disconnect code 4001 likely means server is stopping
- Auto-reconnection would be futile
- User should manually reconnect when server restarts
Behavior on disconnect (code 1000 or 4001):
- Show error message: "Processor has disconnected. Click Reconnect"
- Clear all state (full cleanup)
- Wait for manual user action
- No automatic reconnection
Code:
// Handle code 1000 (Normal Closure)
if (closeEvent.code === 1000) {
if (!serverIsRunningOnProcessorHardware) {
console.log(
'WebSocket middleware: Normal closure (not on processor hardware)'
);
dispatch(
uiActions.setErrorMessage('Processor has disconnected. Click Reconnect')
);
clearStateDataOnDisconnect(dispatch);
return;
}
}
// Also handle 4001
if (closeEvent.code === 4001 && !serverIsRunningOnProcessorHardware) {
console.log(
'WebSocket middleware: Processor disconnected (not on processor hardware)'
);
dispatch(
uiActions.setErrorMessage('Processor has disconnected. Click Reconnect')
);
clearStateDataOnDisconnect(dispatch);
return; // Early exit - no auto-reconnect
}For any other close codes (network issues, server restarts, etc.):
- Automatic reconnection after 5 seconds
- Applies regardless of
serverIsRunningOnProcessorHardwarevalue - Handles edge cases like network blips, router restarts, etc.
Code:
else if (closeEvent.code !== 4001 && closeEvent.code !== 4002) {
console.log(
'WebSocket middleware: Attempting automatic reconnection after unexpected disconnect...'
);
dispatch(wsReconnect());
}┌─────────────────────────────────────────────────────────────────┐
│ WebSocket Disconnects │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
Check Close Code
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
Code 4100 Code 4000 Code 4001
(Client close) (User code change) (Processor disconnect)
│ │ │
▼ ▼ ▼
Clean Shutdown Show Error Check Hardware Flag
No Reconnect No Reconnect │
┌───────────┴──────────┐
│ │
▼ ▼
Processor = true Processor = false
│ │
▼ ▼
"Connection lost..." "Processor disconnected"
Clear state Clear state
Wait 5s No Reconnect
▼ Manual action required
Auto-reconnect
│
┌─────────────────┴─────────────────┐
│ │
▼ ▼
Success: Connected Fail: Try again in 5s
│ │
▼ └──────┐
Request room status │
Back to normal │
└──> Loop until success
The middleware uses a waitingToReconnect flag to prevent concurrent reconnection attempts:
if (state.client || state.waitingToReconnect) {
console.log('Already connected or waiting to reconnect');
return;
}
state.waitingToReconnect = true;
try {
// Connection logic...
await connect(store.dispatch, store.getState);
state.waitingToReconnect = false;
} catch (error) {
console.error('Connection failed:', error);
state.waitingToReconnect = false;
}
// On disconnect - clear client reference immediately
newWs.onclose = (closeEvent: CloseEvent): void => {
console.log('WebSocket middleware: Disconnected');
// Critical: Clear the client reference so reconnection can proceed
state.client = null;
// ... handle reconnection logic
};To prevent the UI from flashing between connected/disconnected states during reconnection attempts, the middleware uses a delayed connection state update:
newWs.onopen = (ev: Event) => {
console.log('WebSocket middleware: Connected');
state.waitingToReconnect = false;
stopReconnectionLoop();
// Delay setting connected state to avoid flashing during failed reconnection attempts
setTimeout(() => {
// Only set connected if this WebSocket is still the current client
if (state.client === newWs && newWs.readyState === WebSocket.OPEN) {
dispatch(runtimeConfigActions.setWebsocketIsConnected(true));
}
}, 100);
};Why this prevents flashing:
- WebSocket
onopencan fire briefly even for connections that will immediately fail - Without delay:
onopen→isConnected = true→ UI shows children →onclose→isConnected = false→ UI shows DisconnectedMessage (flashing) - With delay:
onopen→ wait 100ms → check if still connected → only then setisConnected = true
This ensures:
- Only one connection attempt at a time
- Client reference is cleared immediately on disconnect
- Prevents "already connected" errors on reconnection
- No UI flashing during reconnection attempts
- No race conditions
- Clean state management
✅ Continuous automatic reconnection
- Connection drops are handled automatically
- Attempts reconnection every 5 seconds continuously
- No limit on retry attempts - keeps trying until successful
- User sees "Connection lost. Attempting to reconnect..."
- App reconnects without user intervention
- Room status automatically requested on reconnect
- Minimal disruption to workflow
✅ Clear indication when server is intentionally stopped
- User sees "Processor has disconnected. Click Reconnect"
- No continuous retry attempts (prevents log spam)
- User knows to check server status
- Manual reconnect when ready
The serverIsRunningOnProcessorHardware flag is set during initialization from the /appversion endpoint:
const { data: versionInfo } = await httpClient.get<VersionInfo>(
`${config.apiPath}/appversion`
);
dispatch(
runtimeConfigActions.setServerIsRunningOnProcessorHardware(
versionInfo.serverIsRunningOnProcessorHardware
)
);This is stored in Redux state at runtimeConfig.serverIsRunningOnProcessorHardware.
- Connect to WebSocket
- Stop processor connection (should send close code 1000)
- Expected: Shows "Connection lost. Attempting to reconnect..."
- Expected: Auto-reconnects after 5s
- Expected: Room status automatically requested on reconnect
- Connect to WebSocket
- Simulate network drop (disconnect WiFi for 3 seconds)
- Expected: Auto-reconnects after 5s
- Expected: Shows "Connection lost..."
- Expected: Multiple retry attempts until network is back
- Set touchpanel key in runtime config:
runtimeConfig.touchpanelKey = "some-key" - Connect to WebSocket
- Trigger disconnect with close code 4001
- Expected: Auto-reconnects after 5s (regardless of processor hardware flag)
- Expected: Shows "Connection lost. Attempting to reconnect..."
- Expected: Continuous retry attempts until successful
- Set
serverIsRunningOnProcessorHardware: true - Ensure no touchpanel key:
runtimeConfig.touchpanelKey = null - Connect to WebSocket
- Trigger disconnect with close code 4001
- Expected: Auto-reconnects after 5s
- Expected: Shows "Connection lost. Attempting to reconnect..."
- Set
serverIsRunningOnProcessorHardware: false - Ensure no touchpanel key:
runtimeConfig.touchpanelKey = null - Connect to WebSocket
- Stop server with close code 4001
- Expected: Shows "Processor has disconnected. Click Reconnect"
- Expected: No auto-reconnect attempts
- Expected: Manual reconnect button works when server is back
- Connect to WebSocket
- Trigger user code change (code 4000)
- Expected: Shows "User code changed. Click reconnect to enter the new code"
- Expected: No auto-reconnect attempts
- Expected: State is cleared
- Connect to WebSocket
- Trigger room combination change (code 4002)
- Expected: Shows "Room combination changed. Click Reconnect to re-join the room"
- Expected: No auto-reconnect attempts
- Expected: State is cleared
- Connect to WebSocket
- Stop server completely
- Expected: Shows "Connection lost. Attempting to reconnect..."
- Expected: Attempts reconnection every 5 seconds continuously
- Restart server after 30 seconds
- Expected: Successfully reconnects on next attempt
- Expected: Reconnection loop stops after successful connection
- Connect to WebSocket
- Disconnect and reconnect rapidly
- Expected: Connection lock prevents multiple simultaneous attempts
- Expected: Reconnection timer is reset properly on each disconnect
- Expected: No race conditions or duplicate connections
- ✅ Continuous reconnection - Never gives up until connection succeeds
- ✅ Better UX for all deployments - Seamless recovery from any network issue
- ✅ Clear feedback for terminal errors - User knows when manual action is needed
- ✅ Handles long outages gracefully - Keeps trying every 5 seconds indefinitely
- ✅ Prevents connection spam - Lock mechanism prevents race conditions
- ✅ Automatic room status refresh - State syncs on reconnection
- ✅ Production-ready error handling - Different strategies for different scenarios
- ✅ Smart timer management - Stops reconnection loop on success or terminal errors
src/lib/store/middleware/websocketMiddleware.ts- Main implementationsrc/lib/store/runtimeConfig/runtimeConfig.slice.ts- State managementAUTOMATIC_ROOM_STATUS_IMPLEMENTATION.md- Room status request documentationWEBSOCKET_REFACTOR_SUMMARY.md- Overall WebSocket refactor documentation
Possible improvements for future iterations:
- Exponential backoff - Increase delay between retries (5s, 10s, 20s...) to reduce load during extended outages
- Max retry limit with notification - After N attempts, ask user if they want to continue trying
- Network status detection - Use Navigator.onLine API to detect network availability
- Ping/Pong heartbeat - Detect dead connections proactively
- Connection quality indicators - Show signal strength or latency to user
- Offline queue - Buffer messages to send when reconnected