Managing Connected Clients
In Post #8 we built the raw WebSocket connection. Now we build the chat logic on top: tracking clients, rooms, usernames, and broadcasting messages.
// chat.js — the core state
// Map of socket → client info
const clients = new Map();
// Map of roomName → Set of sockets
const rooms = new Map();
function addClient(socket) {
clients.set(socket, {
id: crypto.randomUUID(),
username: null,
room: null,
connectedAt: Date.now()
});
}
function removeClient(socket) {
const client = clients.get(socket);
if (client?.room) {
leaveRoom(socket, client.room);
}
clients.delete(socket);
}
function getClient(socket) {
return clients.get(socket);
}
Joining and Leaving Rooms
function joinRoom(socket, roomName) {
const client = clients.get(socket);
if (!client) return;
// Leave current room if in one
if (client.room) leaveRoom(socket, client.room);
// Create room if it doesn't exist
if (!rooms.has(roomName)) {
rooms.set(roomName, new Set());
}
rooms.get(roomName).add(socket);
client.room = roomName;
// Notify others in room
broadcast(roomName, {
type: 'system',
text: `${client.username} joined the room`,
room: roomName,
timestamp: Date.now()
}, socket); // exclude the joining user
}
function leaveRoom(socket, roomName) {
const client = clients.get(socket);
const room = rooms.get(roomName);
if (!room) return;
room.delete(socket);
client.room = null;
// Clean up empty rooms
if (room.size === 0) rooms.delete(roomName);
else {
broadcast(roomName, {
type: 'system',
text: `${client.username} left the room`,
room: roomName,
timestamp: Date.now()
});
}
}
Broadcasting Messages
const { sendMessage } = require('./ws');
function broadcast(roomName, message, excludeSocket = null) {
const room = rooms.get(roomName);
if (!room) return;
const json = JSON.stringify(message);
for (const socket of room) {
if (socket === excludeSocket) continue;
if (!socket.destroyed) {
sendMessage(socket, json);
}
}
}
function sendToClient(socket, message) {
if (!socket.destroyed) {
sendMessage(socket, JSON.stringify(message));
}
}
Handling Incoming Messages
function handleMessage(socket, raw) {
let msg;
try {
msg = JSON.parse(raw);
} catch {
return; // ignore malformed messages
}
const client = clients.get(socket);
switch (msg.type) {
case 'register': {
const username = String(msg.username || '').trim().slice(0, 20);
if (!username) {
sendToClient(socket, { type: 'error', text: 'Username required' });
return;
}
client.username = username;
sendToClient(socket, { type: 'registered', username });
break;
}
case 'join': {
if (!client.username) {
sendToClient(socket, { type: 'error', text: 'Register first' });
return;
}
const roomName = String(msg.room || 'general').trim().slice(0, 30);
joinRoom(socket, roomName);
sendToClient(socket, {
type: 'joined',
room: roomName,
members: getRoomMembers(roomName)
});
break;
}
case 'message': {
if (!client.username || !client.room) {
sendToClient(socket, { type: 'error', text: 'Join a room first' });
return;
}
const text = String(msg.text || '').trim().slice(0, 500);
if (!text) return;
broadcast(client.room, {
type: 'message',
username: client.username,
text,
room: client.room,
timestamp: Date.now()
});
break;
}
}
}
function getRoomMembers(roomName) {
const room = rooms.get(roomName);
if (!room) return [];
return [...room].map(s => clients.get(s)?.username).filter(Boolean);
}
The Vanilla JS Chat Frontend
// static/chat.js
const ws = new WebSocket(`ws://${location.host}`);
let myUsername = '';
ws.addEventListener('open', () => {
const username = prompt('Enter your username:');
myUsername = username;
ws.send(JSON.stringify({ type: 'register', username }));
});
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'registered':
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
break;
case 'message':
case 'system':
appendMessage(msg);
break;
case 'joined':
document.getElementById('room-name').textContent = msg.room;
document.getElementById('members').textContent =
msg.members.join(', ');
break;
}
});
function appendMessage(msg) {
const list = document.getElementById('messages');
const li = document.createElement('li');
li.className = msg.type === 'system' ? 'system-msg' : 'chat-msg';
if (msg.type === 'message') {
const time = new Date(msg.timestamp).toLocaleTimeString();
li.innerHTML = `
<span class="time">${time}</span>
<span class="user">${msg.username}</span>
<span class="text"></span>
`;
li.querySelector('.text').textContent = msg.text; // safe
} else {
li.textContent = msg.text;
}
list.appendChild(li);
list.scrollTop = list.scrollHeight;
}
document.getElementById('send-form')
.addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('message-input');
const text = input.value.trim();
if (!text) return;
ws.send(JSON.stringify({ type: 'message', text }));
input.value = '';
});
Wiring Into the Server
// server.js — upgrade handler
const chat = require('./chat');
server.on('upgrade', (req, socket) => {
const ws = handleUpgrade(req, socket);
chat.addClient(ws);
ws.on('data', (buffer) => {
const frame = parseFrame(buffer);
if (frame.type === 'close') {
chat.removeClient(ws);
ws.destroy();
} else if (frame.type === 'message') {
chat.handleMessage(ws, frame.data);
}
});
ws.on('close', () => chat.removeClient(ws));
ws.on('error', () => chat.removeClient(ws));
});
Key takeaways:
Mapof socket → client info — the backbone of connection trackingMapof roomName →Setof sockets — clean room management- Broadcast = iterate room's Set, call
sendMessageon each socket - JSON-based message protocol —
{ type, ... }switch pattern - Always
textContentfor user messages in the DOM — neverinnerHTML
What's Next
Module 3 complete! In Post #10, we start the mini Store — beginning with sessions and cookies, so users can stay logged in without any session management packages.