omgkit 2.0.7 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: real-time-systems
|
|
3
|
+
description: WebSocket, Server-Sent Events, and real-time communication patterns for live features
|
|
4
|
+
category: backend
|
|
5
|
+
triggers:
|
|
6
|
+
- real-time
|
|
7
|
+
- websocket
|
|
8
|
+
- socket.io
|
|
9
|
+
- server-sent events
|
|
10
|
+
- sse
|
|
11
|
+
- live updates
|
|
12
|
+
- presence
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Real-Time Systems
|
|
16
|
+
|
|
17
|
+
Build **real-time communication systems** with WebSocket, SSE, and pub/sub patterns. This skill covers connection management, scaling, and production deployment.
|
|
18
|
+
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Implement live features that users expect:
|
|
22
|
+
|
|
23
|
+
- Real-time messaging and chat
|
|
24
|
+
- Live notifications and updates
|
|
25
|
+
- Collaborative editing
|
|
26
|
+
- Presence detection
|
|
27
|
+
- Live dashboards and metrics
|
|
28
|
+
- Gaming and interactive experiences
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. WebSocket Server with Socket.io
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { Server } from 'socket.io';
|
|
36
|
+
import { createAdapter } from '@socket.io/redis-adapter';
|
|
37
|
+
import { createClient } from 'redis';
|
|
38
|
+
|
|
39
|
+
// Initialize Socket.io with Redis adapter for scaling
|
|
40
|
+
async function createSocketServer(httpServer: http.Server) {
|
|
41
|
+
const io = new Server(httpServer, {
|
|
42
|
+
cors: {
|
|
43
|
+
origin: process.env.CLIENT_URL,
|
|
44
|
+
credentials: true,
|
|
45
|
+
},
|
|
46
|
+
pingTimeout: 60000,
|
|
47
|
+
pingInterval: 25000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Redis adapter for multi-server deployment
|
|
51
|
+
const pubClient = createClient({ url: process.env.REDIS_URL });
|
|
52
|
+
const subClient = pubClient.duplicate();
|
|
53
|
+
await Promise.all([pubClient.connect(), subClient.connect()]);
|
|
54
|
+
io.adapter(createAdapter(pubClient, subClient));
|
|
55
|
+
|
|
56
|
+
// Authentication middleware
|
|
57
|
+
io.use(async (socket, next) => {
|
|
58
|
+
const token = socket.handshake.auth.token;
|
|
59
|
+
|
|
60
|
+
if (!token) {
|
|
61
|
+
return next(new Error('Authentication required'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const user = await verifyToken(token);
|
|
66
|
+
socket.data.user = user;
|
|
67
|
+
next();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
next(new Error('Invalid token'));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Connection handling
|
|
74
|
+
io.on('connection', (socket) => {
|
|
75
|
+
const userId = socket.data.user.id;
|
|
76
|
+
console.log(`User connected: ${userId}`);
|
|
77
|
+
|
|
78
|
+
// Join user's personal room
|
|
79
|
+
socket.join(`user:${userId}`);
|
|
80
|
+
|
|
81
|
+
// Handle joining rooms
|
|
82
|
+
socket.on('join:room', async (roomId: string) => {
|
|
83
|
+
// Verify access
|
|
84
|
+
const hasAccess = await checkRoomAccess(userId, roomId);
|
|
85
|
+
if (!hasAccess) {
|
|
86
|
+
socket.emit('error', { message: 'Access denied' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
socket.join(`room:${roomId}`);
|
|
91
|
+
socket.to(`room:${roomId}`).emit('user:joined', {
|
|
92
|
+
userId,
|
|
93
|
+
username: socket.data.user.name,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Handle messages
|
|
98
|
+
socket.on('message:send', async (data: { roomId: string; content: string }) => {
|
|
99
|
+
const message = await saveMessage({
|
|
100
|
+
roomId: data.roomId,
|
|
101
|
+
userId,
|
|
102
|
+
content: data.content,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
io.to(`room:${data.roomId}`).emit('message:new', message);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Typing indicators
|
|
109
|
+
socket.on('typing:start', (roomId: string) => {
|
|
110
|
+
socket.to(`room:${roomId}`).emit('typing:user', {
|
|
111
|
+
userId,
|
|
112
|
+
username: socket.data.user.name,
|
|
113
|
+
typing: true,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
socket.on('typing:stop', (roomId: string) => {
|
|
118
|
+
socket.to(`room:${roomId}`).emit('typing:user', {
|
|
119
|
+
userId,
|
|
120
|
+
typing: false,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Presence
|
|
125
|
+
socket.on('presence:update', async (status: 'online' | 'away' | 'busy') => {
|
|
126
|
+
await updatePresence(userId, status);
|
|
127
|
+
io.emit('presence:changed', { userId, status });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Disconnect handling
|
|
131
|
+
socket.on('disconnect', async (reason) => {
|
|
132
|
+
console.log(`User disconnected: ${userId}, reason: ${reason}`);
|
|
133
|
+
await updatePresence(userId, 'offline');
|
|
134
|
+
io.emit('presence:changed', { userId, status: 'offline' });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return io;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2. Server-Sent Events (SSE)
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { Router } from 'express';
|
|
146
|
+
|
|
147
|
+
const router = Router();
|
|
148
|
+
|
|
149
|
+
// SSE endpoint for notifications
|
|
150
|
+
router.get('/events/notifications', authenticate, (req, res) => {
|
|
151
|
+
const userId = req.user.id;
|
|
152
|
+
|
|
153
|
+
// Set SSE headers
|
|
154
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
155
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
156
|
+
res.setHeader('Connection', 'keep-alive');
|
|
157
|
+
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
158
|
+
|
|
159
|
+
// Send initial connection event
|
|
160
|
+
res.write(`event: connected\ndata: ${JSON.stringify({ userId })}\n\n`);
|
|
161
|
+
|
|
162
|
+
// Keep-alive ping
|
|
163
|
+
const pingInterval = setInterval(() => {
|
|
164
|
+
res.write(`: ping\n\n`);
|
|
165
|
+
}, 30000);
|
|
166
|
+
|
|
167
|
+
// Subscribe to user's notifications
|
|
168
|
+
const subscription = pubsub.subscribe(`notifications:${userId}`, (message) => {
|
|
169
|
+
res.write(`event: notification\ndata: ${JSON.stringify(message)}\n\n`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Cleanup on disconnect
|
|
173
|
+
req.on('close', () => {
|
|
174
|
+
clearInterval(pingInterval);
|
|
175
|
+
subscription.unsubscribe();
|
|
176
|
+
console.log(`SSE connection closed for user ${userId}`);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// SSE for live updates (e.g., stock prices, metrics)
|
|
181
|
+
router.get('/events/stream/:channel', authenticate, async (req, res) => {
|
|
182
|
+
const { channel } = req.params;
|
|
183
|
+
|
|
184
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
185
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
186
|
+
res.setHeader('Connection', 'keep-alive');
|
|
187
|
+
|
|
188
|
+
// Send initial data
|
|
189
|
+
const initialData = await getChannelData(channel);
|
|
190
|
+
res.write(`event: initial\ndata: ${JSON.stringify(initialData)}\n\n`);
|
|
191
|
+
|
|
192
|
+
// Stream updates
|
|
193
|
+
const unsubscribe = subscribeToChannel(channel, (update) => {
|
|
194
|
+
res.write(`event: update\ndata: ${JSON.stringify(update)}\n\n`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Handle retry on reconnection
|
|
198
|
+
res.write(`retry: 3000\n\n`);
|
|
199
|
+
|
|
200
|
+
req.on('close', () => {
|
|
201
|
+
unsubscribe();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Client-side SSE handling
|
|
206
|
+
const EventSourceComponent = () => {
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const eventSource = new EventSource('/api/events/notifications', {
|
|
209
|
+
withCredentials: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
eventSource.onopen = () => {
|
|
213
|
+
console.log('SSE connected');
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
eventSource.addEventListener('notification', (event) => {
|
|
217
|
+
const notification = JSON.parse(event.data);
|
|
218
|
+
showNotification(notification);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
eventSource.onerror = (error) => {
|
|
222
|
+
console.error('SSE error:', error);
|
|
223
|
+
// EventSource auto-reconnects
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return () => {
|
|
227
|
+
eventSource.close();
|
|
228
|
+
};
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
return null;
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 3. Pub/Sub with Redis
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { createClient } from 'redis';
|
|
239
|
+
|
|
240
|
+
class PubSubService {
|
|
241
|
+
private publisher: ReturnType<typeof createClient>;
|
|
242
|
+
private subscriber: ReturnType<typeof createClient>;
|
|
243
|
+
private handlers: Map<string, Set<(message: any) => void>> = new Map();
|
|
244
|
+
|
|
245
|
+
async connect() {
|
|
246
|
+
this.publisher = createClient({ url: process.env.REDIS_URL });
|
|
247
|
+
this.subscriber = this.publisher.duplicate();
|
|
248
|
+
|
|
249
|
+
await Promise.all([
|
|
250
|
+
this.publisher.connect(),
|
|
251
|
+
this.subscriber.connect(),
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
// Handle incoming messages
|
|
255
|
+
this.subscriber.on('message', (channel, message) => {
|
|
256
|
+
const handlers = this.handlers.get(channel);
|
|
257
|
+
if (handlers) {
|
|
258
|
+
const parsed = JSON.parse(message);
|
|
259
|
+
handlers.forEach(handler => handler(parsed));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async publish(channel: string, message: any): Promise<void> {
|
|
265
|
+
await this.publisher.publish(channel, JSON.stringify(message));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
subscribe(channel: string, handler: (message: any) => void): () => void {
|
|
269
|
+
if (!this.handlers.has(channel)) {
|
|
270
|
+
this.handlers.set(channel, new Set());
|
|
271
|
+
this.subscriber.subscribe(channel);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.handlers.get(channel)!.add(handler);
|
|
275
|
+
|
|
276
|
+
// Return unsubscribe function
|
|
277
|
+
return () => {
|
|
278
|
+
const handlers = this.handlers.get(channel);
|
|
279
|
+
if (handlers) {
|
|
280
|
+
handlers.delete(handler);
|
|
281
|
+
if (handlers.size === 0) {
|
|
282
|
+
this.handlers.delete(channel);
|
|
283
|
+
this.subscriber.unsubscribe(channel);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Pattern subscription
|
|
290
|
+
async psubscribe(pattern: string, handler: (channel: string, message: any) => void): Promise<() => void> {
|
|
291
|
+
await this.subscriber.pSubscribe(pattern, (message, channel) => {
|
|
292
|
+
handler(channel, JSON.parse(message));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return () => {
|
|
296
|
+
this.subscriber.pUnsubscribe(pattern);
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const pubsub = new PubSubService();
|
|
302
|
+
|
|
303
|
+
// Usage in services
|
|
304
|
+
class NotificationService {
|
|
305
|
+
async sendNotification(userId: string, notification: Notification): Promise<void> {
|
|
306
|
+
// Save to database
|
|
307
|
+
await db.notification.create({ data: { ...notification, userId } });
|
|
308
|
+
|
|
309
|
+
// Publish to real-time channel
|
|
310
|
+
await pubsub.publish(`notifications:${userId}`, notification);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async broadcastToRoom(roomId: string, event: string, data: any): Promise<void> {
|
|
314
|
+
await pubsub.publish(`room:${roomId}`, { event, data });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 4. Presence System
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
interface PresenceData {
|
|
323
|
+
status: 'online' | 'away' | 'busy' | 'offline';
|
|
324
|
+
lastSeen: Date;
|
|
325
|
+
socketIds: string[];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
class PresenceService {
|
|
329
|
+
private redis: ReturnType<typeof createClient>;
|
|
330
|
+
private readonly PRESENCE_TTL = 300; // 5 minutes
|
|
331
|
+
|
|
332
|
+
async setPresence(userId: string, socketId: string, status: string): Promise<void> {
|
|
333
|
+
const key = `presence:${userId}`;
|
|
334
|
+
|
|
335
|
+
// Use MULTI for atomic operations
|
|
336
|
+
await this.redis.multi()
|
|
337
|
+
.hSet(key, {
|
|
338
|
+
status,
|
|
339
|
+
lastSeen: Date.now().toString(),
|
|
340
|
+
})
|
|
341
|
+
.sAdd(`${key}:sockets`, socketId)
|
|
342
|
+
.expire(key, this.PRESENCE_TTL)
|
|
343
|
+
.exec();
|
|
344
|
+
|
|
345
|
+
// Publish presence change
|
|
346
|
+
await pubsub.publish('presence:updates', {
|
|
347
|
+
userId,
|
|
348
|
+
status,
|
|
349
|
+
lastSeen: new Date(),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async removeSocket(userId: string, socketId: string): Promise<void> {
|
|
354
|
+
const key = `presence:${userId}`;
|
|
355
|
+
|
|
356
|
+
await this.redis.sRem(`${key}:sockets`, socketId);
|
|
357
|
+
const remaining = await this.redis.sCard(`${key}:sockets`);
|
|
358
|
+
|
|
359
|
+
if (remaining === 0) {
|
|
360
|
+
await this.redis.hSet(key, 'status', 'offline');
|
|
361
|
+
await pubsub.publish('presence:updates', {
|
|
362
|
+
userId,
|
|
363
|
+
status: 'offline',
|
|
364
|
+
lastSeen: new Date(),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async getPresence(userId: string): Promise<PresenceData | null> {
|
|
370
|
+
const key = `presence:${userId}`;
|
|
371
|
+
const data = await this.redis.hGetAll(key);
|
|
372
|
+
|
|
373
|
+
if (!data.status) return null;
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
status: data.status as PresenceData['status'],
|
|
377
|
+
lastSeen: new Date(parseInt(data.lastSeen)),
|
|
378
|
+
socketIds: await this.redis.sMembers(`${key}:sockets`),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async getMultiplePresence(userIds: string[]): Promise<Map<string, PresenceData>> {
|
|
383
|
+
const pipeline = this.redis.multi();
|
|
384
|
+
|
|
385
|
+
userIds.forEach(id => {
|
|
386
|
+
pipeline.hGetAll(`presence:${id}`);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const results = await pipeline.exec();
|
|
390
|
+
const presenceMap = new Map<string, PresenceData>();
|
|
391
|
+
|
|
392
|
+
userIds.forEach((id, index) => {
|
|
393
|
+
const data = results[index] as Record<string, string>;
|
|
394
|
+
if (data?.status) {
|
|
395
|
+
presenceMap.set(id, {
|
|
396
|
+
status: data.status as PresenceData['status'],
|
|
397
|
+
lastSeen: new Date(parseInt(data.lastSeen)),
|
|
398
|
+
socketIds: [],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return presenceMap;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 5. Connection Recovery
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Client-side reconnection logic
|
|
412
|
+
class ReconnectingWebSocket {
|
|
413
|
+
private ws: WebSocket | null = null;
|
|
414
|
+
private reconnectAttempts = 0;
|
|
415
|
+
private maxReconnectAttempts = 10;
|
|
416
|
+
private reconnectInterval = 1000;
|
|
417
|
+
private messageQueue: any[] = [];
|
|
418
|
+
|
|
419
|
+
constructor(
|
|
420
|
+
private url: string,
|
|
421
|
+
private options: {
|
|
422
|
+
onMessage: (data: any) => void;
|
|
423
|
+
onConnect: () => void;
|
|
424
|
+
onDisconnect: () => void;
|
|
425
|
+
}
|
|
426
|
+
) {
|
|
427
|
+
this.connect();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private connect(): void {
|
|
431
|
+
this.ws = new WebSocket(this.url);
|
|
432
|
+
|
|
433
|
+
this.ws.onopen = () => {
|
|
434
|
+
console.log('WebSocket connected');
|
|
435
|
+
this.reconnectAttempts = 0;
|
|
436
|
+
this.options.onConnect();
|
|
437
|
+
|
|
438
|
+
// Flush queued messages
|
|
439
|
+
while (this.messageQueue.length > 0) {
|
|
440
|
+
const msg = this.messageQueue.shift();
|
|
441
|
+
this.send(msg);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
this.ws.onmessage = (event) => {
|
|
446
|
+
const data = JSON.parse(event.data);
|
|
447
|
+
this.options.onMessage(data);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
this.ws.onclose = (event) => {
|
|
451
|
+
console.log('WebSocket closed:', event.code, event.reason);
|
|
452
|
+
this.options.onDisconnect();
|
|
453
|
+
this.scheduleReconnect();
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
this.ws.onerror = (error) => {
|
|
457
|
+
console.error('WebSocket error:', error);
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private scheduleReconnect(): void {
|
|
462
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
463
|
+
console.error('Max reconnection attempts reached');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const delay = Math.min(
|
|
468
|
+
this.reconnectInterval * Math.pow(2, this.reconnectAttempts),
|
|
469
|
+
30000 // Max 30 seconds
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
console.log(`Reconnecting in ${delay}ms...`);
|
|
473
|
+
|
|
474
|
+
setTimeout(() => {
|
|
475
|
+
this.reconnectAttempts++;
|
|
476
|
+
this.connect();
|
|
477
|
+
}, delay);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
send(data: any): void {
|
|
481
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
482
|
+
this.ws.send(JSON.stringify(data));
|
|
483
|
+
} else {
|
|
484
|
+
// Queue message for when connection is restored
|
|
485
|
+
this.messageQueue.push(data);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
close(): void {
|
|
490
|
+
this.maxReconnectAttempts = 0; // Prevent reconnection
|
|
491
|
+
this.ws?.close();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Server-side missed message recovery
|
|
496
|
+
class MessageRecovery {
|
|
497
|
+
async getMessagesSince(roomId: string, lastMessageId: string): Promise<Message[]> {
|
|
498
|
+
// Fetch messages after the last seen message
|
|
499
|
+
return db.message.findMany({
|
|
500
|
+
where: {
|
|
501
|
+
roomId,
|
|
502
|
+
id: { gt: lastMessageId },
|
|
503
|
+
},
|
|
504
|
+
orderBy: { createdAt: 'asc' },
|
|
505
|
+
take: 100, // Limit recovery batch
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async recoverClientState(userId: string, lastSyncTimestamp: number): Promise<{
|
|
510
|
+
messages: Message[];
|
|
511
|
+
notifications: Notification[];
|
|
512
|
+
presenceUpdates: PresenceUpdate[];
|
|
513
|
+
}> {
|
|
514
|
+
const since = new Date(lastSyncTimestamp);
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
messages: await this.getUnreadMessages(userId, since),
|
|
518
|
+
notifications: await this.getUnreadNotifications(userId, since),
|
|
519
|
+
presenceUpdates: await this.getPresenceChanges(since),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### 6. Scaling WebSockets
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// Horizontal scaling with sticky sessions
|
|
529
|
+
// nginx.conf
|
|
530
|
+
upstream websocket_servers {
|
|
531
|
+
ip_hash; // Sticky sessions
|
|
532
|
+
server ws1.example.com:3000;
|
|
533
|
+
server ws2.example.com:3000;
|
|
534
|
+
server ws3.example.com:3000;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
server {
|
|
538
|
+
location /socket.io/ {
|
|
539
|
+
proxy_pass http://websocket_servers;
|
|
540
|
+
proxy_http_version 1.1;
|
|
541
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
542
|
+
proxy_set_header Connection "upgrade";
|
|
543
|
+
proxy_set_header Host $host;
|
|
544
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
545
|
+
proxy_read_timeout 86400;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Broadcasting across servers
|
|
550
|
+
class ScaledBroadcaster {
|
|
551
|
+
async broadcastToRoom(roomId: string, event: string, data: any): Promise<void> {
|
|
552
|
+
// Publish to Redis - all servers will receive
|
|
553
|
+
await pubsub.publish(`broadcast:room:${roomId}`, {
|
|
554
|
+
event,
|
|
555
|
+
data,
|
|
556
|
+
timestamp: Date.now(),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Each server subscribes and emits locally
|
|
561
|
+
setupBroadcastListener(io: Server): void {
|
|
562
|
+
pubsub.psubscribe('broadcast:*', (channel, message) => {
|
|
563
|
+
const [, type, id] = channel.split(':');
|
|
564
|
+
|
|
565
|
+
if (type === 'room') {
|
|
566
|
+
io.to(`room:${id}`).emit(message.event, message.data);
|
|
567
|
+
} else if (type === 'user') {
|
|
568
|
+
io.to(`user:${id}`).emit(message.event, message.data);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## Use Cases
|
|
576
|
+
|
|
577
|
+
### 1. Chat Application
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
// Real-time chat with typing indicators and read receipts
|
|
581
|
+
socket.on('chat:message', async (data) => {
|
|
582
|
+
const message = await createMessage(data);
|
|
583
|
+
io.to(`room:${data.roomId}`).emit('chat:message', message);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
socket.on('chat:read', async ({ roomId, messageId }) => {
|
|
587
|
+
await markAsRead(socket.data.user.id, roomId, messageId);
|
|
588
|
+
socket.to(`room:${roomId}`).emit('chat:read', {
|
|
589
|
+
userId: socket.data.user.id,
|
|
590
|
+
messageId,
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### 2. Live Dashboard
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// Real-time metrics with SSE
|
|
599
|
+
setInterval(async () => {
|
|
600
|
+
const metrics = await gatherMetrics();
|
|
601
|
+
await pubsub.publish('dashboard:metrics', metrics);
|
|
602
|
+
}, 5000);
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## Best Practices
|
|
606
|
+
|
|
607
|
+
### Do's
|
|
608
|
+
|
|
609
|
+
- **Implement heartbeat/ping** - Detect dead connections
|
|
610
|
+
- **Handle reconnection gracefully** - Queue messages, recover state
|
|
611
|
+
- **Use rooms for scaling** - Don't broadcast to all
|
|
612
|
+
- **Implement backpressure** - Handle slow clients
|
|
613
|
+
- **Plan for offline scenarios** - Message queuing
|
|
614
|
+
- **Monitor connection metrics** - Track active connections
|
|
615
|
+
|
|
616
|
+
### Don'ts
|
|
617
|
+
|
|
618
|
+
- Don't trust client data without validation
|
|
619
|
+
- Don't skip authentication
|
|
620
|
+
- Don't broadcast sensitive data
|
|
621
|
+
- Don't ignore connection limits
|
|
622
|
+
- Don't forget cleanup on disconnect
|
|
623
|
+
- Don't use WebSocket for everything
|
|
624
|
+
|
|
625
|
+
## Related Skills
|
|
626
|
+
|
|
627
|
+
- **redis** - Pub/sub and state management
|
|
628
|
+
- **backend-development** - Server architecture
|
|
629
|
+
- **api-architecture** - REST fallbacks
|
|
630
|
+
|
|
631
|
+
## Reference Resources
|
|
632
|
+
|
|
633
|
+
- [Socket.io Documentation](https://socket.io/docs/)
|
|
634
|
+
- [WebSocket MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
|
|
635
|
+
- [SSE MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|