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-scrollFAQ
Common questions about this project.