dist/plugins/web-realtime-websockets/skills/web-realtime-websockets/SKILL.md
Native WebSocket API patterns, connection lifecycle, reconnection strategies, heartbeat, message typing, binary data, custom hooks
npx skillsauth add agents-inc/skills web-realtime-websocketsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use native WebSocket API for real-time bidirectional communication. Implement exponential backoff with jitter for reconnection. Use discriminated unions for type-safe message handling. Queue messages during disconnection for delivery on reconnect. Close connections on
pagehideto allow bfcache.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST implement exponential backoff with jitter for ALL reconnection logic)
(You MUST use discriminated unions with a type field for ALL WebSocket message types)
(You MUST queue messages during disconnection and flush on reconnect)
(You MUST implement heartbeat/ping-pong to detect dead connections)
(You MUST set binaryType to 'arraybuffer' when handling binary data)
(You MUST use wss:// for secure origins - browsers block ws:// on HTTPS pages except localhost)
(You MUST handle bfcache with pagehide/pageshow events)
</critical_requirements>
Auto-detection: WebSocket, ws://, wss://, onmessage, onopen, onclose, onerror, reconnect, heartbeat, ping, pong, real-time, bidirectional
When to use:
When NOT to use:
Key patterns covered:
Detailed Resources:
WebSockets provide full-duplex communication channels over a single TCP connection, enabling real-time bidirectional data flow between client and server. Unlike HTTP, WebSocket connections remain open, eliminating the overhead of repeated handshakes.
The native WebSocket API is simple but requires careful handling:
Connection Resilience: Networks are unreliable. Always implement reconnection with exponential backoff and jitter to prevent thundering herd problems.
Connection Health: Intermediate proxies and firewalls can silently drop idle connections. Heartbeats detect dead connections and keep connections alive.
Message Integrity: Messages sent during disconnection are lost. Queue them and flush on reconnect for reliable delivery.
Type Safety: WebSocket messages are untyped strings. Use discriminated unions with a shared type field for compile-time safety.
bfcache Compatibility: Open WebSocket connections prevent pages from using the browser's back/forward cache, degrading navigation performance. Close connections on pagehide and reconnect on pageshow when event.persisted.
Connection Lifecycle:
CONNECTING -> OPEN <-> (messages) -> CLOSING -> CLOSED
| |
(error) <- reconnect <- (close)
</philosophy>
The native WebSocket API provides four lifecycle events: onopen, onmessage, onerror, and onclose. Always handle all four.
const WS_URL = "wss://api.example.com/ws";
const socket = new WebSocket(WS_URL);
socket.onopen = () => {
/* connection ready - safe to send */
};
socket.onmessage = (event: MessageEvent) => {
/* JSON.parse(event.data) */
};
socket.onerror = (event: Event) => {
/* always followed by onclose */
};
socket.onclose = (event: CloseEvent) => {
/* reconnect here */
};
Why good: All four lifecycle events handled, typed event parameters, named constant for URL
Full implementation: examples/core.md Pattern 1
Reconnection attempts must use exponential backoff with jitter to prevent all clients from reconnecting simultaneously (thundering herd problem). Cap delay at a maximum and limit total retry attempts.
const INITIAL_BACKOFF_MS = 1000;
const MAX_BACKOFF_MS = 30000;
const BACKOFF_MULTIPLIER = 2;
const JITTER_FACTOR = 0.5;
function calculateBackoff(attempt: number): number {
const exponential = Math.min(
INITIAL_BACKOFF_MS * Math.pow(BACKOFF_MULTIPLIER, attempt),
MAX_BACKOFF_MS,
);
const jitter = exponential * JITTER_FACTOR * (Math.random() * 2 - 1);
return Math.floor(exponential + jitter);
}
Why good: Jitter prevents thundering herd, capped maximum delay, retry limit prevents infinite loops
Full reconnecting class: examples/core.md Pattern 2
Heartbeats detect dead connections and prevent intermediate infrastructure from closing idle connections. Send a ping on an interval; if pong is not received within a timeout, consider the connection dead.
const HEARTBEAT_INTERVAL_MS = 30000;
const HEARTBEAT_TIMEOUT_MS = 10000;
// Send ping -> start timeout -> if pong received, clear timeout
// If timeout fires without pong -> connection is dead, close and reconnect
When to use: All WebSocket connections, especially those that may be idle for extended periods or pass through NATs/proxies.
Full implementation: examples/core.md Pattern 3
Messages sent during disconnection are lost. Queue them and flush when connection is restored. Limit queue size to prevent unbounded memory growth.
const MAX_QUEUE_SIZE = 100;
public send(data: unknown): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
this.queueMessage(data); // Queue with size limit
}
}
Why good: Queue has size limit, oldest messages dropped when full, flush on reconnect, readyState check before sending
Full implementation: examples/core.md Pattern 4
Use discriminated unions with a shared type field for compile-time type safety and exhaustive handling. Define separate types for client-to-server and server-to-client messages.
type ServerMessage =
| { type: "subscribed"; channel: string; members: string[] }
| { type: "message"; channel: string; content: string; sender: string }
| { type: "error"; code: number; message: string };
function handleServerMessage(message: ServerMessage): void {
switch (message.type) {
case "subscribed":
/* ... */ break;
case "message":
/* ... */ break;
case "error":
/* ... */ break;
default:
const exhaustiveCheck: never = message; // Compile error if case missing
}
}
Why good: Discriminated union enables type narrowing, exhaustiveness check catches missing cases at compile time, separate types for client/server messages
Full implementation: examples/core.md Pattern 5
WebSockets support binary data via ArrayBuffer or Blob. Set binaryType to 'arraybuffer' for synchronous processing with DataView. Use instanceof ArrayBuffer to distinguish binary from text messages.
socket.binaryType = "arraybuffer";
socket.onmessage = (event: MessageEvent) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data); // Synchronous
} else {
JSON.parse(event.data); // Text message
}
};
Why good: ArrayBuffer enables synchronous DataView access, instanceof check distinguishes binary from text
Full implementation with binary protocol: examples/core.md Pattern 6
WebSocket doesn't support custom HTTP headers. Authenticate via the first message after connection (not query string, which leaks tokens to logs). Queue application messages until auth is confirmed.
socket.onopen = () => {
socket.send(JSON.stringify({ type: "auth", token })); // First message
};
// Queue all other messages until auth_result.success received
Why good: Token not in URL (avoids server logs), messages queued until authenticated, explicit auth state
Full implementation: examples/core.md Pattern 7
Organize connections into logical channels for targeted message delivery. Track local room state (membership, joined status) and guard against sending to unjoined rooms.
Full implementation: examples/core.md Pattern 8
A comprehensive custom hook encapsulating connection lifecycle, reconnection with backoff, heartbeat, message queuing, and cleanup. Exposes status, send, close, and reconnect.
Full implementation: examples/core.md Pattern 9
When multiple components need the same WebSocket, use a context provider with type-based message routing via a subscribe(type, handler) API.
Full implementation: examples/core.md Pattern 10
Open WebSocket connections prevent pages from entering the browser's back/forward cache. Close on pagehide and reconnect on pageshow when event.persisted.
window.addEventListener("pagehide", () => {
socket?.close(1000, "Page hidden");
});
window.addEventListener("pageshow", (event: PageTransitionEvent) => {
if (event.persisted) {
// Page restored from bfcache - reconnect
connect();
}
});
</patterns>Full implementation: examples/core.md Pattern 11
<red_flags>
pagehide for cleanup, not beforeunload - beforeunload prevents bfcacheinstanceof ArrayBuffer check - Don't assume message typebufferedAmount before sending large data</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST implement exponential backoff with jitter for ALL reconnection logic)
(You MUST use discriminated unions with a type field for ALL WebSocket message types)
(You MUST queue messages during disconnection and flush on reconnect)
(You MUST implement heartbeat/ping-pong to detect dead connections)
(You MUST set binaryType to 'arraybuffer' when handling binary data)
(You MUST use wss:// for secure origins - browsers block ws:// on HTTPS pages except localhost)
(You MUST handle bfcache with pagehide/pageshow events)
Failure to follow these rules will result in connection storms, lost messages, blocked connections, and degraded navigation performance.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety