const { WebSocketServer } = require('ws');
const crypto = require('crypto');
const wss = new WebSocketServer({ port: 8080 });
function broadcast(payload, except) {
const data = JSON.stringify(payload);
for (const client of wss.clients) {
if (client === except) continue;
if (client.readyState === client.OPEN) client.send(data);
}
}
wss.on('connection', (ws) => {
ws.id = crypto.randomBytes(4).toString('hex');
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
broadcast({ type: 'system', text: `${ws.id} joined` }, ws);
ws.on('message', (raw) => {
let msg;
try { msg = JSON.parse(raw); } catch { return; }
if (typeof msg.text !== 'string') return;
broadcast({ type: 'chat', from: ws.id, text: msg.text.slice(0, 2000) });
});
ws.on('close', () => {
broadcast({ type: 'system', text: `${ws.id} left` });
});
});
const heartbeat = setInterval(() => {
for (const ws of wss.clients) {
if (!ws.isAlive) { ws.terminate(); continue; }
ws.isAlive = false;
ws.ping();
}
}, 30000);
wss.on('close', () => clearInterval(heartbeat));
export class ReconnectingSocket {
constructor(url, { maxDelay = 15000, base = 500 } = {}) {
this.url = url;
this.maxDelay = maxDelay;
this.base = base;
this.attempt = 0;
this.queue = [];
this.stopped = false;
this.handlers = { message: () => {}, status: () => {} };
this.connect();
}
onMessage(fn) { this.handlers.message = fn; return this; }
onStatus(fn) { this.handlers.status = fn; return this; }
connect() {
this.handlers.status('connecting');
const ws = new WebSocket(this.url);
this.ws = ws;
ws.addEventListener('open', () => {
this.attempt = 0;
this.handlers.status('open');
while (this.queue.length) ws.send(this.queue.shift());
});
ws.addEventListener('message', (e) => {
try { this.handlers.message(JSON.parse(e.data)); } catch {}
});
ws.addEventListener('close', () => {
if (this.stopped) return;
this.handlers.status('reconnecting');
const delay = Math.min(this.maxDelay, this.base * 2 ** this.attempt);
this.attempt += 1;
setTimeout(() => this.connect(), delay + Math.random() * 250);
});
ws.addEventListener('error', () => ws.close());
}
send(payload) {
const data = JSON.stringify(payload);
if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
else this.queue.push(data);
}
close() {
this.stopped = true;
if (this.ws) this.ws.close();
}
}
import { ReconnectingSocket } from './ReconnectingSocket.js';
const log = document.querySelector('#log');
const status = document.querySelector('#status');
const form = document.querySelector('#composer');
const input = document.querySelector('#text');
const socket = new ReconnectingSocket('wss://chat.example.com/ws')
.onStatus((state) => {
status.textContent = state;
status.dataset.state = state;
})
.onMessage((msg) => {
const row = document.createElement('div');
row.className = `msg msg--${msg.type}`;
row.textContent = msg.type === 'chat'
? `${msg.from}: ${msg.text}`
: msg.text;
log.appendChild(row);
log.scrollTop = log.scrollHeight;
});
form.addEventListener('submit', (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
socket.send({ text });
input.value = '';
});
window.addEventListener('beforeunload', () => socket.close());
This snippet shows a full-duplex chat built on raw WebSockets: a broadcasting server that fans messages out to every connected peer, and a browser client that survives dropped connections without losing its place. The three tabs form one story — the server that accepts and relays, the resilient client that reconnects, and a small consumer that wires the client into UI callbacks.
In ChatServer.js, the server runs on the ws library and keeps every socket in a Set. The broadcast helper serializes once and iterates over wss.clients, skipping any socket whose readyState is not OPEN — a common pitfall, since a socket can linger in CLOSING while still enumerable. Heartbeats are the core reliability mechanism: each connection is marked isAlive, a pong handler flips it back to true, and a shared interval terminates any socket that missed the previous ping. Without this, half-open TCP connections (a client that vanished without a close frame) would leak forever. The server also assigns a short id per client so joins and leaves can be announced.
In ReconnectingSocket.js, the client wraps a native WebSocket behind a stable interface. The key idea is that consumers hold a reference to the wrapper, not the underlying socket, so a reconnect is invisible to them. On close it schedules connect again using exponential backoff with jitter — Math.min(maxDelay, base * 2 ** attempt) plus randomness — which spreads reconnect storms so a restarted server is not hammered by every client at once. Outgoing messages are buffered in queue while offline and flushed on open, giving at-least-once delivery of user input across a blip. A manual close sets stopped so the backoff loop does not fight an intentional shutdown.
In chat-ui.js, the wrapper is consumed like an event emitter: onMessage renders incoming frames and onStatus drives a connection indicator. Because the queue and backoff live in the wrapper, the UI code stays declarative and never touches reconnection logic. The trade-off is that buffered messages may arrive out of their original order relative to server broadcasts, so anything requiring strict ordering would need sequence numbers layered on top.
Related snips
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
# Subscribe to a specific room
room = Room.find(params[:room_id])
ActionCable for real-time WebSocket communication
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_type TEXT NOT NULL,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
Transactional outbox in Node (DB write + event)
import { Pool, PoolClient, Client, QueryResult, QueryResultRow } from 'pg';
const MAX_LIFETIME_MS = 30 * 60 * 1000;
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
Postgres connection pooling with pg + max lifetime
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
WebSocket for real-time communication
Share this code
Here's the card — post it anywhere.