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,523 @@
1
+ /**
2
+ * WebSocketService - Handles real-time communication with the server
3
+ * Part of the modular frontend architecture for Phase 3
4
+ */
5
+ class WebSocketService {
6
+ constructor() {
7
+ this.ws = null;
8
+ this.url = null;
9
+ this.isConnected = false;
10
+ this.reconnectAttempts = 0;
11
+ this.maxReconnectAttempts = 5;
12
+ this.reconnectDelay = 1000;
13
+ this.maxReconnectDelay = 30000;
14
+ this.heartbeatInterval = null;
15
+ this.heartbeatTimeout = null;
16
+
17
+ this.eventListeners = new Map();
18
+ this.subscriptions = new Set();
19
+ this.messageQueue = [];
20
+ this.autoReconnect = true;
21
+
22
+ // Message ID tracking for responses
23
+ this.messageId = 0;
24
+ this.pendingMessages = new Map();
25
+ }
26
+
27
+ /**
28
+ * Connect to WebSocket server
29
+ * @param {string} url - WebSocket URL (default: current host with /ws path)
30
+ */
31
+ connect(url = null) {
32
+ if (this.isConnected) {
33
+ console.log('🔌 WebSocket already connected');
34
+ return Promise.resolve();
35
+ }
36
+
37
+ // Auto-detect WebSocket URL if not provided
38
+ if (!url) {
39
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
40
+ const host = window.location.host;
41
+ this.url = `${protocol}//${host}/ws`;
42
+ } else {
43
+ this.url = url;
44
+ }
45
+
46
+ return new Promise((resolve, reject) => {
47
+ try {
48
+ console.log(`🔌 Connecting to WebSocket: ${this.url}`);
49
+ this.ws = new WebSocket(this.url);
50
+
51
+ this.ws.onopen = (event) => {
52
+ this.handleOpen(event);
53
+ resolve();
54
+ };
55
+
56
+ this.ws.onmessage = (event) => {
57
+ this.handleMessage(event);
58
+ };
59
+
60
+ this.ws.onclose = (event) => {
61
+ this.handleClose(event);
62
+ };
63
+
64
+ this.ws.onerror = (event) => {
65
+ this.handleError(event);
66
+ if (!this.isConnected) {
67
+ reject(new Error('WebSocket connection failed'));
68
+ }
69
+ };
70
+
71
+ } catch (error) {
72
+ console.error('❌ Failed to create WebSocket connection:', error);
73
+ reject(error);
74
+ }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Handle WebSocket connection open
80
+ * @param {Event} event - Open event
81
+ */
82
+ handleOpen(event) {
83
+ console.log('✅ WebSocket connected');
84
+ this.isConnected = true;
85
+ this.reconnectAttempts = 0;
86
+
87
+ // Start heartbeat
88
+ this.startHeartbeat();
89
+
90
+ // Process queued messages
91
+ this.processMessageQueue();
92
+
93
+ // Re-subscribe to channels
94
+ this.resubscribeToChannels();
95
+
96
+ // Emit connection event
97
+ this.emit('connected', { event });
98
+ }
99
+
100
+ /**
101
+ * Handle WebSocket message
102
+ * @param {MessageEvent} event - Message event
103
+ */
104
+ handleMessage(event) {
105
+ try {
106
+ const data = JSON.parse(event.data);
107
+ console.log('📨 WebSocket message received:', data.type);
108
+
109
+ // Handle different message types
110
+ switch (data.type) {
111
+ case 'connection':
112
+ this.handleConnectionMessage(data);
113
+ break;
114
+ case 'pong':
115
+ this.handlePong(data);
116
+ break;
117
+ case 'conversation_state_change':
118
+ this.handleConversationStateChange(data);
119
+ break;
120
+ case 'data_refresh':
121
+ this.handleDataRefresh(data);
122
+ break;
123
+ case 'system_status':
124
+ this.handleSystemStatus(data);
125
+ break;
126
+ case 'file_change':
127
+ this.handleFileChange(data);
128
+ break;
129
+ case 'process_change':
130
+ this.handleProcessChange(data);
131
+ break;
132
+ case 'subscription_confirmed':
133
+ case 'unsubscription_confirmed':
134
+ this.handleSubscriptionConfirmation(data);
135
+ break;
136
+ default:
137
+ // Check if it's a response to a pending message
138
+ if (data.messageId && this.pendingMessages.has(data.messageId)) {
139
+ this.handleMessageResponse(data);
140
+ } else {
141
+ this.emit('message', data);
142
+ }
143
+ }
144
+
145
+ } catch (error) {
146
+ console.error('❌ Error parsing WebSocket message:', error);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Handle WebSocket connection close
152
+ * @param {CloseEvent} event - Close event
153
+ */
154
+ handleClose(event) {
155
+ console.log('🔌 WebSocket disconnected:', event.code, event.reason);
156
+ this.isConnected = false;
157
+ this.stopHeartbeat();
158
+
159
+ // Emit disconnection event
160
+ this.emit('disconnected', { event });
161
+
162
+ // Auto-reconnect if enabled
163
+ if (this.autoReconnect && event.code !== 1000) { // 1000 = normal closure
164
+ this.scheduleReconnect();
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Handle WebSocket error
170
+ * @param {Event} event - Error event
171
+ */
172
+ handleError(event) {
173
+ console.error('❌ WebSocket error:', event);
174
+ this.emit('error', { event });
175
+ }
176
+
177
+ /**
178
+ * Handle connection message
179
+ * @param {Object} data - Message data
180
+ */
181
+ handleConnectionMessage(data) {
182
+ console.log('🎉 WebSocket connection established:', data.data.clientId);
183
+ this.clientId = data.data.clientId;
184
+ this.emit('connection_established', data.data);
185
+ }
186
+
187
+ /**
188
+ * Handle conversation state change
189
+ * @param {Object} data - Message data
190
+ */
191
+ handleConversationStateChange(data) {
192
+ console.log(`🔄 Conversation state changed: ${data.data.conversationId} → ${data.data.newState}`);
193
+ this.emit('conversation_state_change', data.data);
194
+ }
195
+
196
+ /**
197
+ * Handle data refresh
198
+ * @param {Object} data - Message data
199
+ */
200
+ handleDataRefresh(data) {
201
+ console.log('📊 Data refresh received');
202
+ this.emit('data_refresh', data.data);
203
+ }
204
+
205
+ /**
206
+ * Handle system status
207
+ * @param {Object} data - Message data
208
+ */
209
+ handleSystemStatus(data) {
210
+ console.log('ℹ️ System status update:', data.data);
211
+ this.emit('system_status', data.data);
212
+ }
213
+
214
+ /**
215
+ * Handle file change notification
216
+ * @param {Object} data - Message data
217
+ */
218
+ handleFileChange(data) {
219
+ console.log('📁 File change detected:', data.data);
220
+ this.emit('file_change', data.data);
221
+ }
222
+
223
+ /**
224
+ * Handle process change notification
225
+ * @param {Object} data - Message data
226
+ */
227
+ handleProcessChange(data) {
228
+ console.log('⚡ Process change detected:', data.data);
229
+ this.emit('process_change', data.data);
230
+ }
231
+
232
+ /**
233
+ * Handle subscription confirmation
234
+ * @param {Object} data - Message data
235
+ */
236
+ handleSubscriptionConfirmation(data) {
237
+ console.log(`📡 Subscription ${data.type}:`, data.data.channel);
238
+ this.emit('subscription_change', data.data);
239
+ }
240
+
241
+ /**
242
+ * Handle pong response
243
+ * @param {Object} data - Message data
244
+ */
245
+ handlePong(data) {
246
+ if (this.heartbeatTimeout) {
247
+ clearTimeout(this.heartbeatTimeout);
248
+ this.heartbeatTimeout = null;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Handle response to pending message
254
+ * @param {Object} data - Response data
255
+ */
256
+ handleMessageResponse(data) {
257
+ const pending = this.pendingMessages.get(data.messageId);
258
+ if (pending) {
259
+ this.pendingMessages.delete(data.messageId);
260
+ if (pending.resolve) {
261
+ pending.resolve(data);
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Send message to server
268
+ * @param {Object} message - Message to send
269
+ * @param {boolean} expectResponse - Whether to expect a response
270
+ * @returns {Promise} Promise that resolves with response (if expected)
271
+ */
272
+ send(message, expectResponse = false) {
273
+ if (!this.isConnected) {
274
+ // Queue message for later
275
+ this.messageQueue.push({ message, expectResponse });
276
+ console.log('📦 Message queued (not connected):', message.type);
277
+ return Promise.resolve();
278
+ }
279
+
280
+ const messageWithId = {
281
+ ...message,
282
+ messageId: expectResponse ? this.generateMessageId() : undefined,
283
+ timestamp: Date.now()
284
+ };
285
+
286
+ if (expectResponse) {
287
+ return new Promise((resolve, reject) => {
288
+ this.pendingMessages.set(messageWithId.messageId, { resolve, reject });
289
+
290
+ // Set timeout for response
291
+ setTimeout(() => {
292
+ if (this.pendingMessages.has(messageWithId.messageId)) {
293
+ this.pendingMessages.delete(messageWithId.messageId);
294
+ reject(new Error('WebSocket message timeout'));
295
+ }
296
+ }, 10000); // 10 second timeout
297
+
298
+ this.ws.send(JSON.stringify(messageWithId));
299
+ });
300
+ } else {
301
+ this.ws.send(JSON.stringify(messageWithId));
302
+ return Promise.resolve();
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Subscribe to a channel
308
+ * @param {string} channel - Channel name
309
+ */
310
+ subscribe(channel) {
311
+ this.subscriptions.add(channel);
312
+ return this.send({
313
+ type: 'subscribe',
314
+ channel
315
+ });
316
+ }
317
+
318
+ /**
319
+ * Unsubscribe from a channel
320
+ * @param {string} channel - Channel name
321
+ */
322
+ unsubscribe(channel) {
323
+ this.subscriptions.delete(channel);
324
+ return this.send({
325
+ type: 'unsubscribe',
326
+ channel
327
+ });
328
+ }
329
+
330
+ /**
331
+ * Request data refresh
332
+ */
333
+ requestRefresh() {
334
+ return this.send({
335
+ type: 'refresh_request'
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Send ping to server
341
+ */
342
+ ping() {
343
+ return this.send({
344
+ type: 'ping'
345
+ });
346
+ }
347
+
348
+ /**
349
+ * Add event listener
350
+ * @param {string} event - Event name
351
+ * @param {Function} callback - Callback function
352
+ * @returns {Function} Unsubscribe function
353
+ */
354
+ on(event, callback) {
355
+ if (!this.eventListeners.has(event)) {
356
+ this.eventListeners.set(event, new Set());
357
+ }
358
+ this.eventListeners.get(event).add(callback);
359
+
360
+ // Return unsubscribe function
361
+ return () => {
362
+ const eventCallbacks = this.eventListeners.get(event);
363
+ if (eventCallbacks) {
364
+ eventCallbacks.delete(callback);
365
+ if (eventCallbacks.size === 0) {
366
+ this.eventListeners.delete(event);
367
+ }
368
+ }
369
+ };
370
+ }
371
+
372
+ /**
373
+ * Emit event to listeners
374
+ * @param {string} event - Event name
375
+ * @param {*} data - Event data
376
+ */
377
+ emit(event, data) {
378
+ const eventCallbacks = this.eventListeners.get(event);
379
+ if (eventCallbacks) {
380
+ eventCallbacks.forEach(callback => {
381
+ try {
382
+ callback(data);
383
+ } catch (error) {
384
+ console.error(`Error in WebSocket event listener for ${event}:`, error);
385
+ }
386
+ });
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Process queued messages
392
+ */
393
+ processMessageQueue() {
394
+ while (this.messageQueue.length > 0 && this.isConnected) {
395
+ const { message, expectResponse } = this.messageQueue.shift();
396
+ this.send(message, expectResponse);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Re-subscribe to channels after reconnection
402
+ */
403
+ resubscribeToChannels() {
404
+ this.subscriptions.forEach(channel => {
405
+ this.send({
406
+ type: 'subscribe',
407
+ channel
408
+ });
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Start heartbeat mechanism
414
+ */
415
+ startHeartbeat() {
416
+ this.heartbeatInterval = setInterval(() => {
417
+ if (this.isConnected) {
418
+ this.ping();
419
+
420
+ // Set timeout for pong response
421
+ this.heartbeatTimeout = setTimeout(() => {
422
+ console.warn('💔 Heartbeat timeout - closing connection');
423
+ this.ws.close();
424
+ }, 5000); // 5 second timeout for pong
425
+ }
426
+ }, 30000); // Send ping every 30 seconds
427
+ }
428
+
429
+ /**
430
+ * Stop heartbeat mechanism
431
+ */
432
+ stopHeartbeat() {
433
+ if (this.heartbeatInterval) {
434
+ clearInterval(this.heartbeatInterval);
435
+ this.heartbeatInterval = null;
436
+ }
437
+ if (this.heartbeatTimeout) {
438
+ clearTimeout(this.heartbeatTimeout);
439
+ this.heartbeatTimeout = null;
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Schedule reconnection attempt
445
+ */
446
+ scheduleReconnect() {
447
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
448
+ console.error('❌ Max reconnection attempts reached');
449
+ this.emit('max_reconnects_reached');
450
+ return;
451
+ }
452
+
453
+ const delay = Math.min(
454
+ this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
455
+ this.maxReconnectDelay
456
+ );
457
+
458
+ console.log(`🔄 Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`);
459
+
460
+ setTimeout(() => {
461
+ this.reconnectAttempts++;
462
+ this.connect(this.url).catch(() => {
463
+ // Reconnection failed, will be handled by scheduleReconnect again
464
+ });
465
+ }, delay);
466
+ }
467
+
468
+ /**
469
+ * Generate unique message ID
470
+ * @returns {string} Message ID
471
+ */
472
+ generateMessageId() {
473
+ return `msg_${++this.messageId}_${Date.now()}`;
474
+ }
475
+
476
+ /**
477
+ * Disconnect WebSocket
478
+ */
479
+ disconnect() {
480
+ this.autoReconnect = false;
481
+ if (this.ws) {
482
+ this.ws.close(1000, 'Client disconnect');
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Get connection status
488
+ * @returns {Object} Connection status
489
+ */
490
+ getStatus() {
491
+ return {
492
+ isConnected: this.isConnected,
493
+ url: this.url,
494
+ clientId: this.clientId,
495
+ reconnectAttempts: this.reconnectAttempts,
496
+ subscriptions: Array.from(this.subscriptions),
497
+ queuedMessages: this.messageQueue.length,
498
+ pendingMessages: this.pendingMessages.size,
499
+ readyState: this.ws ? this.ws.readyState : WebSocket.CLOSED
500
+ };
501
+ }
502
+
503
+ /**
504
+ * Set auto-reconnect behavior
505
+ * @param {boolean} enabled - Enable auto-reconnect
506
+ */
507
+ setAutoReconnect(enabled) {
508
+ this.autoReconnect = enabled;
509
+ }
510
+
511
+ /**
512
+ * Clear message queue
513
+ */
514
+ clearMessageQueue() {
515
+ this.messageQueue = [];
516
+ console.log('🗑️ Message queue cleared');
517
+ }
518
+ }
519
+
520
+ // Export for module use
521
+ if (typeof module !== 'undefined' && module.exports) {
522
+ module.exports = WebSocketService;
523
+ }