A floofy, feature-rich WebSocket library for Node.js that makes real-time communication as cozy as a den! Perfect for building snuggly client-server applications with peer-to-peer capabilities.
- π¦ Easy Server Setup: Create WebSocket servers with minimal configuration
- πΊ Smart Client Management: Built-in authentication, heartbeat monitoring, and reconnection
- π Peer-to-Peer Requests: Clients can directly request from other clients through the server
- π‘ Event Broadcasting: Real-time event system for all your pack communication needs
- π Handler System: Register custom request handlers on both server and clients
- π Auto-Reconnection: Clients automatically reconnect with exponential backoff
- π Heartbeat System: Keep connections alive and detect timeouts
- π‘οΈ Token Authentication: Secure your den with token-based auth
- π― Dual Event System:
server.event.on()for client events,server.on()for server lifecycle events - π¬ Enhanced Event Handlers: All event handlers receive
(data, fromClient)parameters for better tracking - π Connection State Management: Track and respond to connection state changes
- β‘ Structured Logging: Configurable log levels with contextual information
- π Graceful Shutdown: Properly close connections with notification
- π§ Full TypeScript Support: Complete type definitions for IDE assistance
- π¨ Enhanced Error Handling: Specific error types with detailed information
npm install doggyhole-wsFor browser usage with native HTML + CSS + JavaScript (no bundlers required), check out the complete examples in the examples/browser directory:
- Chat Application - Real-time chat with multiple users
- Dashboard - Live metrics and monitoring
- Simple Example - Basic API demonstration
The browser examples include a standalone doggyhole-browser.js client that provides the same API as the Node.js version using native browser WebSocket and EventTarget APIs. All event handlers in the browser client also receive (data, fromClient) parameters.
Quick Start:
# Start the test server
cd examples/browser
node test-server.js
# Open examples/browser/chat.html in your browserSee examples/browser/README.md for detailed instructions.
import { DoggyHoleServer } from 'doggyhole-ws';
const server = DoggyHoleServer.create({
port: 8080,
heartbeatInterval: 1000,
heartbeatTimeout: 3000
});
server.setUser('client1', 'secret-token-1');
server.setUser('client2', 'secret-token-2');
server.addHandler('greet', (data) => {
return `Hello ${data.name}!`;
});
server.on('clientConnected', (clientName) => {
console.log(`${clientName} joined the pack!`);
});
server.on('clientDisconnected', (clientName) => {
console.log(`${clientName} left the den...`);
});import { DoggyHoleClient } from 'doggyhole-ws';
// Option 1: Named authentication
const client = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'client1',
token: 'secret-token-1',
maxReconnectAttempts: 5,
requestTimeout: 10000
});
// Option 2: Token-only authentication
const client2 = DoggyHoleClient.create({
url: 'ws://localhost:8080',
token: 'secret-token-1', // Uses registered name for this token
maxReconnectAttempts: 5,
requestTimeout: 10000
});
await client.connect();
const response = await client.request('greet', { name: 'Fluffy' });
console.log(response);interface ServerOptions {
port: number;
heartbeatInterval?: number; // Default: 1000ms
heartbeatTimeout?: number; // Default: 3000ms
maxConnections?: number; // Default: 1000
logLevel?: LogLevel; // Default: 'info'
gracefulShutdownTimeout?: number; // Default: 5000ms
messageQueueSize?: number; // Default: 100
}Creates a new server instance.
const server = DoggyHoleServer.create({
port: 8080,
heartbeatInterval: 1000,
heartbeatTimeout: 3000
});Add an authenticated user to the server.
server.setUser('alice', 'alice-secret-token');
server.setUser('bob', 'bob-secret-token');Remove a user and disconnect them from the server.
server.removeUser('alice');
// Alice will be disconnected automaticallyAdd a request handler function.
server.addHandler('getUserProfile', async (data) => {
const user = await database.getUser(data.userId);
return {
name: user.name,
email: user.email,
status: 'online'
};
});
server.addHandler('calculate', (data) => {
return data.a + data.b;
});Remove a request handler.
server.removeHandler('getUserProfile');Get all connected clients with their WebSocket connections.
const clients = server.getConnectedClients();
console.log(`Connected clients: ${clients.size}`);
for (const [ws, clientData] of clients) {
console.log(`Client: ${clientData.name}, Last heartbeat: ${clientData.lastHeartbeat}`);
}Get array of connected client names.
const names = server.getConnectedClientNames();
console.log('Connected clients:', names);
// Output: ['alice', 'bob', 'charlie']Get the server logger instance.
const logger = server.getLogger();
logger.info('Custom log message');
logger.setLevel(LogLevel.DEBUG);Check if server is in shutdown process.
if (server.isServerShuttingDown()) {
console.log('Server is shutting down');
}Gracefully shutdown the server with client notification.
await server.gracefulShutdown('Maintenance required');Immediately close the server and all connections.
server.close();DoggyHole Server uses two different event systems:
-
server.event.on()- For client events: Use this for events sent by clients viaclient.event.send(). These events are broadcast between clients and can be handled server-side. -
server.on()- For server lifecycle events: Use this for server-specific events like connections, disconnections, and timeouts.
The server extends EventEmitter and emits these lifecycle events:
Emitted when a client connects.
server.on('clientConnected', (clientName: string) => {
console.log(`${clientName} joined the pack!`);
server.broadcastEvent('userJoined', { username: clientName });
});Emitted when a client disconnects.
server.on('clientDisconnected', (clientName: string) => {
console.log(`${clientName} left the den...`);
server.broadcastEvent('userLeft', { username: clientName });
});Emitted when a client times out.
server.on('clientTimeout', (clientName: string) => {
console.log(`${clientName} timed out`);
// Clean up user's game state, notify other players
});interface ClientOptions {
url: string;
name?: string; // Optional - uses token if not provided
token: string;
maxReconnectAttempts?: number; // Default: 5
heartbeatInterval?: number; // Default: 1000ms
requestTimeout?: number; // Default: 10000ms
logLevel?: LogLevel; // Default: 'info'
reconnectBackoffMultiplier?: number; // Default: 1.5
}Create a new client instance.
// Named authentication
const client = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'alice',
token: 'alice-secret-token',
maxReconnectAttempts: 10,
requestTimeout: 15000
});
// Token-only authentication (name derived from token)
const client2 = DoggyHoleClient.create({
url: 'ws://localhost:8080',
token: 'alice-secret-token', // Will use registered name for this token
maxReconnectAttempts: 10,
requestTimeout: 15000
});Update client name.
client.setName('alice-gaming');Update authentication token.
client.setToken('new-secure-token');Update server URL.
client.setUrl('ws://production-server:8080');Connect to the server.
try {
await client.connect();
console.log('Connected successfully!');
} catch (error) {
console.error('Connection failed:', error);
}Disconnect from the server.
client.disconnect();Check if client is connected.
if (client.isConnected()) {
console.log('Client is connected');
// Make requests or send events
}Get the current connection state.
const state = client.getConnectionState();
console.log('Connection state:', state); // 'connected', 'connecting', 'disconnected', etc.Listen for connection state changes.
client.onStateChange((newState, oldState) => {
console.log(`Connection state: ${oldState} -> ${newState}`);
});Get the client logger instance.
const logger = client.getLogger();
logger.debug('Debug message');Make a request to the server.
try {
const userProfile = await client.request('getUserProfile', { userId: 123 });
console.log('User profile:', userProfile);
const mathResult = await client.request('calculate', { a: 5, b: 3 });
console.log('5 + 3 =', mathResult);
} catch (error) {
console.error('Request failed:', error);
}Make a request to another client through the server.
try {
const result = await client.requestClient('bob', 'getGameState', { gameId: 456 });
console.log('Bob\'s game state:', result);
const fileData = await client.requestClient('fileServer', 'getFile', { filename: 'data.json' });
console.log('File data:', fileData);
} catch (error) {
console.error('Client request failed:', error);
}Add a handler for client-to-client requests.
client.addHandler('ping', (data) => {
return { pong: true, timestamp: Date.now() };
});
client.addHandler('getGameState', (data) => {
return {
gameId: data.gameId,
level: 5,
score: 1250,
players: ['alice', 'bob']
};
});
client.addHandler('processData', async (data) => {
const result = await heavyComputation(data);
return result;
});Remove a client request handler.
client.removeHandler('ping');Send an event to all clients through the server.
client.sendEvent('userMessage', {
message: 'Hello everyone!',
timestamp: Date.now()
});
client.sendEvent('gameMove', {
gameId: 123,
move: 'knight-e4',
player: 'alice'
});Emitted when successfully connected to server.
client.on('connected', () => {
console.log('Connected to server!');
// Initialize UI, start sending data, etc.
});Emitted when disconnected from server.
client.on('disconnected', (code: number, reason: string) => {
console.log(`Disconnected: ${code} - ${reason}`);
// Save state, show reconnection UI
});Emitted when an error occurs.
client.on('error', (error: DoggyHoleError) => {
console.error('Client error:', error.message);
console.error('Error code:', error.code);
// Handle connection errors, authentication failures
});Emitted when connection state changes.
client.on('stateChange', (newState: ConnectionState, oldState: ConnectionState) => {
console.log(`Connection state changed: ${oldState} -> ${newState}`);
// Update UI based on connection state
});Emitted when server notifies of shutdown.
client.on('serverShutdown', (reason: string, gracePeriod: number) => {
console.log(`Server shutting down: ${reason}`);
console.log(`Grace period: ${gracePeriod}ms`);
// Save work, notify user
});Access the client event manager via client.event.
on(eventName: string, handler: (data: any, fromClient: string) => void): DoggyHoleClientEventManager
Listen for events from other clients. All handlers receive (data, fromClient) parameters:
client.event.on('userMessage', (data, fromClient) => {
console.log(`${fromClient}: ${data.message}`);
updateChatUI(data, fromClient);
});
client.event.on('gameMove', (data, fromClient) => {
console.log(`${fromClient} made move: ${data.move}`);
updateGameBoard(data, fromClient);
});once(eventName: string, handler: (data: any, fromClient: string) => void): DoggyHoleClientEventManager
Listen for an event only once. Handlers receive (data, fromClient) parameters:
client.event.once('gameStart', (data, fromClient) => {
console.log(`Game started by ${fromClient}!`);
initializeGame(data, fromClient);
});Remove event listener(s).
const messageHandler = (data, fromClient) => console.log(`${fromClient}: ${data.message}`);
client.event.on('message', messageHandler);
// Remove specific handler
client.event.off('message', messageHandler);
// Remove all handlers for event
client.event.off('message');Alias for on().
client.event.addListener('userJoined', (data, fromClient) => {
console.log(`${data.username} joined from ${fromClient}!`);
});Remove specific event listener.
client.event.removeListener('userJoined', joinHandler);Remove all listeners for event or all events.
// Remove all listeners for specific event
client.event.removeAllListeners('userMessage');
// Remove all listeners for all events
client.event.removeAllListeners();Add listener to beginning of listeners array.
client.event.prependListener('userMessage', (data) => {
console.log('This handler runs first!');
});prependOnceListener(eventName: string, handler: (...args: any[]) => void): DoggyHoleClientEventManager
Add one-time listener to beginning of listeners array.
client.event.prependOnceListener('gameStart', (data) => {
console.log('Game starting - this runs first and only once!');
});Send an event to all clients.
client.event.send('userMessage', {
message: 'Hello everyone!',
timestamp: Date.now()
});Alias for send().
client.event.broadcast('statusUpdate', {
status: 'online',
activity: 'coding'
});Check if there are listeners for an event.
if (client.event.hasListeners('userMessage')) {
console.log('Message handlers are registered');
}Get number of listeners for an event.
const count = client.event.getListenerCount('userMessage');
console.log(`${count} handlers for userMessage`);Get all event names with listeners.
const events = client.event.getEventNames();
console.log('Events with listeners:', events);Set maximum listeners per event.
client.event.setMaxListeners(50);Get maximum listeners per event.
const max = client.event.getMaxListeners();
console.log(`Max listeners: ${max}`);Remove all event listeners.
client.event.clearAll();Remove all listeners for specific event.
client.event.clearEvent('userMessage');Listen for internal event manager events.
client.event.onInternal('listenerAdded', (eventName, handler) => {
console.log(`Listener added for ${eventName}`);
});
client.event.onInternal('handlerError', (eventName, error, handler) => {
console.error(`Error in ${eventName} handler:`, error);
});Remove internal event listener.
client.event.offInternal('listenerAdded', addedHandler);Listen for internal event once.
client.event.onceInternal('eventReceived', (eventName, data) => {
console.log(`First event received: ${eventName}`);
});Access the server event manager via server.event to handle client events. Use this for events sent by clients via client.event.send().
Handle events sent by clients using server.event.on(). All handlers receive (data, fromClient) parameters:
// Listen for chat messages from any client
server.event.on('chatMessage', (data, fromClient) => {
console.log(`[${fromClient}]: ${data.message}`);
// Process message, store in database, moderate content, etc.
});
// Listen for game moves
server.event.on('gameMove', (data, fromClient) => {
console.log(`Player ${fromClient} moved ${data.move}`);
// Validate move, update game state
});
// Listen for user status updates
server.event.on('statusChange', (data, fromClient) => {
console.log(`${fromClient} is now ${data.status}`);
// Update user database, notify other systems
});The server event manager has the same API as client event manager but with server-specific functionality.
Handle incoming events from clients (automatically called by server).
// This is called automatically when clients send events
// You can override it if needed
server.event.handleIncomingEvent = function(fromClient, eventName, data) {
console.log(`Event ${eventName} from ${fromClient}`);
// Custom processing
this.emit(eventName, {...data, fromClient});
this.broadcastToOthers(eventName, data, fromClient);
};Broadcast event to all connected clients.
server.event.broadcastToAll('serverAnnouncement', {
message: 'Scheduled maintenance complete',
timestamp: Date.now()
}, 'server');// Server
const server = DoggyHoleServer.create({ port: 8080 });
server.setUser('alice', 'alice-token');
server.setUser('bob', 'bob-token');
// Handle chat messages on server-side
server.event.on('chatMessage', (data, fromClient) => {
console.log(`[${fromClient}]: ${data.message}`);
// Log to database, moderate content, etc.
});
// Handle user status changes
server.event.on('statusChange', (data, fromClient) => {
console.log(`${fromClient} is now ${data.status}`);
// Update user database, notify other systems
});
// Client
const client = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'alice',
token: 'alice-token'
});
await client.connect();
// Send chat message
client.event.send('chatMessage', {
message: 'Hello everyone!',
timestamp: Date.now()
});
// Listen for chat messages
client.event.on('chatMessage', (data, fromClient) => {
if (fromClient !== 'alice') {
console.log(`${fromClient}: ${data.message}`);
}
});
// Update status
client.event.send('statusChange', { status: 'online' });// Game server
const gameServer = DoggyHoleServer.create({ port: 8080 });
gameServer.setUser('player1', 'p1-token');
gameServer.setUser('player2', 'p2-token');
// Handle game events on server
gameServer.event.on('gameMove', (data, fromClient) => {
console.log(`Player ${fromClient} moved ${data.move}`);
// Validate move, update game state
});
gameServer.event.on('gameStart', (data, fromClient) => {
console.log(`Game started by ${fromClient}!`);
// Initialize game state
});
// Player 1 client
const player1 = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'player1',
token: 'p1-token'
});
// Add handler for game state requests
player1.addHandler('getGameState', (data) => {
return {
position: { x: 10, y: 20 },
health: 100,
inventory: ['sword', 'potion']
};
});
await player1.connect();
// Player 2 client
const player2 = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'player2',
token: 'p2-token'
});
await player2.connect();
// Player 2 requests Player 1's game state
const player1State = await player2.requestClient('player1', 'getGameState', {});
console.log('Player 1 state:', player1State);
// Send game move
player1.event.send('gameMove', {
move: 'forward',
timestamp: Date.now()
});
// Listen for moves
player2.event.on('gameMove', (data, fromClient) => {
if (fromClient === 'player1') {
console.log(`Player 1 moved: ${data.move}`);
// Update game display
}
});// API Gateway Server
const gateway = DoggyHoleServer.create({ port: 8080 });
gateway.setUser('auth-service', 'auth-token');
gateway.setUser('user-service', 'user-token');
gateway.setUser('order-service', 'order-token');
// Handle service events
gateway.event.on('userRegistered', (data, fromClient) => {
console.log(`New user registered: ${data.userId} from ${fromClient}`);
// Notify other services, update metrics
});
gateway.event.on('orderCreated', (data, fromClient) => {
console.log(`Order created: ${data.orderId} from ${fromClient}`);
// Process order, send notifications
});
// Auth Service Client
const authService = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'auth-service',
token: 'auth-token'
});
authService.addHandler('validateToken', (data) => {
// Validate JWT token
return { valid: true, userId: 123 };
});
await authService.connect();
// User Service Client
const userService = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'user-service',
token: 'user-token'
});
userService.addHandler('getUserProfile', (data) => {
return {
userId: data.userId,
name: 'John Doe',
email: 'john@example.com'
};
});
await userService.connect();
// Order Service Client
const orderService = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'order-service',
token: 'order-token'
});
await orderService.connect();
// Order service validates token with auth service
const tokenValidation = await orderService.requestClient('auth-service', 'validateToken', {
token: 'jwt-token-here'
});
if (tokenValidation.valid) {
// Get user profile from user service
const userProfile = await orderService.requestClient('user-service', 'getUserProfile', {
userId: tokenValidation.userId
});
// Create order and notify
orderService.event.send('orderCreated', {
orderId: 'ORDER-123',
userId: userProfile.userId,
items: ['item1', 'item2']
});
}// Monitoring Server
const monitor = DoggyHoleServer.create({ port: 8080 });
monitor.setUser('web-dashboard', 'dashboard-token');
monitor.setUser('mobile-app', 'mobile-token');
monitor.setUser('system-monitor', 'system-token');
// Track system metrics
monitor.event.on('cpuUsage', (data, fromClient) => {
console.log(`CPU: ${data.percentage}% from ${fromClient}`);
if (data.percentage > 90) {
// Send alert
monitor.event.broadcast('alert', {
type: 'high-cpu',
message: 'High CPU usage detected',
value: data.percentage
});
}
});
monitor.event.on('memoryUsage', (data, fromClient) => {
console.log(`Memory: ${data.used}MB/${data.total}MB from ${fromClient}`);
});
// System Monitor Client
const systemMonitor = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'system-monitor',
token: 'system-token'
});
await systemMonitor.connect();
// Send metrics periodically
setInterval(() => {
systemMonitor.event.send('cpuUsage', {
percentage: Math.random() * 100,
timestamp: Date.now()
});
systemMonitor.event.send('memoryUsage', {
used: Math.random() * 8000,
total: 8000,
timestamp: Date.now()
});
}, 5000);
// Dashboard Client
const dashboard = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'web-dashboard',
token: 'dashboard-token'
});
await dashboard.connect();
// Listen for metrics
dashboard.event.on('cpuUsage', (data, fromClient) => {
updateCpuChart(data, fromClient);
});
dashboard.event.on('memoryUsage', (data, fromClient) => {
updateMemoryChart(data, fromClient);
});
dashboard.event.on('alert', (data, fromClient) => {
showAlert(data, fromClient);
});DoggyHole provides specific error types for better error handling:
import {
DoggyHoleError,
AuthenticationError,
ConnectionError,
TimeoutError,
HandlerNotFoundError,
ClientNotFoundError,
NetworkError
} from 'doggyhole-ws';
// All errors extend DoggyHoleError with additional context
try {
await client.request('nonexistent', {});
} catch (error) {
if (error instanceof HandlerNotFoundError) {
console.log('Handler not found:', error.message);
console.log('Function name:', error.details?.functionName);
} else if (error instanceof TimeoutError) {
console.log('Request timed out after:', error.details?.timeout, 'ms');
}
}Full generic type support for requests and events:
interface UserData {
id: number;
name: string;
}
interface UserResponse {
user: UserData;
status: string;
}
// Typed request handlers
server.addHandler<{ userId: number }, UserResponse>('getUser', async (data) => {
// data is typed as { userId: number }
return {
user: { id: data.userId, name: 'John' },
status: 'active'
}; // Return type is validated as UserResponse
});
// Typed client requests
const response = await client.request<{ userId: number }, UserResponse>('getUser', { userId: 123 });
// response is typed as UserResponse
// Typed events
client.event.on('userUpdate', (data: UserData, fromClient: string) => {
// data is typed as UserData, fromClient is string
console.log('User updated:', data.name, 'from', fromClient);
});
client.event.send<UserData>('userUpdate', { id: 1, name: 'Jane' });import { ConnectionState } from 'doggyhole-ws';
client.onStateChange((newState, oldState) => {
switch (newState) {
case ConnectionState.CONNECTING:
showConnectingSpinner();
break;
case ConnectionState.CONNECTED:
hideSpinner();
enableFeatures();
break;
case ConnectionState.RECONNECTING:
showReconnectingMessage();
break;
case ConnectionState.DISCONNECTED:
disableFeatures();
break;
}
});// Server error handling
server.on('error', (error) => {
console.error('Server error:', error);
});
// Client error handling
client.on('error', (error) => {
console.error('Client error:', error);
// Handle different error types
if (error.message.includes('authentication')) {
// Handle auth errors
refreshToken();
}
});
client.on('disconnected', (code, reason) => {
console.log(`Disconnected: ${code} - ${reason}`);
// Handle reconnection based on reason
if (code === 1008) {
// Authentication error
updateCredentials();
}
});
// Request error handling
try {
const result = await client.request('someFunction', data);
} catch (error) {
if (error.message === 'Handler not found') {
console.log('Function not available on server');
} else if (error.message === 'Request timeout') {
console.log('Request took too long');
}
}
// Event handler error handling
client.event.onInternal('handlerError', (eventName, error, handler) => {
console.error(`Error in ${eventName} handler:`, error);
// Log error, notify monitoring system
});DoggyHole uses a hub-and-spoke model where:
- Server acts as the central hub managing all connections
- Clients connect to the server with token authentication
- Direct client requests are routed through the server
- Events can be broadcast to all clients or processed server-side
- Heartbeat system maintains connection health
- EventEmitter-like API for both client and server event handling
// Server optimized for high throughput
const server = DoggyHoleServer.create({
port: 8080,
heartbeatInterval: 30000, // 30 seconds
heartbeatTimeout: 60000, // 1 minute
maxConnections: 10000, // High connection limit
logLevel: LogLevel.WARN // Reduce logging overhead
});
// Client optimized for reliability
const client = DoggyHoleClient.create({
url: 'ws://localhost:8080',
name: 'client',
token: 'token',
maxReconnectAttempts: 10,
requestTimeout: 30000,
heartbeatInterval: 30000,
reconnectBackoffMultiplier: 1.2, // Faster reconnection
logLevel: LogLevel.ERROR // Minimal logging
});
// Batch events for better performance
const events = ['event1', 'event2', 'event3'];
events.forEach(eventName => {
client.sendEvent(eventName, { timestamp: Date.now() });
});We welcome contributions to make DoggyHole even more pawsome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request
MIT License - Feel free to use in your projects!
Having trouble? Create an issue on GitHub or join our community chat!
Made with π by the DoggyHole pack - Building cozy connections, one WebSocket at a time!