Skip to main content

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

  1. Always handle disconnection - Implement proper cleanup and reconnection logic
  2. Use typed messages - Leverage TypeScript for type-safe message handling
  3. Implement heartbeat - Keep connections alive and detect dead connections
  4. Queue messages - Buffer messages when disconnected for later delivery
  5. Handle authentication - Refresh tokens before they expire
  6. Clean up subscriptions - Unsubscribe when components unmount