Files
notes/WebSocket Lifecycle Sequence Diagram.md
2025-11-25 21:38:17 -05:00

509 lines
25 KiB
Markdown

```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
Client->>HTTPServer: HTTP Upgrade Request
HTTPServer->>WebSocketServer: upgrade event
WebSocketServer->>WebSocketServer: handleUpgrade()
WebSocketServer->>WebSocketServer: Generate connection.id (32 chars)
WebSocketServer->>WebSocketServer: Get remoteAddress (from X-Forwarded-For)
WebSocketServer->>WebSocketServer: Setup event handlers<br/>(message, close, pong, error)
WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers)
ClientMQAdapter->>ClientMQAdapter: Set __connectionStart<br/>Set __connectionId<br/>Set __meta {headers}
ClientMQAdapter->>ClientMQAdapter: Register in _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = true
ClientMQAdapter->>ClientMQ: emit('connect', connectionId)
ClientMQ->>ClientMQ: Register adapter in _adaptersById
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsTotal++<br/>_connectionsOpen++
end
alt Connection limit exceeded (>2000)
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
Note over ClientMQAdapter: See "Server-Initiated Close" below
end
Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) ===
Client->>WebSocketServer: Message (binary or base64)
WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data)
ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol)
alt Unsupported message format
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Socket not connected
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Message type = 'Request'
ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta)
alt Request timeout (>15s)
MQWorker-->>ClientMQAdapter: TimeoutError
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request failed
MQWorker-->>ClientMQAdapter: Error
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request succeeded
MQWorker-->>ClientMQAdapter: {type, payload, wallTime}
ClientMQAdapter->>ClientMQAdapter: Build Response message<br/>(messageType: 'Response', requestId, type, payload)
alt Socket not open
ClientMQAdapter->>ClientMQAdapter: Drop response, log debug
else Socket open
ClientMQAdapter->>Client: Response (binary or base64)
ClientMQAdapter->>ClientMQ: emit('request', connectionId, data)
ClientMQ->>ClientMQWorker: emit('request', connectionId, data)
ClientMQWorker->>ClientMQWorker: Handle subscription updates<br/>(JoinRoom, LeaveRoom, etc.)
end
end
else Message type = 'Response'
alt Request handler not found
ClientMQAdapter->>ClientMQAdapter: Log warning, ignore
else Request handler exists
ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise
end
else Message type = 'Event'
ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported)
end
Note over Client,ClientMQWorker: === Server-Initiated Request ===
ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message)
ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message)
ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter)
ClientMQAdapter->>ClientMQAdapter: Register in _requests map
alt Socket not found
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found'
else Socket not connected
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened'
else Socket valid
ClientMQAdapter->>Client: Request (binary or base64)
ClientMQAdapter->>ClientMQAdapter: Start timeout (15s)
Client->>WebSocketServer: Response message
WebSocketServer->>ClientMQAdapter: messageDelegate (Response)
ClientMQAdapter->>ClientMQAdapter: Resolve request promise
ClientMQAdapter-->>ClientMQWorker: Response message
alt Response timeout
ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout
end
end
Note over Client,ClientMQWorker: === Heartbeat/Ping Flow ===
loop Every 20 seconds
PingInterval->>ClientMQAdapter: Ping interval trigger
ClientMQAdapter->>ClientMQAdapter: Iterate all sockets
alt Previous ping still pending
ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning
else Socket has ping() method
alt No pong received (lastPong < lastPing)
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
ClientMQ->>ClientMQWorker: emit('timeout', connectionId)
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
Note over ClientMQAdapter: See "Server-Initiated Close" below
else Pong received
ClientMQAdapter->>Client: ping()
Client->>WebSocketServer: pong()
WebSocketServer->>ClientMQAdapter: pongDelegate(socket)
ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()<br/>Calculate RTT
ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...)
end
else Socket has no ping() method
alt Last ping > 2x heartbeatInterval ago
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
Note over ClientMQAdapter: See timeout flow above
end
end
end
Note over Client,ClientMQWorker: === Server-Initiated Close ===
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
else Close reason: drain
ClientMQWorker->>ClientMQ: stop(3333, 'drain')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each]
else Close reason: connection limit
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
else Close reason: heartbeat terminate
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
else Close reason: send error
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
end
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
ClientMQAdapter->>Client: socket.close(code, reason)
Client->>WebSocketServer: close event
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
end
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
end
end
Note over Client,ClientMQWorker: === Client-Initiated Close ===
Client->>WebSocketServer: close connection
WebSocketServer->>WebSocketServer: close event (reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
Note over ClientMQAdapter: Same disconnect flow as above
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
Client->>WebSocketServer: error event
WebSocketServer->>WebSocketServer: Log error
else Handler error
WebSocketServer->>WebSocketServer: Log error, continue
else Send error (not opened)
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
end
```
## connectionsOpen
```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
Client->>HTTPServer: HTTP Upgrade Request
HTTPServer->>WebSocketServer: upgrade event
WebSocketServer->>WebSocketServer: handleUpgrade()
ClientMQAdapter->>ClientMQ: emit('connect', connectionId)
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsTotal++<br/>_connectionsOpen++
end
Note over Client,ClientMQWorker: === Server-Initiated Close ===
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
end
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
ClientMQAdapter->>Client: socket.close(code, reason)
Client->>WebSocketServer: close event
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
end
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
end
end
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
Client->>WebSocketServer: error event
WebSocketServer->>WebSocketServer: Log error
else Handler error
WebSocketServer->>WebSocketServer: Log error, continue
else Send error (not opened)
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
end
```
# WebSocket Lifecycle Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant HTTPServer
participant WebSocketServer
participant ClientMQWebsocketAdapter as ClientMQAdapter
participant ClientMQ
participant ClientMQWorker
participant MQWorker
participant PingInterval as Ping Interval<br/>(20s)
Note over Client,ClientMQWorker: === Connection Establishment ===
Client->>HTTPServer: HTTP Upgrade Request
HTTPServer->>WebSocketServer: upgrade event
WebSocketServer->>+ClientMQAdapter: connectDelegate(socket, headers)
ClientMQAdapter->>ClientMQAdapter: Set __connectionStart<br/>Set __connectionId<br/>Set __meta {headers}
ClientMQAdapter->>ClientMQAdapter: Register in _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = true
ClientMQAdapter->>-ClientMQ: emit('connect', connectionId)
ClientMQ->>ClientMQ: Register adapter in _adaptersById
ClientMQ->>ClientMQWorker: emit('connect', connectionId)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
ClientMQWorker->>ClientMQWorker: _connectionsTotal++<br/>_connectionsOpen++
end
alt Connection limit exceeded (>2000)
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
Note over ClientMQAdapter: See "Server-Initiated Close" below
end
Note over Client,ClientMQWorker: === Normal Message Flow (Client Request) ===
Client->>WebSocketServer: Message (binary or base64)
WebSocketServer->>ClientMQAdapter: messageDelegate(socket, data)
ClientMQAdapter->>ClientMQAdapter: Decode message (MQ protocol)
alt Unsupported message format
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Socket not connected
ClientMQAdapter->>ClientMQAdapter: Drop message, log warning
else Message type = 'Request'
ClientMQAdapter->>MQWorker: processRequest(connectionId, type, payload, meta)
alt Request timeout (>15s)
MQWorker-->>ClientMQAdapter: TimeoutError
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'timeout'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request failed
MQWorker-->>ClientMQAdapter: Error
ClientMQAdapter->>ClientMQAdapter: Encode mq.Error {reason: 'failed'}
ClientMQAdapter->>Client: Response (mq.Error)
else Request succeeded
MQWorker-->>ClientMQAdapter: {type, payload, wallTime}
ClientMQAdapter->>ClientMQAdapter: Build Response message<br/>(messageType: 'Response', requestId, type, payload)
alt Socket not open
ClientMQAdapter->>ClientMQAdapter: Drop response, log debug
else Socket open
ClientMQAdapter->>Client: Response (binary or base64)
ClientMQAdapter->>ClientMQ: emit('request', connectionId, data)
ClientMQ->>ClientMQWorker: emit('request', connectionId, data)
ClientMQWorker->>ClientMQWorker: Handle subscription updates<br/>(JoinRoom, LeaveRoom, etc.)
end
end
else Message type = 'Response'
alt Request handler not found
ClientMQAdapter->>ClientMQAdapter: Log warning, ignore
else Request handler exists
ClientMQAdapter->>ClientMQAdapter: Resolve pending request promise
end
else Message type = 'Event'
ClientMQAdapter->>ClientMQAdapter: Log warning (unsupported)
end
Note over Client,ClientMQWorker: === Server-Initiated Request ===
ClientMQWorker->>ClientMQ: sendRequest(connectionId, type, message)
ClientMQ->>ClientMQAdapter: sendRequest(connectionId, type, message)
ClientMQAdapter->>ClientMQAdapter: Build request (requestId: 'S' + counter)
ClientMQAdapter->>ClientMQAdapter: Register in _requests map
alt Socket not found
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not found'
else Socket not connected
ClientMQAdapter-->>ClientMQWorker: Error: 'Websocket connection not opened'
else Socket valid
ClientMQAdapter->>Client: Request (binary or base64)
ClientMQAdapter->>ClientMQAdapter: Start timeout (15s)
Client->>WebSocketServer: Response message
WebSocketServer->>ClientMQAdapter: messageDelegate (Response)
ClientMQAdapter->>ClientMQAdapter: Resolve request promise
ClientMQAdapter-->>ClientMQWorker: Response message
alt Response timeout
ClientMQAdapter->>ClientMQAdapter: Log warning, cleanup after 2x timeout
end
end
Note over Client,ClientMQWorker: === Heartbeat/Ping Flow ===
loop Every 20 seconds
PingInterval->>ClientMQAdapter: Ping interval trigger
ClientMQAdapter->>ClientMQAdapter: Iterate all sockets
alt Previous ping still pending
ClientMQAdapter->>ClientMQAdapter: Skip ping, log warning
else Socket has ping() method
alt No pong received (lastPong < lastPing)
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
ClientMQ->>ClientMQWorker: emit('timeout', connectionId)
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
Note over ClientMQAdapter: See "Server-Initiated Close" below
else Pong received
ClientMQAdapter->>Client: ping()
Client->>WebSocketServer: pong()
WebSocketServer->>ClientMQAdapter: pongDelegate(socket)
ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()<br/>Calculate RTT
ClientMQAdapter->>MQWorker: processRoundTripTimeMeasurement(...)
end
else Socket has no ping() method
alt Last ping > 2x heartbeatInterval ago
ClientMQAdapter->>ClientMQAdapter: Detect timeout
ClientMQAdapter->>ClientMQ: emit('timeout', connectionId)
Note over ClientMQAdapter: See timeout flow above
end
end
end
Note over Client,ClientMQWorker: === Server-Initiated Close ===
alt Close reason: timeout
ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout')
else Close reason: drain
ClientMQWorker->>ClientMQ: stop(3333, 'drain')
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) [for each]
else Close reason: connection limit
ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded')
else Close reason: heartbeat terminate
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
else Close reason: send error
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
end
ClientMQ->>ClientMQAdapter: close(connectionId, code, reason)
alt Socket not found
ClientMQAdapter->>ClientMQAdapter: Log warning, return false
else Connection already closed (>5s ago)
ClientMQAdapter->>ClientMQAdapter: Force disconnect
ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
else Connection valid
ClientMQAdapter->>ClientMQAdapter: Set __connectionEnded = now()
rect rgb(200, 255, 200)
Note over ClientMQAdapter,Client: 📤 SERVER SENDS CLOSE FRAME
ClientMQAdapter->>WebSocketServer: socket.close(code, reason)
WebSocketServer->>Client: Send Close Frame (code, reason)
Note over Client: Client receives server's close frame
end
rect rgb(255, 200, 200)
Note over Client,WebSocketServer: 📥 CLIENT MUST RESPOND WITH CLOSE FRAME<br/>(WebSocket Protocol Requirement)
alt Client sends close frame
Client->>WebSocketServer: Send Close Frame (acknowledgment)
Note over WebSocketServer: Platform receives client's close frame
else Client does NOT send close frame
Note over ClientMQAdapter: ⏱️ After 5s timeout (closeTimeout)<br/>Connection marked as forcefully closed
Note over ClientMQAdapter: Next disconnect will use<br/>disconnect-after-forcefull-close path
end
end
rect rgb(200, 200, 255)
Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING
WebSocketServer->>WebSocketServer: 'close' event fires<br/>(reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
alt Multiple close events
ClientMQAdapter->>ClientMQAdapter: Log warning, return early<br/>(_connectionsOpen NOT decremented)
else Socket already disconnected
alt Previously forcefully closed
ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')<br/>(_connectionsOpen NOT decremented)
else Already disconnected
ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')<br/>(_connectionsOpen NOT decremented)
end
else Normal disconnect
ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById
ClientMQAdapter->>ClientMQAdapter: Set __connected = false
ClientMQAdapter->>ClientMQAdapter: Calculate connection duration
ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description)
ClientMQ->>ClientMQ: Remove from _adaptersById
ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description)
rect rgb(255, 255, 200)
Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION<br/>⚠️ Only happens after receiving client's close frame
ClientMQWorker->>ClientMQWorker: _connectionsOpen--
end
ClientMQWorker->>ClientMQWorker: Clean up room subscriptions
ClientMQWorker->>ClientMQWorker: Clean up conversation subscriptions
ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...)
end
end
end
Note over Client,ClientMQWorker: === Client-Initiated Close ===
rect rgb(255, 200, 200)
Note over Client,WebSocketServer: 📥 CLIENT SENDS CLOSE FRAME
Client->>WebSocketServer: Send Close Frame (code, reason)
Note over WebSocketServer: Platform receives client's close frame
end
rect rgb(200, 255, 200)
Note over WebSocketServer,Client: 📤 SERVER RESPONDS WITH CLOSE FRAME<br/>(WebSocket Protocol Requirement)
WebSocketServer->>Client: Send Close Frame (acknowledgment)
end
rect rgb(200, 200, 255)
Note over WebSocketServer,ClientMQWorker: 🔄 CLOSE EVENT & DISCONNECT PROCESSING
WebSocketServer->>WebSocketServer: 'close' event fires<br/>(reasonCode, description)
WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description)
Note over ClientMQAdapter: Same disconnect flow as server-initiated<br/>(_connectionsOpen decremented after close event)
end
Note over Client,ClientMQWorker: === Error Handling ===
alt Socket error
Client->>WebSocketServer: error event
WebSocketServer->>WebSocketServer: Log error
else Handler error
WebSocketServer->>WebSocketServer: Log error, continue
else Send error (not opened)
ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close')
ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...)
Note over ClientMQAdapter: See "Server-Initiated Close" above
else Send error (not found/closed)
ClientMQWorker->>ClientMQWorker: Return {status: 'closed'}
end
```