Chat Application

Real-time communication. WebSocket, message flow, and handling connectivity.

WebSocket Basics

Persistent two-way connection between client and server.

// WebSocket vs HTTP:
// HTTP: Client asks → Server responds → connection closes
// WebSocket: Connection stays open → both sides send anytime

// Client-side WebSocket:
const socket = new WebSocket("wss://chat-server.example.com");

socket.addEventListener("open", () => {
  console.log("Connected to server");
  socket.send(JSON.stringify({
    type: "join",
    username: "Alice",
  }));
});

socket.addEventListener("message", (event) => {
  const data = JSON.parse(event.data);
  handleServerMessage(data);
});

socket.addEventListener("close", (event) => {
  console.log("Disconnected:", event.code, event.reason);
  // Attempt reconnection...
});

socket.addEventListener("error", (event) => {
  console.error("WebSocket error:", event);
});

// Server-side (Node.js + ws library):
// const WebSocket = require("ws");
// const wss = new WebSocket.Server({ port: 8080 });
//
// wss.on("connection", (ws) => {
//   ws.on("message", (data) => {
//     const msg = JSON.parse(data);
//     // Broadcast to all clients:
//     wss.clients.forEach(client => {
//       if (client.readyState === WebSocket.OPEN) {
//         client.send(data);
//       }
//     });
//   });
// });

Message Flow

Structured message protocol between client and server.

// Message protocol — every message has a "type":
// Client → Server:
// { type: "join", username: "Alice" }
// { type: "message", text: "Hello!" }
// { type: "typing", isTyping: true }
// { type: "leave" }

// Server → Client:
// { type: "message", id: "uuid", username: "Bob", text: "Hi!", timestamp: ... }
// { type: "user-joined", username: "Alice", onlineCount: 5 }
// { type: "user-left", username: "Alice", onlineCount: 4 }
// { type: "typing", username: "Bob", isTyping: true }
// { type: "history", messages: [...] }

// Send a chat message:
function sendMessage(text) {
  if (!text.trim() || socket.readyState !== WebSocket.OPEN) return;

  const message = {
    type: "message",
    text: text.trim(),
    timestamp: Date.now(),
  };

  socket.send(JSON.stringify(message));
  // Optimistic UI — show immediately:
  appendMessage({ ...message, username: currentUser, isMine: true });
}

Define a clear protocol: every message is JSON with a 'type' field. The server routes based on type.

1 / 2

UI Rendering

Chat bubbles, timestamps, and auto-scroll.

// Chat UI structure:
// <div class="chat-app">
//   <header>
//     <h1>Chat Room</h1>
//     <span class="online-count">3 online</span>
//   </header>
//   <div class="messages"></div>
//   <div class="typing-indicator hidden"></div>
//   <form class="message-form">
//     <input type="text" placeholder="Type a message...">
//     <button type="submit">Send</button>
//   </form>
// </div>

const messagesEl = document.querySelector(".messages");

function appendMessage(msg, shouldScroll = true) {
  const isMine = msg.username === currentUser;
  const time = formatTime(msg.timestamp);

  const messageEl = document.createElement("div");
  messageEl.className = `message ${isMine ? "mine" : "theirs"}`;
  messageEl.innerHTML = `
    ${!isMine ? `<span class="username">${escapeHtml(msg.username)}</span>` : ""}
    <div class="bubble">
      <p>${escapeHtml(msg.text)}</p>
      <time>${time}</time>
    </div>
  `;

  messagesEl.appendChild(messageEl);
  if (shouldScroll) scrollToBottom();
}

function appendSystemMessage(text) {
  const el = document.createElement("div");
  el.className = "system-message";
  el.textContent = text;
  messagesEl.appendChild(el);
  scrollToBottom();
}

// Smart auto-scroll: only scroll if user is near bottom:
function scrollToBottom() {
  const { scrollTop, scrollHeight, clientHeight } = messagesEl;
  const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
  if (isNearBottom) {
    messagesEl.scrollTop = scrollHeight;
  }
}

function formatTime(timestamp) {
  return new Date(timestamp).toLocaleTimeString([], {
    hour: "2-digit", minute: "2-digit",
  });
}

Features

Typing indicators, online status, and notifications.

// Typing indicator:
const typingEl = document.querySelector(".typing-indicator");
const typingUsers = new Set();

// Send typing status (debounced):
let typingTimeout;
input.addEventListener("input", () => {
  socket.send(JSON.stringify({ type: "typing", isTyping: true }));
  clearTimeout(typingTimeout);
  typingTimeout = setTimeout(() => {
    socket.send(JSON.stringify({ type: "typing", isTyping: false }));
  }, 2000); // stop after 2s of no typing
});

// Display who's typing:
function updateTypingIndicator(username, isTyping) {
  if (isTyping) typingUsers.add(username);
  else typingUsers.delete(username);

  if (typingUsers.size === 0) {
    typingEl.classList.add("hidden");
  } else {
    typingEl.classList.remove("hidden");
    const users = [...typingUsers];
    if (users.length === 1) {
      typingEl.textContent = `${users[0]} is typing...`;
    } else {
      typingEl.textContent = `${users.length} people are typing...`;
    }
  }
}

// Browser notifications for new messages:
async function requestNotificationPermission() {
  if (!("Notification" in window)) return;
  if (Notification.permission === "default") {
    await Notification.requestPermission();
  }
}

function notifyNewMessage(msg) {
  if (document.hasFocus()) return; // don't notify if app is active
  if (Notification.permission !== "granted") return;

  new Notification(msg.username + " says:", {
    body: msg.text.slice(0, 100),
    icon: "/chat-icon.png",
    tag: "chat-message", // replace previous notification
  });
}

// Online users list:
function updateOnlineCount(count) {
  document.querySelector(".online-count").textContent =
    `${count} online`;
}

Resilience

Handle disconnections, reconnect automatically.

// Auto-reconnect with exponential backoff:
class ChatConnection {
  constructor(url, username) {
    this.url = url;
    this.username = username;
    this.socket = null;
    this.retryCount = 0;
    this.maxRetries = 5;
    this.messageQueue = []; // buffer messages while disconnected
  }

  connect() {
    this.socket = new WebSocket(this.url);

    this.socket.onopen = () => {
      this.retryCount = 0;
      updateStatus("connected");
      // Send buffered messages:
      while (this.messageQueue.length > 0) {
        this.socket.send(this.messageQueue.shift());
      }
      // Re-join:
      this.send({ type: "join", username: this.username });
    };

    this.socket.onclose = () => {
      updateStatus("disconnected");
      this.scheduleReconnect();
    };

    this.socket.onmessage = (e) => handleServerMessage(JSON.parse(e.data));
    this.socket.onerror = () => updateStatus("error");
  }

  send(data) {
    const json = JSON.stringify(data);
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(json);
    } else {
      this.messageQueue.push(json); // buffer for later
    }
  }

  scheduleReconnect() {
    if (this.retryCount >= this.maxRetries) {
      updateStatus("failed");
      return;
    }
    const delay = Math.min(1000 * Math.pow(2, this.retryCount), 30000);
    this.retryCount++;
    updateStatus(`reconnecting in ${delay / 1000}s...`);
    setTimeout(() => this.connect(), delay);
  }

  disconnect() {
    this.maxRetries = 0; // prevent reconnect
    this.socket?.close();
  }
}

// Usage:
const chat = new ChatConnection("wss://server.example.com", "Alice");
chat.connect();

// Key patterns in this project:
// ✓ WebSocket real-time communication
// ✓ Message protocol design
// ✓ Optimistic UI updates
// ✓ Auto-reconnect with backoff
// ✓ Message buffering during disconnect
// ✓ Typing indicators with debounce
// ✓ Browser Notifications API
// ✓ Smart auto-scroll

FAQ

Common questions about this project.