claude-code-templates 1.8.0 → 1.8.2

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.
@@ -0,0 +1,526 @@
1
+ /**
2
+ * WebSocketServer - Handles real-time communication between server and clients
3
+ * Part of the modular backend architecture for Phase 3
4
+ */
5
+ const WebSocket = require('ws');
6
+ const chalk = require('chalk');
7
+
8
+ class WebSocketServer {
9
+ constructor(httpServer, options = {}, performanceMonitor = null) {
10
+ this.httpServer = httpServer;
11
+ this.performanceMonitor = performanceMonitor;
12
+ this.options = {
13
+ port: options.port || 3334,
14
+ path: options.path || '/ws',
15
+ heartbeatInterval: options.heartbeatInterval || 30000,
16
+ ...options
17
+ };
18
+
19
+ this.wss = null;
20
+ this.clients = new Map();
21
+ this.heartbeatInterval = null;
22
+ this.isRunning = false;
23
+ this.messageQueue = [];
24
+ this.maxQueueSize = 100;
25
+ }
26
+
27
+ /**
28
+ * Initialize and start the WebSocket server
29
+ */
30
+ async initialize() {
31
+ try {
32
+ console.log(chalk.blue('🔌 Initializing WebSocket server...'));
33
+
34
+ // Create WebSocket server
35
+ this.wss = new WebSocket.Server({
36
+ server: this.httpServer,
37
+ path: this.options.path,
38
+ clientTracking: true
39
+ });
40
+
41
+ this.setupEventHandlers();
42
+ this.startHeartbeat();
43
+ this.isRunning = true;
44
+
45
+ console.log(chalk.green(`✅ WebSocket server initialized on ${this.options.path}`));
46
+ } catch (error) {
47
+ console.error(chalk.red('❌ Failed to initialize WebSocket server:'), error);
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Setup WebSocket event handlers
54
+ */
55
+ setupEventHandlers() {
56
+ this.wss.on('connection', (ws, request) => {
57
+ this.handleConnection(ws, request);
58
+ });
59
+
60
+ this.wss.on('error', (error) => {
61
+ console.error(chalk.red('WebSocket server error:'), error);
62
+ });
63
+
64
+ this.wss.on('close', () => {
65
+ console.log(chalk.yellow('🔌 WebSocket server closed'));
66
+ this.isRunning = false;
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Handle new WebSocket connection
72
+ * @param {WebSocket} ws - WebSocket connection
73
+ * @param {Object} request - HTTP request object
74
+ */
75
+ handleConnection(ws, request) {
76
+ const clientId = this.generateClientId();
77
+ const clientInfo = {
78
+ id: clientId,
79
+ ws: ws,
80
+ ip: request.socket.remoteAddress,
81
+ userAgent: request.headers['user-agent'],
82
+ connectedAt: new Date(),
83
+ isAlive: true,
84
+ subscriptions: new Set()
85
+ };
86
+
87
+ this.clients.set(clientId, clientInfo);
88
+ console.log(chalk.green(`🔗 WebSocket client connected: ${clientId} (${this.clients.size} total)`));
89
+
90
+ // Track WebSocket connection in performance monitor
91
+ if (this.performanceMonitor) {
92
+ this.performanceMonitor.recordWebSocket('connection', {
93
+ clientId,
94
+ totalClients: this.clients.size,
95
+ ip: request.socket.remoteAddress
96
+ });
97
+ }
98
+
99
+ // Send welcome message
100
+ this.sendToClient(clientId, {
101
+ type: 'connection',
102
+ data: {
103
+ clientId: clientId,
104
+ serverTime: new Date().toISOString(),
105
+ message: 'Connected to Claude Code Analytics WebSocket'
106
+ }
107
+ });
108
+
109
+ // Send any queued messages
110
+ this.sendQueuedMessages(clientId);
111
+
112
+ // Setup client event handlers
113
+ ws.on('message', (message) => {
114
+ this.handleClientMessage(clientId, message);
115
+ });
116
+
117
+ ws.on('close', (code, reason) => {
118
+ this.handleClientDisconnect(clientId, code, reason);
119
+ });
120
+
121
+ ws.on('error', (error) => {
122
+ console.error(chalk.red(`WebSocket client error (${clientId}):`), error);
123
+ });
124
+
125
+ ws.on('pong', () => {
126
+ this.handleClientPong(clientId);
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Handle message from client
132
+ * @param {string} clientId - Client ID
133
+ * @param {Buffer} message - Message buffer
134
+ */
135
+ handleClientMessage(clientId, message) {
136
+ try {
137
+ const data = JSON.parse(message.toString());
138
+ const client = this.clients.get(clientId);
139
+
140
+ if (!client) return;
141
+
142
+ console.log(chalk.cyan(`📨 Message from ${clientId}:`), data.type);
143
+
144
+ // Track message in performance monitor
145
+ if (this.performanceMonitor) {
146
+ this.performanceMonitor.recordWebSocket('message_received', {
147
+ clientId,
148
+ messageType: data.type,
149
+ messageSize: message.length
150
+ });
151
+ }
152
+
153
+ switch (data.type) {
154
+ case 'subscribe':
155
+ this.handleSubscription(clientId, data.channel);
156
+ break;
157
+ case 'unsubscribe':
158
+ this.handleUnsubscription(clientId, data.channel);
159
+ break;
160
+ case 'ping':
161
+ this.sendToClient(clientId, { type: 'pong', timestamp: Date.now() });
162
+ break;
163
+ case 'refresh_request':
164
+ this.handleRefreshRequest(clientId);
165
+ break;
166
+ default:
167
+ console.warn(chalk.yellow(`Unknown message type from ${clientId}: ${data.type}`));
168
+ }
169
+ } catch (error) {
170
+ console.error(chalk.red(`Error parsing message from ${clientId}:`), error);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Handle client subscription to a channel
176
+ * @param {string} clientId - Client ID
177
+ * @param {string} channel - Channel name
178
+ */
179
+ handleSubscription(clientId, channel) {
180
+ const client = this.clients.get(clientId);
181
+ if (!client) return;
182
+
183
+ client.subscriptions.add(channel);
184
+ console.log(chalk.green(`📡 Client ${clientId} subscribed to ${channel}`));
185
+
186
+ this.sendToClient(clientId, {
187
+ type: 'subscription_confirmed',
188
+ data: { channel, subscriptions: Array.from(client.subscriptions) }
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Handle client unsubscription from a channel
194
+ * @param {string} clientId - Client ID
195
+ * @param {string} channel - Channel name
196
+ */
197
+ handleUnsubscription(clientId, channel) {
198
+ const client = this.clients.get(clientId);
199
+ if (!client) return;
200
+
201
+ client.subscriptions.delete(channel);
202
+ console.log(chalk.yellow(`📡 Client ${clientId} unsubscribed from ${channel}`));
203
+
204
+ this.sendToClient(clientId, {
205
+ type: 'unsubscription_confirmed',
206
+ data: { channel, subscriptions: Array.from(client.subscriptions) }
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Handle refresh request from client
212
+ * @param {string} clientId - Client ID
213
+ */
214
+ handleRefreshRequest(clientId) {
215
+ console.log(chalk.blue(`🔄 Refresh requested by ${clientId}`));
216
+ // Emit refresh event that the main analytics server can listen to
217
+ this.emit('refresh_requested', { clientId });
218
+ }
219
+
220
+ /**
221
+ * Handle client disconnection
222
+ * @param {string} clientId - Client ID
223
+ * @param {number} code - Close code
224
+ * @param {Buffer} reason - Close reason
225
+ */
226
+ handleClientDisconnect(clientId, code, reason) {
227
+ this.clients.delete(clientId);
228
+ console.log(chalk.yellow(`🔗 WebSocket client disconnected: ${clientId} (${this.clients.size} remaining)`));
229
+ console.log(chalk.gray(` Close code: ${code}, Reason: ${reason || 'No reason provided'}`));
230
+
231
+ // Track disconnection in performance monitor
232
+ if (this.performanceMonitor) {
233
+ this.performanceMonitor.recordWebSocket('disconnection', {
234
+ clientId,
235
+ closeCode: code,
236
+ totalClients: this.clients.size,
237
+ reason: reason?.toString() || 'No reason provided'
238
+ });
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Handle client pong response
244
+ * @param {string} clientId - Client ID
245
+ */
246
+ handleClientPong(clientId) {
247
+ const client = this.clients.get(clientId);
248
+ if (client) {
249
+ client.isAlive = true;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Broadcast message to all connected clients
255
+ * @param {Object} message - Message to broadcast
256
+ * @param {string} channel - Optional channel filter
257
+ */
258
+ broadcast(message, channel = null) {
259
+ const messageStr = JSON.stringify({
260
+ ...message,
261
+ timestamp: Date.now(),
262
+ server: 'Claude Code Analytics'
263
+ });
264
+
265
+ let sentCount = 0;
266
+ this.clients.forEach((client, clientId) => {
267
+ // Filter by channel subscription if specified
268
+ if (channel && !client.subscriptions.has(channel)) {
269
+ return;
270
+ }
271
+
272
+ if (client.ws.readyState === WebSocket.OPEN) {
273
+ try {
274
+ client.ws.send(messageStr);
275
+ sentCount++;
276
+ } catch (error) {
277
+ console.error(chalk.red(`Error sending to client ${clientId}:`), error);
278
+ this.clients.delete(clientId);
279
+ }
280
+ }
281
+ });
282
+
283
+ if (sentCount > 0) {
284
+ console.log(chalk.green(`📢 Broadcasted ${message.type} to ${sentCount} clients${channel ? ` on channel ${channel}` : ''}`));
285
+ }
286
+
287
+ // Queue message if no clients connected
288
+ if (sentCount === 0 && this.clients.size === 0) {
289
+ this.queueMessage(message);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Send message to specific client
295
+ * @param {string} clientId - Client ID
296
+ * @param {Object} message - Message to send
297
+ */
298
+ sendToClient(clientId, message) {
299
+ const client = this.clients.get(clientId);
300
+ if (!client || client.ws.readyState !== WebSocket.OPEN) {
301
+ return false;
302
+ }
303
+
304
+ try {
305
+ const messageStr = JSON.stringify({
306
+ ...message,
307
+ timestamp: Date.now(),
308
+ server: 'Claude Code Analytics'
309
+ });
310
+ client.ws.send(messageStr);
311
+ return true;
312
+ } catch (error) {
313
+ console.error(chalk.red(`Error sending to client ${clientId}:`), error);
314
+ this.clients.delete(clientId);
315
+ return false;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Queue message for future delivery
321
+ * @param {Object} message - Message to queue
322
+ */
323
+ queueMessage(message) {
324
+ this.messageQueue.push({
325
+ ...message,
326
+ queuedAt: Date.now()
327
+ });
328
+
329
+ // Keep queue size manageable
330
+ if (this.messageQueue.length > this.maxQueueSize) {
331
+ this.messageQueue.shift();
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Send queued messages to newly connected client
337
+ * @param {string} clientId - Client ID
338
+ */
339
+ sendQueuedMessages(clientId) {
340
+ if (this.messageQueue.length === 0) return;
341
+
342
+ console.log(chalk.blue(`📦 Sending ${this.messageQueue.length} queued messages to ${clientId}`));
343
+
344
+ this.messageQueue.forEach(message => {
345
+ this.sendToClient(clientId, {
346
+ ...message,
347
+ type: 'queued_' + message.type,
348
+ wasQueued: true
349
+ });
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Notify clients of conversation state change
355
+ * @param {string} conversationId - Conversation ID
356
+ * @param {string} newState - New state
357
+ * @param {Object} metadata - Additional metadata
358
+ */
359
+ notifyConversationStateChange(conversationId, newState, metadata = {}) {
360
+ this.broadcast({
361
+ type: 'conversation_state_change',
362
+ data: {
363
+ conversationId,
364
+ newState,
365
+ ...metadata
366
+ }
367
+ }, 'conversation_updates');
368
+ }
369
+
370
+ /**
371
+ * Notify clients of data refresh
372
+ * @param {Object} data - Updated data
373
+ */
374
+ notifyDataRefresh(data) {
375
+ this.broadcast({
376
+ type: 'data_refresh',
377
+ data
378
+ }, 'data_updates');
379
+ }
380
+
381
+ /**
382
+ * Notify clients of system status change
383
+ * @param {Object} status - System status
384
+ */
385
+ notifySystemStatus(status) {
386
+ this.broadcast({
387
+ type: 'system_status',
388
+ data: status
389
+ }, 'system_updates');
390
+ }
391
+
392
+ /**
393
+ * Start heartbeat mechanism
394
+ */
395
+ startHeartbeat() {
396
+ this.heartbeatInterval = setInterval(() => {
397
+ this.clients.forEach((client, clientId) => {
398
+ if (!client.isAlive) {
399
+ console.log(chalk.yellow(`💔 Terminating unresponsive client: ${clientId}`));
400
+ client.ws.terminate();
401
+ this.clients.delete(clientId);
402
+ return;
403
+ }
404
+
405
+ client.isAlive = false;
406
+ if (client.ws.readyState === WebSocket.OPEN) {
407
+ client.ws.ping();
408
+ }
409
+ });
410
+ }, this.options.heartbeatInterval);
411
+ }
412
+
413
+ /**
414
+ * Stop heartbeat mechanism
415
+ */
416
+ stopHeartbeat() {
417
+ if (this.heartbeatInterval) {
418
+ clearInterval(this.heartbeatInterval);
419
+ this.heartbeatInterval = null;
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Generate unique client ID
425
+ * @returns {string} Client ID
426
+ */
427
+ generateClientId() {
428
+ return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
429
+ }
430
+
431
+ /**
432
+ * Get server statistics
433
+ * @returns {Object} Server statistics
434
+ */
435
+ getStats() {
436
+ const clientStats = Array.from(this.clients.values()).map(client => ({
437
+ id: client.id,
438
+ ip: client.ip,
439
+ connectedAt: client.connectedAt,
440
+ subscriptions: Array.from(client.subscriptions),
441
+ isAlive: client.isAlive
442
+ }));
443
+
444
+ return {
445
+ isRunning: this.isRunning,
446
+ clientCount: this.clients.size,
447
+ queuedMessages: this.messageQueue.length,
448
+ clients: clientStats,
449
+ uptime: this.isRunning ? Date.now() - this.startTime : 0
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Gracefully close all connections and stop server
455
+ */
456
+ async close() {
457
+ console.log(chalk.yellow('🔌 Closing WebSocket server...'));
458
+
459
+ this.stopHeartbeat();
460
+
461
+ // Close all client connections
462
+ this.clients.forEach((client, clientId) => {
463
+ if (client.ws.readyState === WebSocket.OPEN) {
464
+ client.ws.close(1000, 'Server shutting down');
465
+ }
466
+ });
467
+
468
+ this.clients.clear();
469
+
470
+ if (this.wss) {
471
+ await new Promise((resolve) => {
472
+ this.wss.close(resolve);
473
+ });
474
+ }
475
+
476
+ this.isRunning = false;
477
+ console.log(chalk.green('✅ WebSocket server closed'));
478
+ }
479
+
480
+ /**
481
+ * Event emitter functionality
482
+ */
483
+ emit(event, data) {
484
+ // Simple event emitter implementation
485
+ if (this.listeners && this.listeners[event]) {
486
+ this.listeners[event].forEach(callback => {
487
+ try {
488
+ callback(data);
489
+ } catch (error) {
490
+ console.error(`Error in WebSocket event listener for ${event}:`, error);
491
+ }
492
+ });
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Add event listener
498
+ * @param {string} event - Event name
499
+ * @param {Function} callback - Callback function
500
+ */
501
+ on(event, callback) {
502
+ if (!this.listeners) {
503
+ this.listeners = {};
504
+ }
505
+ if (!this.listeners[event]) {
506
+ this.listeners[event] = [];
507
+ }
508
+ this.listeners[event].push(callback);
509
+ }
510
+
511
+ /**
512
+ * Remove event listener
513
+ * @param {string} event - Event name
514
+ * @param {Function} callback - Callback function
515
+ */
516
+ off(event, callback) {
517
+ if (this.listeners && this.listeners[event]) {
518
+ const index = this.listeners[event].indexOf(callback);
519
+ if (index !== -1) {
520
+ this.listeners[event].splice(index, 1);
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ module.exports = WebSocketServer;