bookworm-smart-assistant/skills/websocket-engineer/references/patterns.md

401 lines
9.8 KiB
Markdown

# WebSocket Patterns Reference
## Rooms and Namespaces
### Rooms (Channel Grouping)
```javascript
const io = require('socket.io')(3000);
io.on('connection', (socket) => {
// Join a room
socket.on('join-room', (roomId) => {
socket.join(roomId);
socket.emit('joined', { room: roomId });
// Notify others in room
socket.to(roomId).emit('user-joined', {
userId: socket.id,
timestamp: Date.now()
});
});
// Leave a room
socket.on('leave-room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user-left', { userId: socket.id });
});
// Send to specific room
socket.on('message', ({ roomId, text }) => {
io.to(roomId).emit('message', {
userId: socket.id,
text,
timestamp: Date.now()
});
});
// Get all rooms a socket is in
console.log('Socket rooms:', socket.rooms); // Set { socketId, roomId1, roomId2 }
// Disconnect from all rooms
socket.on('disconnect', () => {
// Automatically leaves all rooms
});
});
// Broadcast to all sockets in a room
io.to('room123').emit('announcement', 'Hello room!');
// Broadcast to multiple rooms
io.to('room1').to('room2').emit('multi-room', 'data');
// Broadcast to room except specific socket
socket.to('room123').emit('message', 'Others see this');
// Get all sockets in a room
const sockets = await io.in('room123').fetchSockets();
console.log(`Room has ${sockets.length} connections`);
```
### Namespaces (Logical Separation)
```javascript
// Admin namespace
const adminNs = io.of('/admin');
adminNs.on('connection', (socket) => {
console.log('Admin connected:', socket.id);
socket.on('admin-action', (data) => {
// Admin-only events
});
});
// Chat namespace
const chatNs = io.of('/chat');
chatNs.on('connection', (socket) => {
console.log('Chat user connected:', socket.id);
socket.on('message', (msg) => {
chatNs.emit('message', msg); // Broadcast to all in /chat
});
});
// Dynamic namespaces
io.of(/^\/workspace-\d+$/).on('connection', (socket) => {
const namespace = socket.nsp;
console.log(`Connected to ${namespace.name}`);
socket.on('message', (data) => {
namespace.emit('message', data);
});
});
```
## Broadcasting Patterns
```javascript
// Broadcast to everyone including sender
io.emit('event', data);
// Broadcast to everyone except sender
socket.broadcast.emit('event', data);
// Broadcast to specific room
io.to('room1').emit('event', data);
// Broadcast to room except sender
socket.to('room1').emit('event', data);
// Broadcast to multiple rooms
io.to('room1').to('room2').emit('event', data);
// Broadcast to all connected clients in namespace
io.of('/namespace').emit('event', data);
// Volatile messages (ok to drop if client not ready)
socket.volatile.emit('high-frequency', data);
// Broadcast with acknowledgment (to all clients)
io.timeout(5000).emit('event', data, (err, responses) => {
if (err) {
console.log('Some clients did not acknowledge');
} else {
console.log('All clients acknowledged:', responses);
}
});
```
## Acknowledgments
```javascript
// Server expects acknowledgment
socket.emit('question', 'Do you agree?', (answer) => {
console.log('Client answered:', answer);
});
// Client sends acknowledgment
socket.on('question', (data, callback) => {
console.log('Server asked:', data);
callback('Yes, I agree');
});
// Timeout for acknowledgment
socket.timeout(5000).emit('request', data, (err, response) => {
if (err) {
console.log('Client did not acknowledge in time');
} else {
console.log('Got response:', response);
}
});
// Error handling in acknowledgment
socket.on('save-data', (data, callback) => {
try {
saveToDatabase(data);
callback({ success: true });
} catch (error) {
callback({ success: false, error: error.message });
}
});
```
## Presence System
```javascript
const redis = require('ioredis');
const redisClient = new redis();
class PresenceManager {
async userConnected(userId, socketId) {
const key = `user:${userId}:sockets`;
// Add socket to user's socket set
await redisClient.sadd(key, socketId);
// Set TTL to auto-cleanup stale connections
await redisClient.expire(key, 3600);
// Get total connections for user
const socketCount = await redisClient.scard(key);
// If first connection, mark user as online
if (socketCount === 1) {
await redisClient.hset('presence', userId, JSON.stringify({
status: 'online',
lastSeen: Date.now()
}));
// Notify friends
const friends = await this.getUserFriends(userId);
friends.forEach(friendId => {
io.to(`user:${friendId}`).emit('presence', {
userId,
status: 'online'
});
});
}
return socketCount;
}
async userDisconnected(userId, socketId) {
const key = `user:${userId}:sockets`;
// Remove socket from set
await redisClient.srem(key, socketId);
const socketCount = await redisClient.scard(key);
// If no more connections, mark offline
if (socketCount === 0) {
await redisClient.hset('presence', userId, JSON.stringify({
status: 'offline',
lastSeen: Date.now()
}));
const friends = await this.getUserFriends(userId);
friends.forEach(friendId => {
io.to(`user:${friendId}`).emit('presence', {
userId,
status: 'offline',
lastSeen: Date.now()
});
});
}
return socketCount;
}
async getUserStatus(userId) {
const data = await redisClient.hget('presence', userId);
return data ? JSON.parse(data) : { status: 'offline' };
}
async getBulkPresence(userIds) {
const pipeline = redisClient.pipeline();
userIds.forEach(id => pipeline.hget('presence', id));
const results = await pipeline.exec();
return userIds.map((id, i) => ({
userId: id,
...JSON.parse(results[i][1] || '{"status":"offline"}')
}));
}
}
// Usage
const presence = new PresenceManager();
io.on('connection', async (socket) => {
const userId = socket.handshake.auth.userId;
await presence.userConnected(userId, socket.id);
socket.join(`user:${userId}`);
socket.on('disconnect', async () => {
await presence.userDisconnected(userId, socket.id);
});
// Get presence for user's friends
socket.on('get-presence', async (friendIds, callback) => {
const presenceData = await presence.getBulkPresence(friendIds);
callback(presenceData);
});
});
```
## Message Queue Pattern
```javascript
// Queue messages when client disconnected
const messageQueue = new Map();
io.on('connection', (socket) => {
const userId = socket.handshake.auth.userId;
// Deliver queued messages on connect
const queuedMessages = messageQueue.get(userId) || [];
if (queuedMessages.length > 0) {
socket.emit('queued-messages', queuedMessages);
messageQueue.delete(userId);
}
socket.on('disconnect', () => {
// Mark user as disconnected
setTimeout(() => {
const userSockets = io.sockets.sockets;
const hasOtherConnection = Array.from(userSockets.values())
.some(s => s.handshake.auth.userId === userId);
if (!hasOtherConnection) {
// User fully disconnected, queue new messages
messageQueue.set(userId, []);
}
}, 1000);
});
});
// Queue message if user offline
async function sendMessage(userId, message) {
const userOnline = await isUserOnline(userId);
if (userOnline) {
io.to(`user:${userId}`).emit('message', message);
} else {
const queue = messageQueue.get(userId) || [];
queue.push(message);
messageQueue.set(userId, queue);
// Persist to database for longer-term storage
await saveMessageToDb(userId, message);
}
}
```
## Pub/Sub Pattern
```javascript
const EventEmitter = require('events');
class MessageBus extends EventEmitter {
constructor(io, redis) {
super();
this.io = io;
this.redis = redis;
this.setupSubscriptions();
}
setupSubscriptions() {
// Subscribe to Redis channels
this.redis.psubscribe('room:*', (err, count) => {
console.log(`Subscribed to ${count} channels`);
});
this.redis.on('pmessage', (pattern, channel, message) => {
const data = JSON.parse(message);
const roomId = channel.split(':')[1];
// Emit to Socket.IO room
this.io.to(roomId).emit(data.event, data.payload);
});
}
publish(roomId, event, payload) {
// Publish to Redis (distributed across servers)
this.redis.publish(
`room:${roomId}`,
JSON.stringify({ event, payload })
);
}
}
// Usage
const messageBus = new MessageBus(io, redisClient);
io.on('connection', (socket) => {
socket.on('send-message', ({ roomId, text }) => {
// Publish to all servers via Redis
messageBus.publish(roomId, 'message', {
userId: socket.id,
text,
timestamp: Date.now()
});
});
});
```
## Backpressure Handling
```javascript
io.on('connection', (socket) => {
const MAX_BUFFER_SIZE = 10000;
let bufferSize = 0;
const originalEmit = socket.emit.bind(socket);
socket.emit = function(event, ...args) {
bufferSize++;
if (bufferSize > MAX_BUFFER_SIZE) {
console.warn('Buffer overflow, dropping message');
return false;
}
const result = originalEmit(event, ...args);
// Track buffer drain
socket.once('drain', () => {
bufferSize = 0;
});
return result;
};
// Monitor buffer size
socket.on('drain', () => {
console.log('Socket buffer drained');
});
});
```