Skip to main content

Overview

Exchange Web uses WebSocket connections for real-time trading data. The WsManager class implements a singleton pattern to manage a single persistent WebSocket connection across the entire application.

WsManager Singleton

The WsManager provides centralized WebSocket management with callback-based message routing.

Class Structure

From apps/web/src/utils/ws_manager.ts:4-24:
export class WsManager {
  private ws: WebSocket;
  private static instance: WsManager;
  private bufferedMessages: any[] = [];
  private callbacks: any = {};
  private id: number;
  private initialized: boolean = false;

  private constructor() {
    this.ws = new WebSocket(BASE_URL);
    this.bufferedMessages = [];
    this.id = 1;
    this.init();
  }

  public static getInstance() {
    if (!this.instance) {
      this.instance = new WsManager();
    }
    return this.instance;
  }
The singleton pattern ensures only one WebSocket connection exists, preventing multiple connections and reducing server load.

Connection Configuration

From apps/web/src/utils/ws_manager.ts:1-2:
export const BASE_URL = "wss://exchange.jogeshwar.xyz/ws";

Initialization

The WsManager initializes the WebSocket connection and sets up message handling.

Init Method

From apps/web/src/utils/ws_manager.ts:26-53:
init() {
  this.ws.onopen = () => {
    this.initialized = true;
    this.bufferedMessages.forEach((message) => {
      this.ws.send(JSON.stringify(message));
    });
    this.bufferedMessages = [];
  };

  this.ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    const type = message.data.e;
    if (this.callbacks[type]) {
      this.callbacks[type].forEach(({ callback }: { callback: any }) => {
        if (type === "depth") {
          const updatedBids = message.data.b;
          const updatedAsks = message.data.a;
          callback({ bids: updatedBids, asks: updatedAsks });
        }

        if (type === "trade") {
          const trades = message.data;
          callback(trades);
        }
      });
    }
  };
}

Connection Lifecycle

1

WebSocket Creation

Constructor creates WebSocket connection to exchange server
2

Open Handler

Sets initialized flag and sends buffered messages
3

Message Handler

Routes incoming messages to registered callbacks

Message Buffering

Messages sent before connection is established are buffered and sent once connected.

Send Message Method

From apps/web/src/utils/ws_manager.ts:55-65:
sendMessage(message: any) {
  const messageToSend = {
    ...message,
    id: this.id++,
  };
  if (!this.initialized) {
    this.bufferedMessages.push(messageToSend);
    return;
  }
  this.ws.send(JSON.stringify(messageToSend));
}
Message buffering ensures no subscription requests are lost during the connection establishment phase.

Buffering Flow

Callback System

The callback system allows multiple components to subscribe to WebSocket events.

Callback Structure

callbacks = {
  "depth": [
    { callback: function1, id: "DEPTH-SOL_USDC" },
    { callback: function2, id: "DEPTH-SOL_USDCC" }
  ],
  "trade": [
    { callback: function3, id: "TRADE-SOL_USDC" }
  ]
}

Register Callback

From apps/web/src/utils/ws_manager.ts:67-71:
async registerCallback(type: string, callback: any, id: string) {
  this.callbacks[type] = this.callbacks[type] || [];
  this.callbacks[type].push({ callback, id });
  // "ticker" => callback
}

Deregister Callback

From apps/web/src/utils/ws_manager.ts:73-82:
async deRegisterCallback(type: string, id: string) {
  if (this.callbacks[type]) {
    const index = this.callbacks[type].findIndex(
      (callback: any) => callback.id === id
    );
    if (index !== -1) {
      this.callbacks[type].splice(index, 1);
    }
  }
}
Always deregister callbacks in component cleanup to prevent memory leaks and duplicate event handling.

Event Types

The WebSocket server sends two main event types:

depth

Order book updates with bids and asks

trade

New trade executions with price and quantity

Depth Event

Message structure:
{
  "data": {
    "e": "depth",
    "b": [["100.5", "10"], ["100.4", "5"]],
    "a": [["100.6", "8"], ["100.7", "12"]]
  }
}
Processing from apps/web/src/utils/ws_manager.ts:40-44:
if (type === "depth") {
  const updatedBids = message.data.b;
  const updatedAsks = message.data.a;
  callback({ bids: updatedBids, asks: updatedAsks });
}

Trade Event

Message structure:
{
  "data": {
    "e": "trade",
    "t": 12345,
    "m": false,
    "p": "100.5",
    "q": "1.5",
    "T": 1699564800000
  }
}
Processing from apps/web/src/utils/ws_manager.ts:46-49:
if (type === "trade") {
  const trades = message.data;
  callback(trades);
}

Subscription Management

Components subscribe and unsubscribe to market data streams.

Subscribe Example

From apps/web/src/components/Depth.tsx:125-133:
WsManager.getInstance().sendMessage({
  method: "SUBSCRIBE",
  params: [`depth.${market}`],
});

WsManager.getInstance().sendMessage({
  method: "SUBSCRIBE",
  params: [`trade.${market}`],
});

Unsubscribe Example

From apps/web/src/components/Depth.tsx:182-191:
WsManager.getInstance().sendMessage({
  method: "UNSUBSCRIBE",
  params: [`depth.${market}`],
});

WsManager.getInstance().sendMessage({
  method: "UNSUBSCRIBE",
  params: [`trade.${market}`],
});

Component Integration

Here’s a complete example of WebSocket integration in a component.

Full Integration Pattern

From apps/web/src/components/Depth.tsx:
useEffect(() => {
  // Register depth callback
  WsManager.getInstance().registerCallback(
    "depth",
    (data: any) => {
      setBids((originalBids) => {
        // Update bid logic
      });
      setAsks((originalAsks) => {
        // Update ask logic
      });
    },
    `DEPTH-${market}`
  );

  // Register trade callback
  WsManager.getInstance().registerCallback(
    "trade",
    (data: any) => {
      const newTrade: Trade = {
        id: data.t,
        isBuyerMaker: data.m,
        price: data.p,
        quantity: data.q,
        quoteQuantity: data.q,
        timestamp: data.T,
      };

      setPrice(data.p);
      setTrades((oldTrades) => {
        const newTrades = [...oldTrades];
        newTrades.unshift(newTrade);
        newTrades.pop();
        return newTrades;
      });
    },
    `TRADE-${market}`
  );

  // Subscribe to events
  WsManager.getInstance().sendMessage({
    method: "SUBSCRIBE",
    params: [`depth.${market}`],
  });

  WsManager.getInstance().sendMessage({
    method: "SUBSCRIBE",
    params: [`trade.${market}`],
  });

  // Cleanup function
  return () => {
    WsManager.getInstance().deRegisterCallback("depth", `DEPTH-${market}`);
    WsManager.getInstance().sendMessage({
      method: "UNSUBSCRIBE",
      params: [`depth.${market}`],
    });

    WsManager.getInstance().deRegisterCallback("trade", `TRADE-${market}`);
    WsManager.getInstance().sendMessage({
      method: "UNSUBSCRIBE",
      params: [`trade.${market}`],
    });
  };
}, [market]);

Message Flow

Complete Message Flow Diagram

Best Practices

Always use unique IDs when registering callbacks to enable proper cleanup:
const callbackId = `DEPTH-${market}`;
WsManager.getInstance().registerCallback("depth", callback, callbackId);
Always deregister callbacks and unsubscribe in useEffect cleanup:
useEffect(() => {
  // Register and subscribe
  
  return () => {
    // Deregister and unsubscribe
  };
}, [market]);
Consider implementing reconnection logic for production:
this.ws.onclose = () => {
  console.log('Connection closed, reconnecting...');
  setTimeout(() => this.reconnect(), 1000);
};
Validate message structure before processing:
if (message && message.data && message.data.e) {
  const type = message.data.e;
  // Process message
}

Error Handling

Consider adding error handlers for production:
this.ws.onerror = (error) => {
  console.error('WebSocket error:', error);
  // Notify user or trigger reconnection
};

this.ws.onclose = (event) => {
  console.log('WebSocket closed:', event.code, event.reason);
  // Implement reconnection strategy
};

Performance Considerations

Single Connection

Singleton pattern ensures one connection for all components

Message Buffering

Prevents message loss during connection phase

Callback Routing

Efficient message distribution to multiple subscribers

Unique IDs

Auto-incrementing message IDs for tracking

Next Steps

State Management

Learn how WebSocket data updates application state

Components

See how components use WebSocket data

Build docs developers (and LLMs) love