@devo-bmad-custom/agent-orchestration 1.0.6 → 1.0.7
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 +4 -2
- package/src/.agents/skills/tmux-commands/SKILL.md +1 -1
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/SKILL.md +475 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/vision-requests.md +736 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/visionkit-scanner.md +738 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/SKILL.md +410 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/references/weatherkit-patterns.md +567 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/SKILL.md +497 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/references/widgetkit-advanced.md +871 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/typography.csv +58 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/ui-reasoning.csv +101 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/ux-guidelines.csv +100 -0
- package/src/.agents/skills/ui-ux-pro-custom/data/web-interface.csv +31 -0
- package/src/.agents/skills/ui-ux-pro-custom/scripts/core.py +253 -0
- package/src/.agents/skills/ui-ux-pro-custom/scripts/design_system.py +1067 -0
- package/src/.agents/skills/ui-ux-pro-custom/scripts/search.py +114 -0
- package/src/.agents/skills/ux-audit/SKILL.md +151 -0
- package/src/.agents/skills/websocket-engineer/SKILL.md +168 -0
- package/src/.agents/skills/websocket-engineer/references/alternatives.md +391 -0
- package/src/.agents/skills/websocket-engineer/references/patterns.md +400 -0
- package/src/.agents/skills/websocket-engineer/references/protocol.md +195 -0
- package/src/.agents/skills/websocket-engineer/references/scaling.md +333 -0
- package/src/.agents/skills/websocket-engineer/references/security.md +474 -0
- package/src/.agents/skills/writing-skills/SKILL.md +655 -0
- package/src/.agents/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/src/.agents/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/src/.agents/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/src/.agents/skills/writing-skills/persuasion-principles.md +187 -0
- package/src/.agents/skills/writing-skills/render-graphs.js +168 -0
- package/src/.agents/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/src/.claude/commands/bmad-track-compact.md +19 -0
- package/src/.claude/commands/bmad-track-extended.md +19 -0
- package/src/.claude/commands/bmad-track-large.md +19 -0
- package/src/.claude/commands/bmad-track-medium.md +19 -0
- package/src/.claude/commands/bmad-track-nano.md +19 -0
- package/src/.claude/commands/bmad-track-rv.md +18 -0
- package/src/.claude/commands/bmad-track-small.md +19 -0
- package/src/.claude/commands/master-orchestrator.md +15 -0
- package/src/_memory/master-orchestrator-sidecar/docs-index.md +3 -0
- package/src/_memory/master-orchestrator-sidecar/instructions.md +2616 -0
- package/src/_memory/master-orchestrator-sidecar/memories.md +8 -0
- package/src/_memory/master-orchestrator-sidecar/session-state.md +15 -0
- package/src/_memory/master-orchestrator-sidecar/triage-history.md +3 -0
- package/src/_memory/master-orchestrator-sidecar/workflows-overview.html +1230 -0
- package/src/core/agents/master-orchestrator.md +54 -0
- package/src/docs/dev/tmux/actions_popup.py +291 -0
- package/src/docs/dev/tmux/actions_popup.sh +110 -0
- package/src/docs/dev/tmux/claude_usage.sh +15 -0
- package/src/docs/dev/tmux/colors.conf +26 -0
- package/src/docs/dev/tmux/cpu_usage.sh +7 -0
- package/src/docs/dev/tmux/dispatch.sh +10 -0
- package/src/docs/dev/tmux/float_init.sh +13 -0
- package/src/docs/dev/tmux/float_term.sh +23 -0
- package/src/docs/dev/tmux/open_clip.sh +14 -0
- package/src/docs/dev/tmux/paste_claude.sh +26 -0
- package/src/docs/dev/tmux/paste_clipboard.sh +13 -0
- package/src/docs/dev/tmux/paste_image_wrapper.sh +98 -0
- package/src/docs/dev/tmux/ram_usage.sh +3 -0
- package/src/docs/dev/tmux/title_sync.sh +54 -0
- package/src/docs/dev/tmux/tmux-setup.md +867 -0
- package/src/docs/dev/tmux/tmux-test.sh +255 -0
- package/src/docs/dev/tmux/tmux.conf +127 -0
- package/src/docs/dev/tmux/xclip +18 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# WebSocket Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Rooms and Namespaces
|
|
4
|
+
|
|
5
|
+
### Rooms (Channel Grouping)
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
const io = require('socket.io')(3000);
|
|
9
|
+
|
|
10
|
+
io.on('connection', (socket) => {
|
|
11
|
+
// Join a room
|
|
12
|
+
socket.on('join-room', (roomId) => {
|
|
13
|
+
socket.join(roomId);
|
|
14
|
+
socket.emit('joined', { room: roomId });
|
|
15
|
+
|
|
16
|
+
// Notify others in room
|
|
17
|
+
socket.to(roomId).emit('user-joined', {
|
|
18
|
+
userId: socket.id,
|
|
19
|
+
timestamp: Date.now()
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Leave a room
|
|
24
|
+
socket.on('leave-room', (roomId) => {
|
|
25
|
+
socket.leave(roomId);
|
|
26
|
+
socket.to(roomId).emit('user-left', { userId: socket.id });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Send to specific room
|
|
30
|
+
socket.on('message', ({ roomId, text }) => {
|
|
31
|
+
io.to(roomId).emit('message', {
|
|
32
|
+
userId: socket.id,
|
|
33
|
+
text,
|
|
34
|
+
timestamp: Date.now()
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Get all rooms a socket is in
|
|
39
|
+
console.log('Socket rooms:', socket.rooms); // Set { socketId, roomId1, roomId2 }
|
|
40
|
+
|
|
41
|
+
// Disconnect from all rooms
|
|
42
|
+
socket.on('disconnect', () => {
|
|
43
|
+
// Automatically leaves all rooms
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Broadcast to all sockets in a room
|
|
48
|
+
io.to('room123').emit('announcement', 'Hello room!');
|
|
49
|
+
|
|
50
|
+
// Broadcast to multiple rooms
|
|
51
|
+
io.to('room1').to('room2').emit('multi-room', 'data');
|
|
52
|
+
|
|
53
|
+
// Broadcast to room except specific socket
|
|
54
|
+
socket.to('room123').emit('message', 'Others see this');
|
|
55
|
+
|
|
56
|
+
// Get all sockets in a room
|
|
57
|
+
const sockets = await io.in('room123').fetchSockets();
|
|
58
|
+
console.log(`Room has ${sockets.length} connections`);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Namespaces (Logical Separation)
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
// Admin namespace
|
|
65
|
+
const adminNs = io.of('/admin');
|
|
66
|
+
adminNs.on('connection', (socket) => {
|
|
67
|
+
console.log('Admin connected:', socket.id);
|
|
68
|
+
|
|
69
|
+
socket.on('admin-action', (data) => {
|
|
70
|
+
// Admin-only events
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Chat namespace
|
|
75
|
+
const chatNs = io.of('/chat');
|
|
76
|
+
chatNs.on('connection', (socket) => {
|
|
77
|
+
console.log('Chat user connected:', socket.id);
|
|
78
|
+
|
|
79
|
+
socket.on('message', (msg) => {
|
|
80
|
+
chatNs.emit('message', msg); // Broadcast to all in /chat
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Dynamic namespaces
|
|
85
|
+
io.of(/^\/workspace-\d+$/).on('connection', (socket) => {
|
|
86
|
+
const namespace = socket.nsp;
|
|
87
|
+
console.log(`Connected to ${namespace.name}`);
|
|
88
|
+
|
|
89
|
+
socket.on('message', (data) => {
|
|
90
|
+
namespace.emit('message', data);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Broadcasting Patterns
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// Broadcast to everyone including sender
|
|
99
|
+
io.emit('event', data);
|
|
100
|
+
|
|
101
|
+
// Broadcast to everyone except sender
|
|
102
|
+
socket.broadcast.emit('event', data);
|
|
103
|
+
|
|
104
|
+
// Broadcast to specific room
|
|
105
|
+
io.to('room1').emit('event', data);
|
|
106
|
+
|
|
107
|
+
// Broadcast to room except sender
|
|
108
|
+
socket.to('room1').emit('event', data);
|
|
109
|
+
|
|
110
|
+
// Broadcast to multiple rooms
|
|
111
|
+
io.to('room1').to('room2').emit('event', data);
|
|
112
|
+
|
|
113
|
+
// Broadcast to all connected clients in namespace
|
|
114
|
+
io.of('/namespace').emit('event', data);
|
|
115
|
+
|
|
116
|
+
// Volatile messages (ok to drop if client not ready)
|
|
117
|
+
socket.volatile.emit('high-frequency', data);
|
|
118
|
+
|
|
119
|
+
// Broadcast with acknowledgment (to all clients)
|
|
120
|
+
io.timeout(5000).emit('event', data, (err, responses) => {
|
|
121
|
+
if (err) {
|
|
122
|
+
console.log('Some clients did not acknowledge');
|
|
123
|
+
} else {
|
|
124
|
+
console.log('All clients acknowledged:', responses);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Acknowledgments
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
// Server expects acknowledgment
|
|
133
|
+
socket.emit('question', 'Do you agree?', (answer) => {
|
|
134
|
+
console.log('Client answered:', answer);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Client sends acknowledgment
|
|
138
|
+
socket.on('question', (data, callback) => {
|
|
139
|
+
console.log('Server asked:', data);
|
|
140
|
+
callback('Yes, I agree');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Timeout for acknowledgment
|
|
144
|
+
socket.timeout(5000).emit('request', data, (err, response) => {
|
|
145
|
+
if (err) {
|
|
146
|
+
console.log('Client did not acknowledge in time');
|
|
147
|
+
} else {
|
|
148
|
+
console.log('Got response:', response);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Error handling in acknowledgment
|
|
153
|
+
socket.on('save-data', (data, callback) => {
|
|
154
|
+
try {
|
|
155
|
+
saveToDatabase(data);
|
|
156
|
+
callback({ success: true });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
callback({ success: false, error: error.message });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Presence System
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const redis = require('ioredis');
|
|
167
|
+
const redisClient = new redis();
|
|
168
|
+
|
|
169
|
+
class PresenceManager {
|
|
170
|
+
async userConnected(userId, socketId) {
|
|
171
|
+
const key = `user:${userId}:sockets`;
|
|
172
|
+
|
|
173
|
+
// Add socket to user's socket set
|
|
174
|
+
await redisClient.sadd(key, socketId);
|
|
175
|
+
|
|
176
|
+
// Set TTL to auto-cleanup stale connections
|
|
177
|
+
await redisClient.expire(key, 3600);
|
|
178
|
+
|
|
179
|
+
// Get total connections for user
|
|
180
|
+
const socketCount = await redisClient.scard(key);
|
|
181
|
+
|
|
182
|
+
// If first connection, mark user as online
|
|
183
|
+
if (socketCount === 1) {
|
|
184
|
+
await redisClient.hset('presence', userId, JSON.stringify({
|
|
185
|
+
status: 'online',
|
|
186
|
+
lastSeen: Date.now()
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
// Notify friends
|
|
190
|
+
const friends = await this.getUserFriends(userId);
|
|
191
|
+
friends.forEach(friendId => {
|
|
192
|
+
io.to(`user:${friendId}`).emit('presence', {
|
|
193
|
+
userId,
|
|
194
|
+
status: 'online'
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return socketCount;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async userDisconnected(userId, socketId) {
|
|
203
|
+
const key = `user:${userId}:sockets`;
|
|
204
|
+
|
|
205
|
+
// Remove socket from set
|
|
206
|
+
await redisClient.srem(key, socketId);
|
|
207
|
+
|
|
208
|
+
const socketCount = await redisClient.scard(key);
|
|
209
|
+
|
|
210
|
+
// If no more connections, mark offline
|
|
211
|
+
if (socketCount === 0) {
|
|
212
|
+
await redisClient.hset('presence', userId, JSON.stringify({
|
|
213
|
+
status: 'offline',
|
|
214
|
+
lastSeen: Date.now()
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
const friends = await this.getUserFriends(userId);
|
|
218
|
+
friends.forEach(friendId => {
|
|
219
|
+
io.to(`user:${friendId}`).emit('presence', {
|
|
220
|
+
userId,
|
|
221
|
+
status: 'offline',
|
|
222
|
+
lastSeen: Date.now()
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return socketCount;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async getUserStatus(userId) {
|
|
231
|
+
const data = await redisClient.hget('presence', userId);
|
|
232
|
+
return data ? JSON.parse(data) : { status: 'offline' };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getBulkPresence(userIds) {
|
|
236
|
+
const pipeline = redisClient.pipeline();
|
|
237
|
+
userIds.forEach(id => pipeline.hget('presence', id));
|
|
238
|
+
|
|
239
|
+
const results = await pipeline.exec();
|
|
240
|
+
return userIds.map((id, i) => ({
|
|
241
|
+
userId: id,
|
|
242
|
+
...JSON.parse(results[i][1] || '{"status":"offline"}')
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Usage
|
|
248
|
+
const presence = new PresenceManager();
|
|
249
|
+
|
|
250
|
+
io.on('connection', async (socket) => {
|
|
251
|
+
const userId = socket.handshake.auth.userId;
|
|
252
|
+
|
|
253
|
+
await presence.userConnected(userId, socket.id);
|
|
254
|
+
socket.join(`user:${userId}`);
|
|
255
|
+
|
|
256
|
+
socket.on('disconnect', async () => {
|
|
257
|
+
await presence.userDisconnected(userId, socket.id);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Get presence for user's friends
|
|
261
|
+
socket.on('get-presence', async (friendIds, callback) => {
|
|
262
|
+
const presenceData = await presence.getBulkPresence(friendIds);
|
|
263
|
+
callback(presenceData);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Message Queue Pattern
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
// Queue messages when client disconnected
|
|
272
|
+
const messageQueue = new Map();
|
|
273
|
+
|
|
274
|
+
io.on('connection', (socket) => {
|
|
275
|
+
const userId = socket.handshake.auth.userId;
|
|
276
|
+
|
|
277
|
+
// Deliver queued messages on connect
|
|
278
|
+
const queuedMessages = messageQueue.get(userId) || [];
|
|
279
|
+
if (queuedMessages.length > 0) {
|
|
280
|
+
socket.emit('queued-messages', queuedMessages);
|
|
281
|
+
messageQueue.delete(userId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
socket.on('disconnect', () => {
|
|
285
|
+
// Mark user as disconnected
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
const userSockets = io.sockets.sockets;
|
|
288
|
+
const hasOtherConnection = Array.from(userSockets.values())
|
|
289
|
+
.some(s => s.handshake.auth.userId === userId);
|
|
290
|
+
|
|
291
|
+
if (!hasOtherConnection) {
|
|
292
|
+
// User fully disconnected, queue new messages
|
|
293
|
+
messageQueue.set(userId, []);
|
|
294
|
+
}
|
|
295
|
+
}, 1000);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Queue message if user offline
|
|
300
|
+
async function sendMessage(userId, message) {
|
|
301
|
+
const userOnline = await isUserOnline(userId);
|
|
302
|
+
|
|
303
|
+
if (userOnline) {
|
|
304
|
+
io.to(`user:${userId}`).emit('message', message);
|
|
305
|
+
} else {
|
|
306
|
+
const queue = messageQueue.get(userId) || [];
|
|
307
|
+
queue.push(message);
|
|
308
|
+
messageQueue.set(userId, queue);
|
|
309
|
+
|
|
310
|
+
// Persist to database for longer-term storage
|
|
311
|
+
await saveMessageToDb(userId, message);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Pub/Sub Pattern
|
|
317
|
+
|
|
318
|
+
```javascript
|
|
319
|
+
const EventEmitter = require('events');
|
|
320
|
+
|
|
321
|
+
class MessageBus extends EventEmitter {
|
|
322
|
+
constructor(io, redis) {
|
|
323
|
+
super();
|
|
324
|
+
this.io = io;
|
|
325
|
+
this.redis = redis;
|
|
326
|
+
this.setupSubscriptions();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
setupSubscriptions() {
|
|
330
|
+
// Subscribe to Redis channels
|
|
331
|
+
this.redis.psubscribe('room:*', (err, count) => {
|
|
332
|
+
console.log(`Subscribed to ${count} channels`);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
this.redis.on('pmessage', (pattern, channel, message) => {
|
|
336
|
+
const data = JSON.parse(message);
|
|
337
|
+
const roomId = channel.split(':')[1];
|
|
338
|
+
|
|
339
|
+
// Emit to Socket.IO room
|
|
340
|
+
this.io.to(roomId).emit(data.event, data.payload);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
publish(roomId, event, payload) {
|
|
345
|
+
// Publish to Redis (distributed across servers)
|
|
346
|
+
this.redis.publish(
|
|
347
|
+
`room:${roomId}`,
|
|
348
|
+
JSON.stringify({ event, payload })
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Usage
|
|
354
|
+
const messageBus = new MessageBus(io, redisClient);
|
|
355
|
+
|
|
356
|
+
io.on('connection', (socket) => {
|
|
357
|
+
socket.on('send-message', ({ roomId, text }) => {
|
|
358
|
+
// Publish to all servers via Redis
|
|
359
|
+
messageBus.publish(roomId, 'message', {
|
|
360
|
+
userId: socket.id,
|
|
361
|
+
text,
|
|
362
|
+
timestamp: Date.now()
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Backpressure Handling
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
io.on('connection', (socket) => {
|
|
372
|
+
const MAX_BUFFER_SIZE = 10000;
|
|
373
|
+
let bufferSize = 0;
|
|
374
|
+
|
|
375
|
+
const originalEmit = socket.emit.bind(socket);
|
|
376
|
+
|
|
377
|
+
socket.emit = function(event, ...args) {
|
|
378
|
+
bufferSize++;
|
|
379
|
+
|
|
380
|
+
if (bufferSize > MAX_BUFFER_SIZE) {
|
|
381
|
+
console.warn('Buffer overflow, dropping message');
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const result = originalEmit(event, ...args);
|
|
386
|
+
|
|
387
|
+
// Track buffer drain
|
|
388
|
+
socket.once('drain', () => {
|
|
389
|
+
bufferSize = 0;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
return result;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// Monitor buffer size
|
|
396
|
+
socket.on('drain', () => {
|
|
397
|
+
console.log('Socket buffer drained');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
```
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# WebSocket Protocol Reference
|
|
2
|
+
|
|
3
|
+
## Protocol Basics
|
|
4
|
+
|
|
5
|
+
### Handshake Process
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Client → Server: HTTP Upgrade Request
|
|
9
|
+
GET /socket.io/?EIO=4&transport=websocket HTTP/1.1
|
|
10
|
+
Host: example.com
|
|
11
|
+
Upgrade: websocket
|
|
12
|
+
Connection: Upgrade
|
|
13
|
+
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
|
|
14
|
+
Sec-WebSocket-Version: 13
|
|
15
|
+
|
|
16
|
+
Server → Client: HTTP 101 Switching Protocols
|
|
17
|
+
HTTP/1.1 101 Switching Protocols
|
|
18
|
+
Upgrade: websocket
|
|
19
|
+
Connection: Upgrade
|
|
20
|
+
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Frame Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
0 1 2 3
|
|
27
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
28
|
+
+-+-+-+-+-------+-+-------------+-------------------------------+
|
|
29
|
+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|
|
30
|
+
|I|S|S|S| (4) |A| (7) | (16/64) |
|
|
31
|
+
|N|V|V|V| |S| | (if payload len==126/127) |
|
|
32
|
+
| |1|2|3| |K| | |
|
|
33
|
+
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
|
34
|
+
| Extended payload length continued, if payload len == 127 |
|
|
35
|
+
+ - - - - - - - - - - - - - - - +-------------------------------+
|
|
36
|
+
| |Masking-key, if MASK set to 1 |
|
|
37
|
+
+-------------------------------+-------------------------------+
|
|
38
|
+
| Masking-key (continued) | Payload Data |
|
|
39
|
+
+-------------------------------- - - - - - - - - - - - - - - - +
|
|
40
|
+
: Payload Data continued ... :
|
|
41
|
+
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
|
42
|
+
| Payload Data continued ... |
|
|
43
|
+
+---------------------------------------------------------------+
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Opcodes
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
const OPCODES = {
|
|
50
|
+
CONTINUATION: 0x0, // Continuation frame
|
|
51
|
+
TEXT: 0x1, // Text frame
|
|
52
|
+
BINARY: 0x2, // Binary frame
|
|
53
|
+
CLOSE: 0x8, // Connection close
|
|
54
|
+
PING: 0x9, // Heartbeat ping
|
|
55
|
+
PONG: 0xA // Heartbeat pong
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Ping/Pong Mechanism
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// Server-side ping/pong with ws library
|
|
63
|
+
const WebSocket = require('ws');
|
|
64
|
+
const wss = new WebSocket.Server({ port: 8080 });
|
|
65
|
+
|
|
66
|
+
wss.on('connection', (ws) => {
|
|
67
|
+
ws.isAlive = true;
|
|
68
|
+
|
|
69
|
+
ws.on('pong', () => {
|
|
70
|
+
ws.isAlive = true;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ws.on('message', (data) => {
|
|
74
|
+
console.log('Received:', data);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Ping clients every 30 seconds
|
|
79
|
+
const interval = setInterval(() => {
|
|
80
|
+
wss.clients.forEach((ws) => {
|
|
81
|
+
if (ws.isAlive === false) {
|
|
82
|
+
return ws.terminate();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ws.isAlive = false;
|
|
86
|
+
ws.ping(); // Send ping frame
|
|
87
|
+
});
|
|
88
|
+
}, 30000);
|
|
89
|
+
|
|
90
|
+
wss.on('close', () => {
|
|
91
|
+
clearInterval(interval);
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Close Codes
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const CLOSE_CODES = {
|
|
99
|
+
1000: 'Normal Closure',
|
|
100
|
+
1001: 'Going Away',
|
|
101
|
+
1002: 'Protocol Error',
|
|
102
|
+
1003: 'Unsupported Data',
|
|
103
|
+
1005: 'No Status Received',
|
|
104
|
+
1006: 'Abnormal Closure',
|
|
105
|
+
1007: 'Invalid Payload',
|
|
106
|
+
1008: 'Policy Violation',
|
|
107
|
+
1009: 'Message Too Big',
|
|
108
|
+
1010: 'Mandatory Extension',
|
|
109
|
+
1011: 'Internal Server Error',
|
|
110
|
+
1015: 'TLS Handshake Fail'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Proper close handling
|
|
114
|
+
ws.close(1000, 'Normal closure');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Message Size Limits
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
// Set max payload size (default 100MB)
|
|
121
|
+
const wss = new WebSocket.Server({
|
|
122
|
+
port: 8080,
|
|
123
|
+
maxPayload: 1024 * 1024 // 1MB
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Handle too large messages
|
|
127
|
+
ws.on('error', (error) => {
|
|
128
|
+
if (error.message.includes('Max payload size exceeded')) {
|
|
129
|
+
ws.close(1009, 'Message too big');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Compression (permessage-deflate)
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const wss = new WebSocket.Server({
|
|
138
|
+
port: 8080,
|
|
139
|
+
perMessageDeflate: {
|
|
140
|
+
zlibDeflateOptions: {
|
|
141
|
+
chunkSize: 1024,
|
|
142
|
+
memLevel: 7,
|
|
143
|
+
level: 3
|
|
144
|
+
},
|
|
145
|
+
zlibInflateOptions: {
|
|
146
|
+
chunkSize: 10 * 1024
|
|
147
|
+
},
|
|
148
|
+
clientNoContextTakeover: true,
|
|
149
|
+
serverNoContextTakeover: true,
|
|
150
|
+
serverMaxWindowBits: 10,
|
|
151
|
+
concurrencyLimit: 10,
|
|
152
|
+
threshold: 1024 // Only compress messages > 1KB
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Binary Data Handling
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// Send binary data
|
|
161
|
+
const buffer = Buffer.from([0x00, 0x01, 0x02, 0x03]);
|
|
162
|
+
ws.send(buffer, { binary: true });
|
|
163
|
+
|
|
164
|
+
// Receive binary data
|
|
165
|
+
ws.on('message', (data) => {
|
|
166
|
+
if (data instanceof Buffer) {
|
|
167
|
+
console.log('Received binary:', data);
|
|
168
|
+
} else {
|
|
169
|
+
console.log('Received text:', data.toString());
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ArrayBuffer in browser
|
|
174
|
+
socket.binaryType = 'arraybuffer';
|
|
175
|
+
socket.onmessage = (event) => {
|
|
176
|
+
if (event.data instanceof ArrayBuffer) {
|
|
177
|
+
const view = new Uint8Array(event.data);
|
|
178
|
+
console.log('Received:', view);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Protocol Comparison
|
|
184
|
+
|
|
185
|
+
| Feature | WebSocket | Socket.IO |
|
|
186
|
+
|---------|-----------|-----------|
|
|
187
|
+
| Protocol | Native WS | WS + fallbacks |
|
|
188
|
+
| Handshake | HTTP Upgrade | Engine.IO handshake |
|
|
189
|
+
| Reconnection | Manual | Automatic |
|
|
190
|
+
| Broadcasting | Manual | Built-in |
|
|
191
|
+
| Rooms | Manual | Built-in |
|
|
192
|
+
| Acknowledgments | Manual | Built-in |
|
|
193
|
+
| Binary | Native | Converted |
|
|
194
|
+
| Overhead | Minimal | Higher |
|
|
195
|
+
| Fallback | None | Long polling, SSE |
|