1. Polling là gì?

Polling là kỹ thuật client định kỳ gửi request đến server để kiểm tra dữ liệu mới. Có hai loại polling chính:

1.1 Short Polling

async function shortPolling() {
  while (true) {
    try {
      const response = await fetch("/api/updates");
      const data = await response.json();
      processData(data);
    } catch (error) {
      console.error("Polling error:", error);
    }

    // Đợi 5 giây trước khi gửi request tiếp theo
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
}

function processData(data) {
  console.log("New data:", data);
}

1.2 Long Polling

interface UpdateResponse {
  data: any;
  timestamp: number;
}

class LongPollingClient {
  private lastTimestamp: number = 0;
  private endpoint: string;
  private timeout: number;

  constructor(endpoint: string, timeout: number = 30000) {
    this.endpoint = endpoint;
    this.timeout = timeout;
  }

  async startPolling() {
    while (true) {
      try {
        const response = await fetch(
          `${this.endpoint}?lastTimestamp=${this.lastTimestamp}`,
          {
            signal: AbortSignal.timeout(this.timeout),
          }
        );

        if (response.ok) {
          const data: UpdateResponse = await response.json();
          this.lastTimestamp = data.timestamp;
          this.handleUpdate(data);
        }
      } catch (error) {
        if (error instanceof DOMException && error.name === "TimeoutError") {
          continue; // Restart polling on timeout
        }
        console.error("Long polling error:", error);
        await new Promise((resolve) => setTimeout(resolve, 1000));
      }
    }
  }

  private handleUpdate(data: UpdateResponse) {
    console.log("Received update:", data);
  }
}

// Usage
const client = new LongPollingClient("/api/updates");
client.startPolling();

2. WebSockets là gì?

WebSocket là giao thức cho phép giao tiếp hai chiều giữa client và server qua một kết nối duy nhất.

2.1 Basic WebSocket Implementation

class WebSocketClient {
  private ws: WebSocket;
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private reconnectDelay: number = 1000;

  constructor(private url: string) {
    this.connect();
  }

  private connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log("Connected to server");
      this.reconnectAttempts = 0;
    };

    this.ws.onmessage = (event) => {
      this.handleMessage(event.data);
    };

    this.ws.onclose = () => {
      console.log("Connection closed");
      this.handleReconnect();
    };

    this.ws.onerror = (error) => {
      console.error("WebSocket error:", error);
    };
  }

  private handleReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);

      setTimeout(() => {
        this.connect();
      }, this.reconnectDelay * this.reconnectAttempts);
    } else {
      console.error("Max reconnection attempts reached");
    }
  }

  private handleMessage(data: string) {
    try {
      const parsedData = JSON.parse(data);
      console.log("Received:", parsedData);
    } catch (error) {
      console.error("Error parsing message:", error);
    }
  }

  public send(data: any) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.error("Connection not open");
    }
  }

  public close() {
    this.ws.close();
  }
}

2.2 Type-safe WebSocket với TypeScript

interface WebSocketMessage<T = any> {
  type: string;
  payload: T;
}

interface ChatMessage {
  id: string;
  user: string;
  content: string;
  timestamp: number;
}

class TypedWebSocketClient {
  private ws: WebSocket;
  private messageHandlers: Map<string, (payload: any) => void>;

  constructor(url: string) {
    this.ws = new WebSocket(url);
    this.messageHandlers = new Map();
    this.setupWebSocket();
  }

  private setupWebSocket() {
    this.ws.onmessage = (event) => {
      try {
        const message: WebSocketMessage = JSON.parse(event.data);
        const handler = this.messageHandlers.get(message.type);

        if (handler) {
          handler(message.payload);
        }
      } catch (error) {
        console.error("Error handling message:", error);
      }
    };
  }

  public on<T>(type: string, handler: (payload: T) => void) {
    this.messageHandlers.set(type, handler);
  }

  public send<T>(type: string, payload: T) {
    const message: WebSocketMessage<T> = { type, payload };
    this.ws.send(JSON.stringify(message));
  }
}

// Usage
const chat = new TypedWebSocketClient("ws://chat.server");

chat.on<ChatMessage>("message", (message) => {
  console.log(`${message.user}: ${message.content}`);
});

chat.send("message", {
  id: crypto.randomUUID(),
  user: "John",
  content: "Hello!",
  timestamp: Date.now(),
});

3. Ví dụ Thực Tế: Real-time Chat Application

3.1 Polling Version

interface ChatState {
  messages: ChatMessage[];
  users: string[];
  lastUpdate: number;
}

class PollingChatClient {
  private state: ChatState = {
    messages: [],
    users: [],
    lastUpdate: 0,
  };

  constructor(private apiUrl: string) {}

  async start() {
    this.pollMessages();
    this.pollUsers();
  }

  private async pollMessages() {
    while (true) {
      try {
        const response = await fetch(
          `${this.apiUrl}/messages?since=${this.state.lastUpdate}`
        );
        const data = await response.json();

        if (data.messages.length > 0) {
          this.state.messages.push(...data.messages);
          this.state.lastUpdate = data.timestamp;
          this.renderMessages();
        }
      } catch (error) {
        console.error("Error polling messages:", error);
      }

      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }

  private async pollUsers() {
    while (true) {
      try {
        const response = await fetch(`${this.apiUrl}/users`);
        const users = await response.json();

        if (JSON.stringify(users) !== JSON.stringify(this.state.users)) {
          this.state.users = users;
          this.renderUsers();
        }
      } catch (error) {
        console.error("Error polling users:", error);
      }

      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }

  private renderMessages() {
    // Update UI with new messages
  }

  private renderUsers() {
    // Update UI with user list
  }
}

3.2 WebSocket Version

interface ChatEvent {
  type: "message" | "user_joined" | "user_left";
  payload: any;
}

class WebSocketChatClient {
  private ws: WebSocket;
  private state: ChatState = {
    messages: [],
    users: [],
    lastUpdate: 0,
  };

  constructor(private wsUrl: string) {
    this.connect();
  }

  private connect() {
    this.ws = new WebSocket(this.wsUrl);

    this.ws.onmessage = (event) => {
      const chatEvent: ChatEvent = JSON.parse(event.data);

      switch (chatEvent.type) {
        case "message":
          this.handleNewMessage(chatEvent.payload);
          break;
        case "user_joined":
          this.handleUserJoined(chatEvent.payload);
          break;
        case "user_left":
          this.handleUserLeft(chatEvent.payload);
          break;
      }
    };

    this.ws.onclose = () => {
      console.log("Connection lost, reconnecting...");
      setTimeout(() => this.connect(), 1000);
    };
  }

  private handleNewMessage(message: ChatMessage) {
    this.state.messages.push(message);
    this.state.lastUpdate = Date.now();
    this.renderMessages();
  }

  private handleUserJoined(user: string) {
    if (!this.state.users.includes(user)) {
      this.state.users.push(user);
      this.renderUsers();
    }
  }

  private handleUserLeft(user: string) {
    const index = this.state.users.indexOf(user);
    if (index !== -1) {
      this.state.users.splice(index, 1);
      this.renderUsers();
    }
  }

  public sendMessage(content: string) {
    const message: ChatMessage = {
      id: crypto.randomUUID(),
      user: "current_user",
      content,
      timestamp: Date.now(),
    };

    this.ws.send(
      JSON.stringify({
        type: "message",
        payload: message,
      })
    );
  }

  private renderMessages() {
    // Update UI with new messages
  }

  private renderUsers() {
    // Update UI with user list
  }
}

4. So sánh Polling và WebSockets

4.1 Polling

Ưu điểm:

  • Dễ triển khai
  • Hoạt động với mọi browser
  • Không cần thay đổi server infrastructure
  • Phù hợp với dữ liệu cập nhật không thường xuyên

Nhược điểm:

  • Tốn bandwidth
  • Độ trễ cao
  • Server load lớn
  • Không real-time thực sự

4.2 WebSockets

Ưu điểm:

  • Real-time thực sự
  • Hiệu quả về bandwidth
  • Độ trễ thấp
  • Giao tiếp hai chiều

Nhược điểm:

  • Phức tạp hơn để triển khai
  • Cần server hỗ trợ WebSocket
  • Có thể gặp vấn đề với firewalls
  • Cần xử lý reconnection

5. Khi nào nên sử dụng?

5.1 Sử dụng Polling khi:

  • Dữ liệu cập nhật không thường xuyên
  • Không yêu cầu real-time tuyệt đối
  • Server infrastructure đơn giản
  • Cần hỗ trợ nhiều loại client

5.2 Sử dụng WebSockets khi:

  • Cần real-time thực sự
  • Dữ liệu cập nhật thường xuyên
  • Cần giao tiếp hai chiều
  • Bandwidth là vấn đề quan trọng

6. Kết luận

Polling và WebSockets đều có vai trò riêng trong real-time communication. Việc lựa chọn phụ thuộc vào yêu cầu cụ thể của ứng dụng, infrastructure hiện có và trade-offs có thể chấp nhận được.