sehawq.db 4.0.3 → 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.
- package/.github/workflows/npm-publish.yml +30 -30
- package/LICENSE +21 -21
- package/index.js +1 -1
- package/package.json +36 -36
- package/readme.md +413 -413
- package/src/core/Database.js +294 -294
- package/src/core/Events.js +285 -285
- package/src/core/IndexManager.js +813 -813
- package/src/core/Persistence.js +375 -375
- package/src/core/QueryEngine.js +447 -447
- package/src/core/Storage.js +321 -321
- package/src/core/Validator.js +324 -324
- package/src/index.js +115 -115
- package/src/performance/Cache.js +338 -338
- package/src/performance/LazyLoader.js +354 -354
- package/src/performance/MemoryManager.js +495 -495
- package/src/server/api.js +687 -687
- package/src/server/websocket.js +527 -527
- package/src/utils/benchmark.js +51 -51
- package/src/utils/dot-notation.js +247 -247
- package/src/utils/helpers.js +275 -275
- package/src/utils/profiler.js +70 -70
- package/src/version.js +37 -37
package/src/server/websocket.js
CHANGED
|
@@ -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;
|