```mermaid sequenceDiagram participant Client participant HTTPServer participant WebSocketServer participant ClientMQWebsocketAdapter as ClientMQAdapter participant ClientMQ participant ClientMQWorker participant MQWorker participant PingInterval as Ping Interval
(20s) Note over Client,ClientMQWorker: === Connection Establishment === activate Client Client->>HTTPServer: HTTP Upgrade Request activate HTTPServer HTTPServer->>WebSocketServer: upgrade event deactivate HTTPServer activate WebSocketServer WebSocketServer->>WebSocketServer: handleUpgrade() WebSocketServer->>WebSocketServer: ... WebSocketServer->>ClientMQAdapter: connectDelegate(socket, headers) deactivate WebSocketServer activate ClientMQAdapter ClientMQAdapter->>ClientMQ: emit('connect', connectionId) deactivate ClientMQAdapter activate ClientMQ ClientMQ->>ClientMQ: Register adapter in _adaptersById ClientMQ->>ClientMQWorker: emit('connect', connectionId) deactivate ClientMQ activate ClientMQWorker rect rgb(255, 255, 200) Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION ClientMQWorker->>ClientMQWorker: _connectionsTotal++
_connectionsOpen++ end deactivate ClientMQWorker deactivate Client Note over Client,ClientMQWorker: === Heartbeat/Ping Flow === loop Every 20 seconds activate PingInterval PingInterval->>ClientMQAdapter: Ping interval trigger deactivate PingInterval activate ClientMQAdapter 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) deactivate ClientMQAdapter activate ClientMQ ClientMQ->>ClientMQWorker: emit('timeout', connectionId) deactivate ClientMQ activate ClientMQWorker ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') deactivate ClientMQWorker Note over ClientMQAdapter: See "Server-Initiated Close" below else Pong received ClientMQAdapter->>Client: ping() activate Client Client->>WebSocketServer: pong() deactivate Client activate WebSocketServer WebSocketServer->>ClientMQAdapter: pongDelegate(socket) deactivate WebSocketServer ClientMQAdapter->>ClientMQAdapter: Set __lastPong = Date.now()
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 deactivate ClientMQAdapter Note over Client,ClientMQWorker: === Server-Initiated Close === activate ClientMQWorker alt Close reason: timeout ClientMQWorker->>ClientMQ: close(connectionId, 3334, 'timeout') deactivate ClientMQWorker activate ClientMQ ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) deactivate ClientMQ activate ClientMQAdapter else Close reason: drain ClientMQWorker->>ClientMQ: stop(3333, 'drain') deactivate ClientMQWorker activate ClientMQ loop For each connection ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) activate ClientMQAdapter Note over ClientMQAdapter: Full close processing happens
for each connection in loop 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->>WebSocketServer: socket.close(code, reason) activate WebSocketServer WebSocketServer->>Client: Send Close Frame activate Client deactivate Client activate Client Client->>WebSocketServer: Send Close Frame (acknowledgment) deactivate Client WebSocketServer->>WebSocketServer: 'close' event fires WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) deactivate WebSocketServer ClientMQAdapter->>ClientMQAdapter: Remove from _socketsById ClientMQAdapter->>ClientMQ: emit('disconnect', connectionId, code, description) activate ClientMQ ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) deactivate ClientMQ activate ClientMQWorker ClientMQWorker->>ClientMQWorker: _connectionsOpen-- ClientMQWorker->>MQWorker: publish('pcast.ConnectionDisconnected', ...) activate MQWorker deactivate MQWorker deactivate ClientMQWorker end deactivate ClientMQAdapter end deactivate ClientMQ activate ClientMQAdapter Note over ClientMQAdapter: All connections processed in loop above
Processing code below will be no-op (socket not found) else Close reason: connection limit ClientMQWorker->>ClientMQ: close(connectionId, 1013, 'concurrency-limit-exceeded') deactivate ClientMQWorker activate ClientMQ ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) deactivate ClientMQ activate ClientMQAdapter else Close reason: heartbeat terminate ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') deactivate ClientMQWorker activate ClientMQ ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) deactivate ClientMQ activate ClientMQAdapter else Close reason: send error ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') deactivate ClientMQWorker activate ClientMQ ClientMQ->>ClientMQAdapter: close(connectionId, code, reason) deactivate ClientMQ activate ClientMQAdapter end Note over ClientMQAdapter: Processing for single connection close
(ClientMQAdapter active for non-drain cases only) alt Socket not found ClientMQAdapter->>ClientMQAdapter: Log warning, return false deactivate ClientMQAdapter else Connection already closed (>5s ago) ClientMQAdapter->>ClientMQAdapter: Force disconnect ClientMQAdapter->>ClientMQAdapter: Set __forcefullyClosed = true ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close') deactivate ClientMQAdapter activate ClientMQ deactivate ClientMQ 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) activate WebSocketServer WebSocketServer->>Client: Send Close Frame (code, reason) activate Client Note over Client: Client receives server's close frame deactivate Client end rect rgb(255, 200, 200) Note over Client,WebSocketServer: 📥 CLIENT MUST RESPOND WITH CLOSE FRAME
(WebSocket Protocol Requirement) alt Client sends close frame activate Client Client->>WebSocketServer: Send Close Frame (acknowledgment) deactivate Client Note over WebSocketServer: Platform receives client's close frame else Client does NOT send close frame Note over ClientMQAdapter: ⏱️ After 5s timeout (closeTimeout)
Connection marked as forcefully closed Note over ClientMQAdapter: Next disconnect will use
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
(reasonCode, description) WebSocketServer->>ClientMQAdapter: disconnectDelegate(socket, code, description) deactivate WebSocketServer alt Multiple close events ClientMQAdapter->>ClientMQAdapter: Log warning, return early
(_connectionsOpen NOT decremented) deactivate ClientMQAdapter else Socket already disconnected alt Previously forcefully closed ClientMQAdapter->>ClientMQ: emit('disconnect-after-forcefull-close')
(_connectionsOpen NOT decremented) deactivate ClientMQAdapter activate ClientMQ deactivate ClientMQ else Already disconnected ClientMQAdapter->>ClientMQ: emit('disconnect-already-disconnected')
(_connectionsOpen NOT decremented) deactivate ClientMQAdapter activate ClientMQ deactivate ClientMQ 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) deactivate ClientMQAdapter activate ClientMQ ClientMQ->>ClientMQ: Remove from _adaptersById ClientMQ->>ClientMQWorker: emit('disconnect', connectionId, code, description) deactivate ClientMQ activate ClientMQWorker rect rgb(255, 255, 200) Note over ClientMQWorker: 🔢 CONNECTION COUNTER MUTATION
⚠️ 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', ...) activate MQWorker deactivate MQWorker deactivate ClientMQWorker end end end Note over Client,ClientMQWorker: === Error Handling === alt Socket error activate Client Client->>WebSocketServer: error event deactivate Client activate WebSocketServer WebSocketServer->>WebSocketServer: Log error deactivate WebSocketServer else Handler error activate WebSocketServer WebSocketServer->>WebSocketServer: Log error, continue deactivate WebSocketServer else Send error (not opened) activate ClientMQWorker ClientMQWorker->>ClientMQ: close(connectionId, 3335, 'unexpected-close') deactivate ClientMQWorker activate ClientMQ deactivate ClientMQ activate ClientMQWorker ClientMQWorker->>MQWorker: request('pcast.ConnectionDisconnected', ...) activate MQWorker deactivate MQWorker deactivate ClientMQWorker Note over ClientMQAdapter: See "Server-Initiated Close" above else Send error (not found/closed) activate ClientMQWorker ClientMQWorker->>ClientMQWorker: Return {status: 'closed'} deactivate ClientMQWorker end ```