1. The Problem
Build the message-routing core of a chat server: a hub tracks connected clients, and when one client sends a message the hub broadcasts it to every other client (not back to the sender). Clients can join and leave at any time.
Why this project? Every real-time app — chat, multiplayer games, live dashboards — has this pub/sub hub at its core. The networking (sockets) is plumbing; the interesting logic is who receives what.
2. How to Think About This
Model each client as an object with a send() method and keep a list of connected clients in the hub. broadcast(message, sender) loops the clients and delivers to everyone except the sender. Join/leave just add/remove from the list. In a real server each client is a live TCP or WebSocket connection and send() writes to that socket — but the routing logic is identical, which is why we can build and test it without any networking.
3. The Build
// A client: in a real server this wraps a live socket; here .send()
// just records what the client received so we can see the routing.
export class Client {
constructor(name) { this.name = name; this.inbox = []; }
send(message) { this.inbox.push(message); }
}
export class ChatHub {
constructor() { this.clients = []; }
join(client) { this.clients.push(client); }
leave(client) { this.clients = this.clients.filter(c => c !== client); }
// Deliver a message to everyone EXCEPT the sender.
broadcast(message, sender) {
for (const client of this.clients) {
if (client !== sender) client.send(message);
}
}
}
// Demo
const hub = new ChatHub();
const alice = new Client("alice");
const bob = new Client("bob");
const carol = new Client("carol");
[alice, bob, carol].forEach(c => hub.join(c));
hub.broadcast("hello everyone", alice);
console.log("bob heard:", bob.inbox); // [ 'hello everyone' ]
console.log("carol heard:", carol.inbox); // [ 'hello everyone' ]
console.log("alice heard:", alice.inbox); // [] (sender excluded)
4. Test & Prove
import { ChatHub, Client } from "./chat.mjs";
let pass = 0, fail = 0;
function test(desc, fn) {
try { fn(); console.log(" \u2713", desc); pass++; }
catch (e) { console.log(" \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }
test("a message reaches every other client", () => {
const hub = new ChatHub();
const a = new Client("a"), b = new Client("b"), c = new Client("c");
[a, b, c].forEach(x => hub.join(x));
hub.broadcast("hi", a);
assert(b.inbox[0] === "hi" && c.inbox[0] === "hi");
});
test("the sender does not receive its own message", () => {
const hub = new ChatHub();
const a = new Client("a"), b = new Client("b");
[a, b].forEach(x => hub.join(x));
hub.broadcast("hi", a);
assert(a.inbox.length === 0);
});
test("a client who left stops receiving", () => {
const hub = new ChatHub();
const a = new Client("a"), b = new Client("b");
[a, b].forEach(x => hub.join(x));
hub.leave(b);
hub.broadcast("hi", a);
assert(b.inbox.length === 0);
});
test("messages accumulate in order", () => {
const hub = new ChatHub();
const a = new Client("a"), b = new Client("b");
[a, b].forEach(x => hub.join(x));
hub.broadcast("one", a);
hub.broadcast("two", a);
assert(JSON.stringify(b.inbox) === JSON.stringify(["one", "two"]));
});
console.log(`\n${pass} passed, ${fail} failed`);
5. The Interface
node chat.mjs bob heard: [ 'hello everyone' ] carol heard: [ 'hello everyone' ] alice heard: []
6. Run It
node chat.mjs
node chat.test.mjs
# The full networked server uses Node's net or ws module to accept real
# socket connections; the broadcast/join/leave logic above is unchanged.🎯 Try this next
- Wire it to Node\u2019s ws (WebSocket) module so real browsers can connect
- Add named rooms/channels so a message only reaches one room
- Add usernames and prefix each broadcast with who sent it
- Add private (direct) messages between two named clients