Broadcasting, Rooms & Presence

WebSockets & Real-Time Communication — Broadcasting, Rooms & Presence
WebSockets & Real-Time Communication
Chapter 6 · Broadcasting, Rooms & Presence

🏠 Broadcasting, Rooms & Presence

Chapter 3's broadcast sent every message to everyone. Real applications need targeted messaging — a chat channel, a game table, a document's collaborators — not one global blast. Chapter 4 pointed out that Socket.IO gives you rooms for free; this chapter builds the equivalent by hand on raw ws, which is exactly what Chapter 7's scaling approach will extend across multiple servers.

Modeling Rooms

Instead of Chapter 3's single global Set, a room system needs a collection of sets — one per room:

const rooms = new Map(); // roomName -> Set of sockets function joinRoom(socket, roomName) { if (!rooms.has(roomName)) rooms.set(roomName, new Set()); rooms.get(roomName).add(socket); socket.rooms.add(roomName); // tracked on the socket too — see below } function leaveRoom(socket, roomName) { rooms.get(roomName)?.delete(socket); socket.rooms.delete(roomName); }

Tracking Rooms on the Socket

A ws socket is just a plain object — attaching socket.rooms = new Set() when a client connects gives each socket its own record of which rooms it's currently in, essential for cleanup on disconnect.

Map of Sets

The rooms Map holds one Set of sockets per room name — the same readyState-checked broadcast pattern from Chapter 3, just scoped to one Set at a time instead of one global Set.

Broadcasting to Just One Room

function sendToRoom(roomName, message) { const members = rooms.get(roomName); if (!members) return; for (const socket of members) { if (socket.readyState === socket.OPEN) { socket.send(message); } } }

Cleaning Up on Disconnect

socket.on("close", () => { for (const roomName of socket.rooms) { leaveRoom(socket, roomName); // remove from every room it was in, not just one } });

This is why tracking rooms on the socket itself (not just in the rooms Map) matters — without socket.rooms, there'd be no way to know which rooms to clean this socket out of.

👀 Presence: Who's Online

A "who's here" feature just means notifying a room when membership changes:

function joinRoom(socket, roomName) { if (!rooms.has(roomName)) rooms.set(roomName, new Set()); rooms.get(roomName).add(socket); socket.rooms.add(roomName); sendToRoom(roomName, JSON.stringify({ type: "presence", event: "joined", userId: socket.userId })); }

Chapter 3's Global Broadcast vs This Chapter's Room Broadcast

Chapter 3

One Set, one audience — every connected client, no matter what they're actually doing.

This Chapter

A Map of many Sets — each message reaches only the clients who actually joined that specific room.

Detecting Dead Connections Faster

An ungraceful disconnect (a dropped Wi-Fi connection, not a clean close) can take a while for the OS-level TCP timeout to notice. The ping/pong frames from Chapter 2 let the server detect a truly dead socket well before that, so presence data doesn't lag behind reality.

Single-Server Limitation

This entire rooms Map lives in one process's memory — a client connected to a different server instance has no visibility into it at all. Chapter 7 covers exactly this problem.

💻 Coding Challenges

Challenge 1: Implement getRoomMembers

Write a function getRoomMembers(roomName) that returns an array of userId values for every socket currently in that room (skip sockets that don't have a userId set).

Goal: Practice reading from the room Map structure for a "who's here" UI feature.

→ Solution

Challenge 2: Broadcast a "Left" Presence Event

Extend leaveRoom so that, in addition to removing the socket from the room, it broadcasts a { type: "presence", event: "left", userId } message to the room's remaining members.

Goal: Practice mirroring the "joined" presence pattern for the leave case.

→ Solution

Challenge 3: Find the Memory Leak

A joinRoom/sendToRoom implementation is used in production, but the close event handler is never wired up to call leaveRoom. Explain what accumulates over time and why it's a problem, even though messages still appear to broadcast correctly at first.

Goal: Practice reasoning about a bug that isn't visible until the system has been running a while.

→ Solution

⚠️ Gotcha: Forgetting Room Cleanup on Disconnect

It's easy to wire up joinRoom and sendToRoom correctly and never notice that leaveRoom isn't being called on disconnect — broadcasts still appear to work fine, because sendToRoom's readyState check silently skips closed sockets. The bug is invisible in normal testing and only shows up as a slow memory leak: every room's Set quietly accumulates references to long-closed sockets forever, since nothing is removing them. Always wire leaveRoom (for every room a socket was in) into the close event, not just into an explicit "leave" action a user might trigger.

🎯 What's Next

Rooms currently live entirely in one server's memory — the next chapter tackles what happens when there's more than one server: Scaling WebSockets — sticky sessions behind a load balancer, and a Redis pub/sub adapter for broadcasting across every instance.