WebSocket Client
The @54vie/api package provides a robust WebSocket client for real-time bidirectional communication with built-in support for automatic reconnection, event handling, and message serialization.
Installation
npm install @54vie/api
# or
yarn add @54vie/api
# or
pnpm add @54vie/api
TypeScript Interfaces
interface WebSocketConfig {
url: string;
protocols?: string | string[];
reconnect?: ReconnectConfig;
heartbeat?: HeartbeatConfig;
auth?: WebSocketAuthConfig;
serializer?: MessageSerializer;
debug?: boolean;
}
interface ReconnectConfig {
enabled?: boolean;
maxAttempts?: number;
delay?: number;
maxDelay?: number;
backoffMultiplier?: number;
onReconnect?: (attempt: number) => void;
onReconnectFailed?: () => void;
}
interface HeartbeatConfig {
enabled?: boolean;
interval?: number;
timeout?: number;
message?: string | object;
}
interface WebSocketAuthConfig {
token?: string | (() => string | Promise<string>);
query?: Record<string, string>;
headers?: Record<string, string>;
}
interface MessageSerializer {
encode: (data: unknown) => string | ArrayBuffer;
decode: (data: string | ArrayBuffer) => unknown;
}
interface WebSocketMessage<T = unknown> {
type: string;
payload: T;
timestamp?: number;
id?: string;
}
interface WebSocketEventMap {
open: Event;
close: CloseEvent;
error: Event;
message: MessageEvent;
reconnecting: CustomEvent<{ attempt: number }>;
reconnected: CustomEvent<{ attempt: number }>;
reconnect_failed: CustomEvent;
}
type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
interface WebSocketClient {
// Connection
connect(): Promise<void>;
disconnect(code?: number, reason?: string): void;
reconnect(): Promise<void>;
// State
getState(): ConnectionState;
isConnected(): boolean;
// Messaging
send<T>(type: string, payload: T): void;
sendRaw(data: string | ArrayBuffer): void;
// Events
on<K extends keyof WebSocketEventMap>(
event: K,
handler: (event: WebSocketEventMap[K]) => void
): () => void;
off<K extends keyof WebSocketEventMap>(
event: K,
handler: (event: WebSocketEventMap[K]) => void
): void;
once<K extends keyof WebSocketEventMap>(
event: K,
handler: (event: WebSocketEventMap[K]) => void
): void;
// Message handlers
subscribe<T>(type: string, handler: (payload: T) => void): () => void;
unsubscribe(type: string, handler?: (payload: unknown) => void): void;
// Utilities
getSocket(): WebSocket | null;
getReconnectAttempts(): number;
}
interface UseWebSocketOptions {
url: string;
protocols?: string | string[];
reconnect?: boolean;
reconnectAttempts?: number;
reconnectInterval?: number;
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
onMessage?: (event: MessageEvent) => void;
shouldConnect?: boolean;
}
interface UseWebSocketReturn {
sendMessage: (message: string | object) => void;
lastMessage: MessageEvent | null;
readyState: number;
isConnected: boolean;
connect: () => void;
disconnect: () => void;
}
Creating a WebSocket Client
import { WebSocketClient } from '@54vie/api';
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
reconnect: {
enabled: true,
maxAttempts: 5,
delay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
},
heartbeat: {
enabled: true,
interval: 30000,
timeout: 5000,
},
});
// Connect to the server
await ws.connect();
Connection Management
Basic Connection
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
});
// Connect
await ws.connect();
// Check connection state
console.log(ws.getState()); // 'connected'
console.log(ws.isConnected()); // true
// Disconnect
ws.disconnect(1000, 'User initiated disconnect');
With Authentication
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
auth: {
// Token can be string or function
token: () => localStorage.getItem('accessToken') || '',
// Query parameters added to URL
query: {
clientId: 'app-123',
version: '1.0.0',
},
},
});
await ws.connect();
With Custom Protocols
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
protocols: ['v1.json', 'v2.json'],
});
await ws.connect();
Event Handling
Connection Events
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
});
// Listen for connection open
ws.on('open', (event) => {
console.log('Connected to WebSocket server');
});
// Listen for connection close
ws.on('close', (event) => {
console.log(`Connection closed: ${event.code} - ${event.reason}`);
});
// Listen for errors
ws.on('error', (event) => {
console.error('WebSocket error:', event);
});
// Listen for all messages
ws.on('message', (event) => {
console.log('Received:', event.data);
});
await ws.connect();
Reconnection Events
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
reconnect: {
enabled: true,
maxAttempts: 5,
},
});
// Listen for reconnection attempts
ws.on('reconnecting', (event) => {
console.log(`Reconnecting... Attempt ${event.detail.attempt}`);
});
// Listen for successful reconnection
ws.on('reconnected', (event) => {
console.log(`Reconnected after ${event.detail.attempt} attempts`);
});
// Listen for reconnection failure
ws.on('reconnect_failed', () => {
console.error('Failed to reconnect after max attempts');
});
await ws.connect();
Removing Event Listeners
// Using returned unsubscribe function
const unsubscribe = ws.on('message', (event) => {
console.log('Message:', event.data);
});
// Later, remove the listener
unsubscribe();
// Or using off method
const handler = (event: MessageEvent) => {
console.log('Message:', event.data);
};
ws.on('message', handler);
ws.off('message', handler);
// One-time listener
ws.once('open', () => {
console.log('Connected (only called once)');
});
Sending Messages
Typed Messages
interface ChatMessage {
roomId: string;
content: string;
senderId: string;
}
// Send a typed message
ws.send<ChatMessage>('chat:message', {
roomId: 'room-123',
content: 'Hello, world!',
senderId: 'user-456',
});
// Message is serialized as:
// { type: 'chat:message', payload: { roomId: '...', content: '...', senderId: '...' } }
Raw Messages
// Send raw string
ws.sendRaw('ping');
// Send binary data
const buffer = new ArrayBuffer(8);
ws.sendRaw(buffer);
Message Queue (Auto-Send on Connect)
import { WebSocketClient } from '@54vie/api';
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
});
// Messages are queued if sent before connection
ws.send('user:status', { status: 'online' });
// They will be sent automatically when connected
await ws.connect();
Subscribing to Message Types
Basic Subscription
interface NotificationPayload {
id: string;
title: string;
body: string;
createdAt: string;
}
// Subscribe to specific message types
const unsubscribe = ws.subscribe<NotificationPayload>('notification', (payload) => {
console.log(`New notification: ${payload.title}`);
showNotification(payload);
});
// Later, unsubscribe
unsubscribe();
Multiple Subscriptions
interface UserOnline {
userId: string;
username: string;
}
interface UserOffline {
userId: string;
}
interface ChatMessage {
roomId: string;
senderId: string;
content: string;
timestamp: number;
}
// Subscribe to multiple message types
ws.subscribe<UserOnline>('user:online', (payload) => {
addOnlineUser(payload);
});
ws.subscribe<UserOffline>('user:offline', (payload) => {
removeOnlineUser(payload.userId);
});
ws.subscribe<ChatMessage>('chat:message', (payload) => {
addMessageToRoom(payload.roomId, payload);
});
Wildcard Subscriptions
// Subscribe to all messages with a prefix
ws.subscribe('chat:*', (payload, type) => {
console.log(`Chat event: ${type}`, payload);
});
// Handles: chat:message, chat:typing, chat:read, etc.
Reconnection Strategies
Exponential Backoff
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
reconnect: {
enabled: true,
maxAttempts: 10,
delay: 1000, // Start with 1 second
maxDelay: 30000, // Max 30 seconds
backoffMultiplier: 2, // Double each attempt
onReconnect: (attempt) => {
console.log(`Reconnect attempt ${attempt}`);
showReconnectingUI(attempt);
},
onReconnectFailed: () => {
console.error('Connection lost');
showConnectionLostUI();
},
},
});
Custom Reconnection Logic
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
reconnect: {
enabled: true,
maxAttempts: Infinity, // Never give up
delay: 1000,
maxDelay: 60000,
backoffMultiplier: 1.5,
},
});
// Custom reconnection with token refresh
ws.on('reconnecting', async () => {
// Refresh token before reconnecting
const newToken = await refreshAuthToken();
ws.updateAuth({ token: newToken });
});
Manual Reconnection
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
reconnect: {
enabled: false, // Disable auto-reconnect
},
});
ws.on('close', (event) => {
if (event.code !== 1000) {
// Abnormal closure, attempt reconnect
setTimeout(() => {
ws.reconnect();
}, 5000);
}
});
Heartbeat / Keep-Alive
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
heartbeat: {
enabled: true,
interval: 30000, // Send ping every 30 seconds
timeout: 5000, // Wait 5 seconds for pong
message: { type: 'ping' }, // Custom ping message
},
});
// The client automatically handles heartbeat
// If pong is not received within timeout, connection is considered dead
Custom Serialization
JSON Serializer (Default)
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
serializer: {
encode: (data) => JSON.stringify(data),
decode: (data) => JSON.parse(data as string),
},
});
MessagePack Serializer
import { encode, decode } from '@msgpack/msgpack';
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
serializer: {
encode: (data) => encode(data),
decode: (data) => decode(data as ArrayBuffer),
},
});
Protocol Buffers
import { MyMessage } from './proto/messages';
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
serializer: {
encode: (data) => MyMessage.encode(data).finish(),
decode: (data) => MyMessage.decode(new Uint8Array(data as ArrayBuffer)),
},
});
React Hook: useWebSocket
Basic Usage
import { useWebSocket } from '@54vie/api';
function ChatRoom({ roomId }: { roomId: string }) {
const {
sendMessage,
lastMessage,
readyState,
isConnected,
} = useWebSocket({
url: `wss://api.54vie.com/ws?room=${roomId}`,
onMessage: (event) => {
const message = JSON.parse(event.data);
handleNewMessage(message);
},
});
const handleSend = (text: string) => {
sendMessage({
type: 'chat:message',
payload: { text, roomId },
});
};
return (
<div>
<ConnectionStatus connected={isConnected} />
<MessageList />
<MessageInput onSend={handleSend} disabled={!isConnected} />
</div>
);
}
With Reconnection
function LiveFeed() {
const [messages, setMessages] = useState<Message[]>([]);
const { isConnected, connect, disconnect } = useWebSocket({
url: 'wss://api.54vie.com/ws/feed',
reconnect: true,
reconnectAttempts: 5,
reconnectInterval: 3000,
onOpen: () => {
console.log('Connected to live feed');
},
onClose: () => {
console.log('Disconnected from live feed');
},
onMessage: (event) => {
const message = JSON.parse(event.data);
setMessages((prev) => [...prev, message]);
},
});
return (
<div>
<button onClick={isConnected ? disconnect : connect}>
{isConnected ? 'Disconnect' : 'Connect'}
</button>
<FeedList messages={messages} />
</div>
);
}
Conditional Connection
function UserPresence({ userId }: { userId: string | null }) {
const { isConnected } = useWebSocket({
url: `wss://api.54vie.com/ws/presence?userId=${userId}`,
// Only connect when userId is available
shouldConnect: !!userId,
onOpen: () => {
console.log('Presence connected');
},
});
return <PresenceIndicator connected={isConnected} />;
}
Room/Channel Management
import { WebSocketClient } from '@54vie/api';
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
});
// Join a room
function joinRoom(roomId: string) {
ws.send('room:join', { roomId });
}
// Leave a room
function leaveRoom(roomId: string) {
ws.send('room:leave', { roomId });
}
// Send message to room
function sendRoomMessage(roomId: string, content: string) {
ws.send('room:message', { roomId, content });
}
// Subscribe to room events
ws.subscribe('room:joined', ({ roomId, members }) => {
console.log(`Joined room ${roomId} with ${members.length} members`);
});
ws.subscribe('room:member_joined', ({ roomId, member }) => {
console.log(`${member.name} joined room ${roomId}`);
});
ws.subscribe('room:member_left', ({ roomId, memberId }) => {
console.log(`Member ${memberId} left room ${roomId}`);
});
Error Handling
const ws = new WebSocketClient({
url: 'wss://api.54vie.com/ws',
});
// Handle connection errors
ws.on('error', (event) => {
console.error('WebSocket error:', event);
// Show error UI
showErrorNotification('Connection error occurred');
});
// Handle close with error codes
ws.on('close', (event) => {
switch (event.code) {
case 1000:
console.log('Normal closure');
break;
case 1001:
console.log('Going away');
break;
case 1006:
console.error('Abnormal closure (no close frame)');
break;
case 1008:
console.error('Policy violation');
break;
case 1011:
console.error('Server error');
break;
case 4001:
console.error('Authentication failed');
handleAuthError();
break;
case 4002:
console.error('Rate limited');
handleRateLimit();
break;
default:
console.error(`Unexpected close: ${event.code}`);
}
});
// Handle message errors
ws.subscribe('error', (error) => {
console.error('Server error:', error);
showErrorNotification(error.message);
});
Best Practices
- Always handle disconnection - Implement proper cleanup and reconnection logic
- Use typed messages - Leverage TypeScript for type-safe message handling
- Implement heartbeat - Keep connections alive and detect dead connections
- Queue messages - Buffer messages when disconnected for later delivery
- Handle authentication - Refresh tokens before they expire
- Clean up subscriptions - Unsubscribe when components unmount
Related
- ApiClient - Core HTTP client
- React Hooks - useQuery, useMutation hooks
- Caching - Response caching and offline support