sehawq.db 4.0.2 → 4.0.5

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.
@@ -1,528 +1,528 @@
1
- /**
2
- * WebSocket Server - Real-time magic for your database ✨
3
- *
4
- * Watch data change in real-time across multiple clients
5
- * Because polling is so last decade 😎
6
- */
7
-
8
- const { Server } = require('socket.io');
9
- const { performance } = require('perf_hooks');
10
-
11
- class WebSocketServer {
12
- constructor(server, database, options = {}) {
13
- this.db = database;
14
- this.options = {
15
- corsOrigin: '*',
16
- enableStats: true,
17
- maxClients: 100,
18
- heartbeatInterval: 30000,
19
- ...options
20
- };
21
-
22
- // Initialize Socket.IO
23
- this.io = new Server(server, {
24
- cors: {
25
- origin: this.options.corsOrigin,
26
- methods: ['GET', 'POST']
27
- }
28
- });
29
-
30
- this.clients = new Map(); // socket.id -> client info
31
- this.rooms = new Map(); // room -> Set of socket ids
32
-
33
- this.stats = {
34
- totalConnections: 0,
35
- activeConnections: 0,
36
- messagesSent: 0,
37
- messagesReceived: 0,
38
- errors: 0,
39
- rooms: 0
40
- };
41
-
42
- this._setupEventHandlers();
43
- this._startHeartbeat();
44
-
45
- console.log('🔌 WebSocket server initialized - Real-time sync ready');
46
- }
47
-
48
- /**
49
- * Setup Socket.IO event handlers
50
- */
51
- _setupEventHandlers() {
52
- this.io.on('connection', (socket) => {
53
- this._handleConnection(socket);
54
- });
55
-
56
- // Listen to database events for real-time updates
57
- this._setupDatabaseEventListeners();
58
- }
59
-
60
- /**
61
- * Handle new client connection
62
- */
63
- _handleConnection(socket) {
64
- const clientInfo = {
65
- id: socket.id,
66
- ip: socket.handshake.address,
67
- connectedAt: Date.now(),
68
- lastActivity: Date.now(),
69
- rooms: new Set()
70
- };
71
-
72
- this.clients.set(socket.id, clientInfo);
73
- this.stats.totalConnections++;
74
- this.stats.activeConnections++;
75
-
76
- console.log(`🔗 Client connected: ${socket.id} from ${clientInfo.ip}`);
77
-
78
- // Send initial data snapshot
79
- this._sendInitialData(socket);
80
-
81
- // Setup client event handlers
82
- this._setupClientHandlers(socket);
83
-
84
- // Emit connection event
85
- this.db.emit('client:connected', { socketId: socket.id, ip: clientInfo.ip });
86
- }
87
-
88
- /**
89
- * Send initial data to newly connected client
90
- */
91
- _sendInitialData(socket) {
92
- try {
93
- const data = this.db.all();
94
- socket.emit('data:init', {
95
- type: 'init',
96
- data: data,
97
- timestamp: Date.now()
98
- });
99
-
100
- if (this.options.debug) {
101
- console.log(`📤 Sent initial data to ${socket.id} (${Object.keys(data).length} records)`);
102
- }
103
- } catch (error) {
104
- console.error('🚨 Failed to send initial data:', error);
105
- socket.emit('error', {
106
- type: 'init_failed',
107
- message: 'Failed to load initial data'
108
- });
109
- }
110
- }
111
-
112
- /**
113
- * Setup event handlers for a client socket
114
- */
115
- _setupClientHandlers(socket) {
116
- // Join room
117
- socket.on('join:room', (room) => {
118
- this._handleJoinRoom(socket, room);
119
- });
120
-
121
- // Leave room
122
- socket.on('leave:room', (room) => {
123
- this._handleLeaveRoom(socket, room);
124
- });
125
-
126
- // Subscribe to key changes
127
- socket.on('subscribe:key', (key) => {
128
- this._handleSubscribeKey(socket, key);
129
- });
130
-
131
- // Unsubscribe from key
132
- socket.on('unsubscribe:key', (key) => {
133
- this._handleUnsubscribeKey(socket, key);
134
- });
135
-
136
- // Custom message
137
- socket.on('message', (data) => {
138
- this._handleCustomMessage(socket, data);
139
- });
140
-
141
- // Ping from client
142
- socket.on('ping', () => {
143
- socket.emit('pong', { timestamp: Date.now() });
144
- });
145
-
146
- // Disconnect
147
- socket.on('disconnect', (reason) => {
148
- this._handleDisconnect(socket, reason);
149
- });
150
-
151
- // Error handling
152
- socket.on('error', (error) => {
153
- this._handleClientError(socket, error);
154
- });
155
- }
156
-
157
- /**
158
- * Setup database event listeners for real-time updates
159
- */
160
- _setupDatabaseEventListeners() {
161
- // Database set operation
162
- this.db.on('set', ({ key, value, oldValue }) => {
163
- this._broadcastDataChange('set', key, value, oldValue);
164
- });
165
-
166
- // Database delete operation
167
- this.db.on('delete', ({ key, oldValue }) => {
168
- this._broadcastDataChange('delete', key, null, oldValue);
169
- });
170
-
171
- // Database clear operation
172
- this.db.on('clear', ({ size }) => {
173
- this._broadcastToAll('data:changed', {
174
- action: 'clear',
175
- timestamp: Date.now(),
176
- affectedRecords: size
177
- });
178
- });
179
-
180
- // Array push operation
181
- this.db.on('push', ({ key, value }) => {
182
- this._broadcastDataChange('push', key, value);
183
- });
184
-
185
- // Array pull operation
186
- this.db.on('pull', ({ key, value }) => {
187
- this._broadcastDataChange('pull', key, value);
188
- });
189
-
190
- // Math operations
191
- this.db.on('add', ({ key, number }) => {
192
- this._broadcastDataChange('add', key, number);
193
- });
194
-
195
- // Backup events
196
- this.db.on('backup', ({ backupPath }) => {
197
- this._broadcastToAll('system:backup', {
198
- action: 'backup',
199
- path: backupPath,
200
- timestamp: Date.now()
201
- });
202
- });
203
- }
204
-
205
- /**
206
- * Broadcast data change to all interested clients
207
- */
208
- _broadcastDataChange(action, key, value, oldValue = null) {
209
- const changeEvent = {
210
- action,
211
- key,
212
- value,
213
- oldValue,
214
- timestamp: Date.now()
215
- };
216
-
217
- // Broadcast to all clients in the key-specific room
218
- this._broadcastToRoom(`key:${key}`, 'data:changed', changeEvent);
219
-
220
- // Also broadcast to general data room
221
- this._broadcastToRoom('data', 'data:changed', changeEvent);
222
-
223
- // Update stats
224
- this.stats.messagesSent += this._getRoomSize(`key:${key}`) + this._getRoomSize('data');
225
-
226
- if (this.options.debug) {
227
- console.log(`📢 Broadcast ${action} on ${key} to ${this._getRoomSize(`key:${key}`)} clients`);
228
- }
229
- }
230
-
231
- /**
232
- * Handle client joining a room
233
- */
234
- _handleJoinRoom(socket, room) {
235
- if (!room || typeof room !== 'string') {
236
- socket.emit('error', { message: 'Invalid room name' });
237
- return;
238
- }
239
-
240
- socket.join(room);
241
-
242
- const client = this.clients.get(socket.id);
243
- if (client) {
244
- client.rooms.add(room);
245
- client.lastActivity = Date.now();
246
- }
247
-
248
- // Initialize room tracking
249
- if (!this.rooms.has(room)) {
250
- this.rooms.set(room, new Set());
251
- this.stats.rooms++;
252
- }
253
- this.rooms.get(room).add(socket.id);
254
-
255
- socket.emit('room:joined', { room });
256
-
257
- if (this.options.debug) {
258
- console.log(`🚪 Client ${socket.id} joined room: ${room}`);
259
- }
260
- }
261
-
262
- /**
263
- * Handle client leaving a room
264
- */
265
- _handleLeaveRoom(socket, room) {
266
- socket.leave(room);
267
-
268
- const client = this.clients.get(socket.id);
269
- if (client) {
270
- client.rooms.delete(room);
271
- client.lastActivity = Date.now();
272
- }
273
-
274
- // Update room tracking
275
- if (this.rooms.has(room)) {
276
- this.rooms.get(room).delete(socket.id);
277
-
278
- // Clean up empty rooms
279
- if (this.rooms.get(room).size === 0) {
280
- this.rooms.delete(room);
281
- this.stats.rooms--;
282
- }
283
- }
284
-
285
- socket.emit('room:left', { room });
286
-
287
- if (this.options.debug) {
288
- console.log(`🚪 Client ${socket.id} left room: ${room}`);
289
- }
290
- }
291
-
292
- /**
293
- * Handle client subscribing to a key
294
- */
295
- _handleSubscribeKey(socket, key) {
296
- if (!key || typeof key !== 'string') {
297
- socket.emit('error', { message: 'Invalid key for subscription' });
298
- return;
299
- }
300
-
301
- const room = `key:${key}`;
302
- this._handleJoinRoom(socket, room);
303
-
304
- // Send current value immediately
305
- const currentValue = this.db.get(key);
306
- socket.emit('subscription:update', {
307
- key,
308
- value: currentValue,
309
- timestamp: Date.now()
310
- });
311
-
312
- if (this.options.debug) {
313
- console.log(`📡 Client ${socket.id} subscribed to key: ${key}`);
314
- }
315
- }
316
-
317
- /**
318
- * Handle client unsubscribing from a key
319
- */
320
- _handleUnsubscribeKey(socket, key) {
321
- const room = `key:${key}`;
322
- this._handleLeaveRoom(socket, room);
323
-
324
- if (this.options.debug) {
325
- console.log(`📡 Client ${socket.id} unsubscribed from key: ${key}`);
326
- }
327
- }
328
-
329
- /**
330
- * Handle custom messages from clients
331
- */
332
- _handleCustomMessage(socket, data) {
333
- this.stats.messagesReceived++;
334
-
335
- const client = this.clients.get(socket.id);
336
- if (client) {
337
- client.lastActivity = Date.now();
338
- }
339
-
340
- // Echo back for testing
341
- if (data.echo) {
342
- socket.emit('message', {
343
- ...data,
344
- echoed: true,
345
- timestamp: Date.now()
346
- });
347
- }
348
-
349
- // Broadcast to room if specified
350
- if (data.room && data.message) {
351
- this._broadcastToRoom(data.room, 'message', {
352
- from: socket.id,
353
- message: data.message,
354
- timestamp: Date.now()
355
- });
356
- }
357
- }
358
-
359
- /**
360
- * Handle client disconnect
361
- */
362
- _handleDisconnect(socket, reason) {
363
- const client = this.clients.get(socket.id);
364
-
365
- if (client) {
366
- // Leave all rooms
367
- for (const room of client.rooms) {
368
- this._handleLeaveRoom(socket, room);
369
- }
370
-
371
- this.clients.delete(socket.id);
372
- this.stats.activeConnections--;
373
-
374
- console.log(`🔌 Client disconnected: ${socket.id} (${reason})`);
375
-
376
- // Emit disconnect event
377
- this.db.emit('client:disconnected', {
378
- socketId: socket.id,
379
- reason: reason,
380
- duration: Date.now() - client.connectedAt
381
- });
382
- }
383
- }
384
-
385
- /**
386
- * Handle client errors
387
- */
388
- _handleClientError(socket, error) {
389
- this.stats.errors++;
390
-
391
- console.error(`🚨 WebSocket client error (${socket.id}):`, error);
392
-
393
- socket.emit('error', {
394
- type: 'client_error',
395
- message: 'An error occurred'
396
- });
397
- }
398
-
399
- /**
400
- * Broadcast to all clients in a room
401
- */
402
- _broadcastToRoom(room, event, data) {
403
- this.io.to(room).emit(event, data);
404
- }
405
-
406
- /**
407
- * Broadcast to all connected clients
408
- */
409
- _broadcastToAll(event, data) {
410
- this.io.emit(event, data);
411
- }
412
-
413
- /**
414
- * Get number of clients in a room
415
- */
416
- _getRoomSize(room) {
417
- const roomSockets = this.io.sockets.adapter.rooms.get(room);
418
- return roomSockets ? roomSockets.size : 0;
419
- }
420
-
421
- /**
422
- * Start heartbeat to detect dead connections
423
- */
424
- _startHeartbeat() {
425
- setInterval(() => {
426
- this._checkHeartbeats();
427
- }, this.options.heartbeatInterval);
428
- }
429
-
430
- /**
431
- * Check client heartbeats and clean up dead connections
432
- */
433
- _checkHeartbeats() {
434
- const now = Date.now();
435
- const maxInactiveTime = this.options.heartbeatInterval * 3; // 3x interval
436
-
437
- for (const [socketId, client] of this.clients.entries()) {
438
- if (now - client.lastActivity > maxInactiveTime) {
439
- console.log(`💀 Disconnecting inactive client: ${socketId}`);
440
-
441
- const socket = this.io.sockets.sockets.get(socketId);
442
- if (socket) {
443
- socket.disconnect(true);
444
- }
445
- }
446
- }
447
- }
448
-
449
- /**
450
- * Get WebSocket server statistics
451
- */
452
- getStats() {
453
- const now = Date.now();
454
- const activeClients = Array.from(this.clients.values()).map(client => ({
455
- id: client.id,
456
- ip: client.ip,
457
- connectedAt: client.connectedAt,
458
- lastActivity: client.lastActivity,
459
- uptime: now - client.connectedAt,
460
- inactiveTime: now - client.lastActivity,
461
- rooms: Array.from(client.rooms)
462
- }));
463
-
464
- return {
465
- ...this.stats,
466
- activeClients: activeClients,
467
- rooms: Array.from(this.rooms.entries()).map(([room, clients]) => ({
468
- room,
469
- clientCount: clients.size,
470
- clients: Array.from(clients)
471
- })),
472
- uptime: process.uptime()
473
- };
474
- }
475
-
476
- /**
477
- * Send message to specific client
478
- */
479
- sendToClient(socketId, event, data) {
480
- const socket = this.io.sockets.sockets.get(socketId);
481
- if (socket) {
482
- socket.emit(event, data);
483
- return true;
484
- }
485
- return false;
486
- }
487
-
488
- /**
489
- * Disconnect specific client
490
- */
491
- disconnectClient(socketId, reason = 'admin_disconnect') {
492
- const socket = this.io.sockets.sockets.get(socketId);
493
- if (socket) {
494
- socket.disconnect(true);
495
- console.log(`🔌 Admin disconnected client: ${socketId} (${reason})`);
496
- return true;
497
- }
498
- return false;
499
- }
500
-
501
- /**
502
- * Get client information
503
- */
504
- getClientInfo(socketId) {
505
- return this.clients.get(socketId);
506
- }
507
-
508
- /**
509
- * Close WebSocket server
510
- */
511
- close() {
512
- // Disconnect all clients gracefully
513
- this._broadcastToAll('system:shutdown', {
514
- message: 'Server is shutting down',
515
- timestamp: Date.now()
516
- });
517
-
518
- // Disconnect all clients after short delay
519
- setTimeout(() => {
520
- this.io.disconnectSockets(true);
521
- this.io.close();
522
- }, 1000);
523
-
524
- console.log('🛑 WebSocket server closed');
525
- }
526
- }
527
-
1
+ /**
2
+ * WebSocket Server - Real-time magic for your database ✨
3
+ *
4
+ * Watch data change in real-time across multiple clients
5
+ * Because polling is so last decade 😎
6
+ */
7
+
8
+ const { Server } = require('socket.io');
9
+ const { performance } = require('perf_hooks');
10
+
11
+ class WebSocketServer {
12
+ constructor(server, database, options = {}) {
13
+ this.db = database;
14
+ this.options = {
15
+ corsOrigin: '*',
16
+ enableStats: true,
17
+ maxClients: 100,
18
+ heartbeatInterval: 30000,
19
+ ...options
20
+ };
21
+
22
+ // Initialize Socket.IO
23
+ this.io = new Server(server, {
24
+ cors: {
25
+ origin: this.options.corsOrigin,
26
+ methods: ['GET', 'POST']
27
+ }
28
+ });
29
+
30
+ this.clients = new Map(); // socket.id -> client info
31
+ this.rooms = new Map(); // room -> Set of socket ids
32
+
33
+ this.stats = {
34
+ totalConnections: 0,
35
+ activeConnections: 0,
36
+ messagesSent: 0,
37
+ messagesReceived: 0,
38
+ errors: 0,
39
+ rooms: 0
40
+ };
41
+
42
+ this._setupEventHandlers();
43
+ this._startHeartbeat();
44
+
45
+ console.log('🔌 WebSocket server initialized - Real-time sync ready');
46
+ }
47
+
48
+ /**
49
+ * Setup Socket.IO event handlers
50
+ */
51
+ _setupEventHandlers() {
52
+ this.io.on('connection', (socket) => {
53
+ this._handleConnection(socket);
54
+ });
55
+
56
+ // Listen to database events for real-time updates
57
+ this._setupDatabaseEventListeners();
58
+ }
59
+
60
+ /**
61
+ * Handle new client connection
62
+ */
63
+ _handleConnection(socket) {
64
+ const clientInfo = {
65
+ id: socket.id,
66
+ ip: socket.handshake.address,
67
+ connectedAt: Date.now(),
68
+ lastActivity: Date.now(),
69
+ rooms: new Set()
70
+ };
71
+
72
+ this.clients.set(socket.id, clientInfo);
73
+ this.stats.totalConnections++;
74
+ this.stats.activeConnections++;
75
+
76
+ console.log(`🔗 Client connected: ${socket.id} from ${clientInfo.ip}`);
77
+
78
+ // Send initial data snapshot
79
+ this._sendInitialData(socket);
80
+
81
+ // Setup client event handlers
82
+ this._setupClientHandlers(socket);
83
+
84
+ // Emit connection event
85
+ this.db.emit('client:connected', { socketId: socket.id, ip: clientInfo.ip });
86
+ }
87
+
88
+ /**
89
+ * Send initial data to newly connected client
90
+ */
91
+ _sendInitialData(socket) {
92
+ try {
93
+ const data = this.db.all();
94
+ socket.emit('data:init', {
95
+ type: 'init',
96
+ data: data,
97
+ timestamp: Date.now()
98
+ });
99
+
100
+ if (this.options.debug) {
101
+ console.log(`📤 Sent initial data to ${socket.id} (${Object.keys(data).length} records)`);
102
+ }
103
+ } catch (error) {
104
+ console.error('🚨 Failed to send initial data:', error);
105
+ socket.emit('error', {
106
+ type: 'init_failed',
107
+ message: 'Failed to load initial data'
108
+ });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Setup event handlers for a client socket
114
+ */
115
+ _setupClientHandlers(socket) {
116
+ // Join room
117
+ socket.on('join:room', (room) => {
118
+ this._handleJoinRoom(socket, room);
119
+ });
120
+
121
+ // Leave room
122
+ socket.on('leave:room', (room) => {
123
+ this._handleLeaveRoom(socket, room);
124
+ });
125
+
126
+ // Subscribe to key changes
127
+ socket.on('subscribe:key', (key) => {
128
+ this._handleSubscribeKey(socket, key);
129
+ });
130
+
131
+ // Unsubscribe from key
132
+ socket.on('unsubscribe:key', (key) => {
133
+ this._handleUnsubscribeKey(socket, key);
134
+ });
135
+
136
+ // Custom message
137
+ socket.on('message', (data) => {
138
+ this._handleCustomMessage(socket, data);
139
+ });
140
+
141
+ // Ping from client
142
+ socket.on('ping', () => {
143
+ socket.emit('pong', { timestamp: Date.now() });
144
+ });
145
+
146
+ // Disconnect
147
+ socket.on('disconnect', (reason) => {
148
+ this._handleDisconnect(socket, reason);
149
+ });
150
+
151
+ // Error handling
152
+ socket.on('error', (error) => {
153
+ this._handleClientError(socket, error);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Setup database event listeners for real-time updates
159
+ */
160
+ _setupDatabaseEventListeners() {
161
+ // Database set operation
162
+ this.db.on('set', ({ key, value, oldValue }) => {
163
+ this._broadcastDataChange('set', key, value, oldValue);
164
+ });
165
+
166
+ // Database delete operation
167
+ this.db.on('delete', ({ key, oldValue }) => {
168
+ this._broadcastDataChange('delete', key, null, oldValue);
169
+ });
170
+
171
+ // Database clear operation
172
+ this.db.on('clear', ({ size }) => {
173
+ this._broadcastToAll('data:changed', {
174
+ action: 'clear',
175
+ timestamp: Date.now(),
176
+ affectedRecords: size
177
+ });
178
+ });
179
+
180
+ // Array push operation
181
+ this.db.on('push', ({ key, value }) => {
182
+ this._broadcastDataChange('push', key, value);
183
+ });
184
+
185
+ // Array pull operation
186
+ this.db.on('pull', ({ key, value }) => {
187
+ this._broadcastDataChange('pull', key, value);
188
+ });
189
+
190
+ // Math operations
191
+ this.db.on('add', ({ key, number }) => {
192
+ this._broadcastDataChange('add', key, number);
193
+ });
194
+
195
+ // Backup events
196
+ this.db.on('backup', ({ backupPath }) => {
197
+ this._broadcastToAll('system:backup', {
198
+ action: 'backup',
199
+ path: backupPath,
200
+ timestamp: Date.now()
201
+ });
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Broadcast data change to all interested clients
207
+ */
208
+ _broadcastDataChange(action, key, value, oldValue = null) {
209
+ const changeEvent = {
210
+ action,
211
+ key,
212
+ value,
213
+ oldValue,
214
+ timestamp: Date.now()
215
+ };
216
+
217
+ // Broadcast to all clients in the key-specific room
218
+ this._broadcastToRoom(`key:${key}`, 'data:changed', changeEvent);
219
+
220
+ // Also broadcast to general data room
221
+ this._broadcastToRoom('data', 'data:changed', changeEvent);
222
+
223
+ // Update stats
224
+ this.stats.messagesSent += this._getRoomSize(`key:${key}`) + this._getRoomSize('data');
225
+
226
+ if (this.options.debug) {
227
+ console.log(`📢 Broadcast ${action} on ${key} to ${this._getRoomSize(`key:${key}`)} clients`);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Handle client joining a room
233
+ */
234
+ _handleJoinRoom(socket, room) {
235
+ if (!room || typeof room !== 'string') {
236
+ socket.emit('error', { message: 'Invalid room name' });
237
+ return;
238
+ }
239
+
240
+ socket.join(room);
241
+
242
+ const client = this.clients.get(socket.id);
243
+ if (client) {
244
+ client.rooms.add(room);
245
+ client.lastActivity = Date.now();
246
+ }
247
+
248
+ // Initialize room tracking
249
+ if (!this.rooms.has(room)) {
250
+ this.rooms.set(room, new Set());
251
+ this.stats.rooms++;
252
+ }
253
+ this.rooms.get(room).add(socket.id);
254
+
255
+ socket.emit('room:joined', { room });
256
+
257
+ if (this.options.debug) {
258
+ console.log(`🚪 Client ${socket.id} joined room: ${room}`);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Handle client leaving a room
264
+ */
265
+ _handleLeaveRoom(socket, room) {
266
+ socket.leave(room);
267
+
268
+ const client = this.clients.get(socket.id);
269
+ if (client) {
270
+ client.rooms.delete(room);
271
+ client.lastActivity = Date.now();
272
+ }
273
+
274
+ // Update room tracking
275
+ if (this.rooms.has(room)) {
276
+ this.rooms.get(room).delete(socket.id);
277
+
278
+ // Clean up empty rooms
279
+ if (this.rooms.get(room).size === 0) {
280
+ this.rooms.delete(room);
281
+ this.stats.rooms--;
282
+ }
283
+ }
284
+
285
+ socket.emit('room:left', { room });
286
+
287
+ if (this.options.debug) {
288
+ console.log(`🚪 Client ${socket.id} left room: ${room}`);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Handle client subscribing to a key
294
+ */
295
+ _handleSubscribeKey(socket, key) {
296
+ if (!key || typeof key !== 'string') {
297
+ socket.emit('error', { message: 'Invalid key for subscription' });
298
+ return;
299
+ }
300
+
301
+ const room = `key:${key}`;
302
+ this._handleJoinRoom(socket, room);
303
+
304
+ // Send current value immediately
305
+ const currentValue = this.db.get(key);
306
+ socket.emit('subscription:update', {
307
+ key,
308
+ value: currentValue,
309
+ timestamp: Date.now()
310
+ });
311
+
312
+ if (this.options.debug) {
313
+ console.log(`📡 Client ${socket.id} subscribed to key: ${key}`);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Handle client unsubscribing from a key
319
+ */
320
+ _handleUnsubscribeKey(socket, key) {
321
+ const room = `key:${key}`;
322
+ this._handleLeaveRoom(socket, room);
323
+
324
+ if (this.options.debug) {
325
+ console.log(`📡 Client ${socket.id} unsubscribed from key: ${key}`);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Handle custom messages from clients
331
+ */
332
+ _handleCustomMessage(socket, data) {
333
+ this.stats.messagesReceived++;
334
+
335
+ const client = this.clients.get(socket.id);
336
+ if (client) {
337
+ client.lastActivity = Date.now();
338
+ }
339
+
340
+ // Echo back for testing
341
+ if (data.echo) {
342
+ socket.emit('message', {
343
+ ...data,
344
+ echoed: true,
345
+ timestamp: Date.now()
346
+ });
347
+ }
348
+
349
+ // Broadcast to room if specified
350
+ if (data.room && data.message) {
351
+ this._broadcastToRoom(data.room, 'message', {
352
+ from: socket.id,
353
+ message: data.message,
354
+ timestamp: Date.now()
355
+ });
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Handle client disconnect
361
+ */
362
+ _handleDisconnect(socket, reason) {
363
+ const client = this.clients.get(socket.id);
364
+
365
+ if (client) {
366
+ // Leave all rooms
367
+ for (const room of client.rooms) {
368
+ this._handleLeaveRoom(socket, room);
369
+ }
370
+
371
+ this.clients.delete(socket.id);
372
+ this.stats.activeConnections--;
373
+
374
+ console.log(`🔌 Client disconnected: ${socket.id} (${reason})`);
375
+
376
+ // Emit disconnect event
377
+ this.db.emit('client:disconnected', {
378
+ socketId: socket.id,
379
+ reason: reason,
380
+ duration: Date.now() - client.connectedAt
381
+ });
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Handle client errors
387
+ */
388
+ _handleClientError(socket, error) {
389
+ this.stats.errors++;
390
+
391
+ console.error(`🚨 WebSocket client error (${socket.id}):`, error);
392
+
393
+ socket.emit('error', {
394
+ type: 'client_error',
395
+ message: 'An error occurred'
396
+ });
397
+ }
398
+
399
+ /**
400
+ * Broadcast to all clients in a room
401
+ */
402
+ _broadcastToRoom(room, event, data) {
403
+ this.io.to(room).emit(event, data);
404
+ }
405
+
406
+ /**
407
+ * Broadcast to all connected clients
408
+ */
409
+ _broadcastToAll(event, data) {
410
+ this.io.emit(event, data);
411
+ }
412
+
413
+ /**
414
+ * Get number of clients in a room
415
+ */
416
+ _getRoomSize(room) {
417
+ const roomSockets = this.io.sockets.adapter.rooms.get(room);
418
+ return roomSockets ? roomSockets.size : 0;
419
+ }
420
+
421
+ /**
422
+ * Start heartbeat to detect dead connections
423
+ */
424
+ _startHeartbeat() {
425
+ setInterval(() => {
426
+ this._checkHeartbeats();
427
+ }, this.options.heartbeatInterval);
428
+ }
429
+
430
+ /**
431
+ * Check client heartbeats and clean up dead connections
432
+ */
433
+ _checkHeartbeats() {
434
+ const now = Date.now();
435
+ const maxInactiveTime = this.options.heartbeatInterval * 3; // 3x interval
436
+
437
+ for (const [socketId, client] of this.clients.entries()) {
438
+ if (now - client.lastActivity > maxInactiveTime) {
439
+ console.log(`💀 Disconnecting inactive client: ${socketId}`);
440
+
441
+ const socket = this.io.sockets.sockets.get(socketId);
442
+ if (socket) {
443
+ socket.disconnect(true);
444
+ }
445
+ }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Get WebSocket server statistics
451
+ */
452
+ getStats() {
453
+ const now = Date.now();
454
+ const activeClients = Array.from(this.clients.values()).map(client => ({
455
+ id: client.id,
456
+ ip: client.ip,
457
+ connectedAt: client.connectedAt,
458
+ lastActivity: client.lastActivity,
459
+ uptime: now - client.connectedAt,
460
+ inactiveTime: now - client.lastActivity,
461
+ rooms: Array.from(client.rooms)
462
+ }));
463
+
464
+ return {
465
+ ...this.stats,
466
+ activeClients: activeClients,
467
+ rooms: Array.from(this.rooms.entries()).map(([room, clients]) => ({
468
+ room,
469
+ clientCount: clients.size,
470
+ clients: Array.from(clients)
471
+ })),
472
+ uptime: process.uptime()
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Send message to specific client
478
+ */
479
+ sendToClient(socketId, event, data) {
480
+ const socket = this.io.sockets.sockets.get(socketId);
481
+ if (socket) {
482
+ socket.emit(event, data);
483
+ return true;
484
+ }
485
+ return false;
486
+ }
487
+
488
+ /**
489
+ * Disconnect specific client
490
+ */
491
+ disconnectClient(socketId, reason = 'admin_disconnect') {
492
+ const socket = this.io.sockets.sockets.get(socketId);
493
+ if (socket) {
494
+ socket.disconnect(true);
495
+ console.log(`🔌 Admin disconnected client: ${socketId} (${reason})`);
496
+ return true;
497
+ }
498
+ return false;
499
+ }
500
+
501
+ /**
502
+ * Get client information
503
+ */
504
+ getClientInfo(socketId) {
505
+ return this.clients.get(socketId);
506
+ }
507
+
508
+ /**
509
+ * Close WebSocket server
510
+ */
511
+ close() {
512
+ // Disconnect all clients gracefully
513
+ this._broadcastToAll('system:shutdown', {
514
+ message: 'Server is shutting down',
515
+ timestamp: Date.now()
516
+ });
517
+
518
+ // Disconnect all clients after short delay
519
+ setTimeout(() => {
520
+ this.io.disconnectSockets(true);
521
+ this.io.close();
522
+ }, 1000);
523
+
524
+ console.log('🛑 WebSocket server closed');
525
+ }
526
+ }
527
+
528
528
  module.exports = WebSocketServer;