Skip to content

WebSocketManager

Manages WebSocket connections using the Cloudflare Hibernation API for real-time portfolio event broadcasting.

Purpose

WebSocketManager handles the full lifecycle of WebSocket connections within a UserPaperTradePortfolio Durable Object. It uses the Cloudflare Hibernation API to maintain long-lived connections that survive DO eviction from memory, enabling real-time push notifications without keeping the DO permanently active.

High-Level Design

Data Flow

Client (browser)                    Web Worker                  UserPaperTradePortfolio DO
     │                                  │                                  │
     │  GET /api/ws/portfolio           │                                  │
     │  Upgrade: websocket              │                                  │
     │─────────────────────────────────>│  withWalletAuth                  │
     │                                  │  authenticate user               │
     │                                  │─────────────────────────────────>│
     │                                  │  portfolioDO.fetch(request)      │
     │                                  │                                  │
     │                                  │           WebSocketManager       │
     │                                  │           handleUpgrade()        │
     │                                  │           ┌─────────────────┐    │
     │                                  │           │ WebSocketPair   │    │
     │                                  │           │ [client, server]│    │
     │                                  │           │                 │    │
     │                                  │           │ acceptWebSocket │    │
     │                                  │           │ (Hibernation)   │    │
     │<─────────────────────────────────│<──────────│ 101 Switching   │    │
     │  WebSocket established           │           └─────────────────┘    │
     │                                  │                                  │
     │              ... time passes, DO may be evicted ...                 │
     │                                  │                                  │
     │                                  │   Domain manager calls           │
     │                                  │   broadcast(PortfolioEvent)      │
     │                                  │           ┌─────────────────┐    │
     │  { type, data }                  │           │ JSON.stringify  │    │
     │<─────────────────────────────────│───────────│ → send to ALL   │    │
     │  (all connected sessions)        │           │   sessions      │    │
     │                                  │           └─────────────────┘    │

Hibernation Lifecycle

The Hibernation API is what makes this manager scalable — WebSocket connections survive DO memory eviction:

  1. ConnecthandleUpgrade() accepts the socket via state.acceptWebSocket(server) and serializes session metadata ({ id, userId }) as a WebSocket attachment.

  2. Hibernate — After inactivity, the DO is evicted from memory. Connections stay open. Ping/pong auto-response (setWebSocketAutoResponse) keeps connections alive without waking the DO.

  3. Wake — When an event needs broadcasting (trade executed, alert triggered), the DO wakes and initialize() restores the session map by calling state.getWebSockets() and deserializing each socket's attachment.

  4. Close — Client disconnects or send fails. Session is removed from the map and the server-side socket is closed.

Auto-Subscribe & Cross-Device Sync

The manager uses an auto-subscribe pattern — there is no subscription protocol. Every connected client receives every portfolio event. This works because:

  • The initiating client already has data from the HTTP response (e.g., POST /portfolio/open returns the trade). The WebSocket event is redundant for that client.
  • Other tabs/devices for the same user get the event via WebSocket and update their UI.
  • The frontend deduplicates by entity ID (tradeId, limitOrderId, orderId), so duplicate processing is harmless.

Event Categories

Events are a discriminated union (PortfolioEvent) with four categories:

CategoryEventsTrigger
User-initiatedPOSITION_OPENED, POSITION_CLOSED, POSITION_UPDATED, LEVERAGE_CHANGED, PENDING_ORDER_CREATED/EXECUTED/CANCELLED, LIMIT_ORDER_CREATED/CANCELLED/UPDATEDUser action via HTTP → domain manager broadcasts
Server-initiatedLIMIT_ORDER_TRIGGERED, STOP_LOSS_TRIGGERED, TAKE_PROFIT_TRIGGERED, POSITION_LIQUIDATEDPriceAlert alarm detects threshold cross → calls onAlert() → domain manager broadcasts
Portfolio statePORTFOLIO_RESET, POSITION_ALERTS_UPDATED, CHALLENGE_STARTED/PASSED/FAILED/DEACTIVATEDState transitions from various managers
Notification & riskNOTIFICATION_CREATED/SEEN/DELETED, NOTIFICATIONS_MARKED_SEEN/ALL_SEEN, MLL_STATUS_CHANGEDNotificationManager and AccountHealthManager

All trade-related events include balance snapshot fields (newBalance, newTotalRealizedPnL, newTotalFeesPaid) so the frontend can update account value in real time without a separate API call.

Broadcast Failure Isolation

When broadcast() fails to send to a specific client, that connection is closed and removed — other sessions continue receiving events. This prevents one bad connection from breaking the broadcast for all clients.

See Also