Capstone: Building a Real-Time Feature

WebSockets & Real-Time Communication — Capstone: Building a Real-Time Feature
WebSockets & Real-Time Communication
Chapter 9 · Capstone: Building a Real-Time Feature

🏁 Capstone: Building a Real-Time Feature

Every previous chapter built one piece in isolation. This capstone puts them together into a single, real feature: an authenticated live chat room with presence, reconnection, and abuse protection — nothing new is introduced here, only assembly.

What's Being Built

  • A user connects with a token and joins a named room
  • Everyone in the room sees who's present, and is notified when someone joins or leaves
  • Messages are validated, rate-limited, and safely rendered
  • A dropped connection reconnects automatically and rejoins the room it was in

🖥️ Server: Everything Combined

import { WebSocketServer } from "ws"; const rooms = new Map(); // Ch.6 — roomName -> Set of sockets const wss = new WebSocketServer({ port: 8080, verifyClient(info, callback) { // Ch.8 — auth + origin, before the handshake completes if (!["https://example.com"].includes(info.origin)) { return callback(false, 403, "Forbidden origin"); } const token = new URL(info.req.url, "http://x").searchParams.get("token"); const userId = verifyToken(token); // null if invalid if (!userId) return callback(false, 401, "Unauthorized"); info.req.userId = userId; // carried forward to the "connection" event callback(true); } }); wss.on("connection", (socket, req) => { socket.userId = req.userId; socket.rooms = new Set(); // Ch.6 — tracked for cleanup on close attachRateLimit(socket); // Ch.8 — token bucket per connection socket.on("message", (raw) => { let data; try { data = JSON.parse(raw.toString()); } catch { return; } if (!validateChatMessage(data)) return; // Ch.8 — shape check if (data.type === "join") joinRoom(socket, data.roomName); if (data.type === "chat") sendToRoom(data.roomName, JSON.stringify({ type: "chat", userId: socket.userId, text: data.text })); }); socket.on("close", () => { for (const roomName of socket.rooms) leaveRoom(socket, roomName); // Ch.6 cleanup }); });

joinRoom, leaveRoom, sendToRoom, and attachRateLimit are exactly the functions built in Chapters 6 and 8 — nothing here is new, only wired together.

🌐 Client: Reconnecting and Rendering Safely

const chat = new ReconnectingSocket(`wss://example.com/chat?token=${myToken}`); // Ch.5 chat.socket.addEventListener("open", () => { chat.send(JSON.stringify({ type: "join", roomName: "lobby" })); // rejoin on every reconnect too }); chat.socket.addEventListener("message", (event) => { const data = JSON.parse(event.data); const messageDiv = document.createElement("div"); messageDiv.textContent = `${data.userId}: ${data.text}`; // Ch.8 — never innerHTML chatWindow.appendChild(messageDiv); });

Rejoining After Reconnect

Because ReconnectingSocket creates a brand-new socket on every reconnect (Chapter 5), the "join" message is sent again in the open handler every time — the server has no memory of a previous connection that's now gone.

Where Chapter 7 Would Slot In

This version runs on one server. Adding a second instance for scale means routing sendToRoom through Redis pub/sub instead of calling it directly — the room logic itself doesn't change at all.

An Honest Look Back at Chapter 4

What Was Hand-Built

Reconnection, rooms, presence, rate limiting, and message validation — every one of these exists for free in Socket.IO.

What Was Gained

Full control over every piece, no Engine.IO framing overhead, and a server reachable by any WebSocket client — not just other Socket.IO instances.

💻 Coding Challenges

Challenge 1: Add a "Leave Room" Message Type

Extend the server's message handler to support { type: "leave", roomName }, calling leaveRoom explicitly rather than only relying on the close event's cleanup.

Goal: Practice adding a new message type to an existing validated message-handling structure.

→ Solution

Challenge 2: Show a Reconnecting Indicator

Using the connection-state ideas from Chapter 5, update the client so that the chat window displays a visible "Reconnecting..." banner between the close and next successful open event, and hides it once reconnected.

Goal: Practice reflecting connection state in the UI for this specific feature.

→ Solution

Challenge 3: Trace a Full Connection Lifecycle

Narrate, step by step, everything that happens from the moment a browser tab calls new ReconnectingSocket(...) to the moment a "chat" message it sends is rendered in another user's browser — naming which chapter's mechanism handles each step.

Goal: Practice holding the entire course's material together as one coherent system.

→ Solution

⚠️ Gotcha: Testing This Requires More Than One Client

A single-request HTTP endpoint can be tested in isolation — send a request, check the response. A real-time feature like this one is only actually exercised by multiple concurrent connections interacting: presence only means something with two clients, broadcasting only proves anything when a second client actually receives what a first one sent, and reconnection logic only matters when a connection is deliberately killed mid-session. Testing this course's capstone properly means opening multiple simulated clients against the same server and asserting on cross-client behavior — a fundamentally different testing shape than the single-request tests most of the earlier framework courses relied on.

🎓 Course Complete

This closes out WebSockets & Real-Time Communication — from the basic question of why HTTP can't push, through the protocol itself, a hand-built server and client, a fair comparison against Socket.IO, rooms and presence, scaling across multiple servers, security, and finally this capstone tying every piece into one working feature.