dolphin-server-modules 1.5.6 → 1.5.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/DOLPHIN_MASTER_GUIDE_NEPALI.md +491 -210
- package/dist/realtime/core.d.ts +143 -2
- package/dist/realtime/core.js +511 -115
- package/dist/realtime/core.js.map +1 -1
- package/dist/realtime/devicemanager.d.ts +0 -0
- package/dist/realtime/devicemanager.js +2 -0
- package/dist/realtime/devicemanager.js.map +1 -0
- package/dist/realtime/realtime.test.js +436 -49
- package/dist/realtime/realtime.test.js.map +1 -1
- package/package.json +1 -1
package/dist/realtime/core.js
CHANGED
|
@@ -33,13 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.RealtimeCore = void 0;
|
|
36
|
+
exports.toBase64 = exports.toBuffer = exports.djson = exports.getSize = exports.decode = exports.encode = exports.TopicTrie = exports.RealtimeCore = void 0;
|
|
37
37
|
const events_1 = require("events");
|
|
38
38
|
const trie_1 = require("./trie");
|
|
39
39
|
const codec_1 = require("./codec");
|
|
40
40
|
const djson_1 = require("../djson/djson");
|
|
41
41
|
/**
|
|
42
|
-
* RealtimeCore - High performance unified pub/sub bus for Dolphin
|
|
42
|
+
* RealtimeCore v2.0 - High performance unified pub/sub bus for Dolphin
|
|
43
|
+
* Added Features: pubPush, subPull, pubFile, subFile, Resume, P2P Stream
|
|
43
44
|
*/
|
|
44
45
|
class RealtimeCore extends events_1.EventEmitter {
|
|
45
46
|
config;
|
|
@@ -49,6 +50,16 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
49
50
|
plugins = new Map();
|
|
50
51
|
pending = new Map();
|
|
51
52
|
msgId = 0;
|
|
53
|
+
// High-Frequency Buffers (pubPush/subPull)
|
|
54
|
+
highFreqBuffers = new Map();
|
|
55
|
+
MAX_BUFFER_SIZE = 100; // Lightweight: only 100 items per topic
|
|
56
|
+
// File Transfer Registry (pubFile/subFile)
|
|
57
|
+
fileRegistry = new Map();
|
|
58
|
+
fileProgress = new Map(); // deviceId -> fileId -> lastChunk
|
|
59
|
+
DEFAULT_CHUNK_SIZE = 64 * 1024; // 64KB chunks (Lightweight)
|
|
60
|
+
// P2P Peer Registry
|
|
61
|
+
peerRegistry = new Map(); // fileId -> Set<deviceId>
|
|
62
|
+
// JSON Cache (existing)
|
|
52
63
|
jsonCache = new Map();
|
|
53
64
|
CACHE_TTL = 5000;
|
|
54
65
|
MAX_CACHE_SIZE = 100;
|
|
@@ -57,6 +68,7 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
57
68
|
// Cleanup intervals
|
|
58
69
|
cleanupInterval;
|
|
59
70
|
cacheCleanupInterval;
|
|
71
|
+
bufferCleanupInterval;
|
|
60
72
|
constructor(config = {}) {
|
|
61
73
|
super();
|
|
62
74
|
this.config = config;
|
|
@@ -67,10 +79,12 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
67
79
|
if (config.enableJSONCache) {
|
|
68
80
|
this.cacheCleanupInterval = setInterval(() => this.cleanJSONCache(), 10000);
|
|
69
81
|
}
|
|
82
|
+
// v2.0: Buffer cleanup
|
|
83
|
+
this.bufferCleanupInterval = setInterval(() => this.cleanupBuffers(), 30000);
|
|
70
84
|
}
|
|
71
85
|
log(...args) {
|
|
72
86
|
if (this.config.debug) {
|
|
73
|
-
console.log('[RealtimeCore]', ...args);
|
|
87
|
+
console.log('[RealtimeCore v2]', ...args);
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
90
|
toJSON(data, skipCache = false) {
|
|
@@ -117,6 +131,20 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
}
|
|
134
|
+
cleanupBuffers() {
|
|
135
|
+
// Remove old buffers that haven't been accessed
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
for (const [topic, buffer] of this.highFreqBuffers.entries()) {
|
|
138
|
+
// Remove messages older than 5 minutes
|
|
139
|
+
const freshBuffer = buffer.filter(msg => now - msg.ts < 300000);
|
|
140
|
+
if (freshBuffer.length === 0) {
|
|
141
|
+
this.highFreqBuffers.delete(topic);
|
|
142
|
+
}
|
|
143
|
+
else if (freshBuffer.length !== buffer.length) {
|
|
144
|
+
this.highFreqBuffers.set(topic, freshBuffer);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
120
148
|
async initRedis(url) {
|
|
121
149
|
try {
|
|
122
150
|
const Redis = (await Promise.resolve().then(() => __importStar(require('ioredis')))).default;
|
|
@@ -147,9 +175,13 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
147
175
|
throw new Error('ACL deny');
|
|
148
176
|
}
|
|
149
177
|
this.trie.add(topic, fn);
|
|
178
|
+
// BUG FIX #1: Use TopicTrie matching for retained messages instead of exact string compare.
|
|
179
|
+
// Previously `t === topic` only matched exact topics, breaking wildcard subscriptions like
|
|
180
|
+
// 'sensors/+' or 'sensors/#' which would never receive retained messages.
|
|
150
181
|
for (const [t, data] of this.retained.entries()) {
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
const tempTrie = new trie_1.TopicTrie();
|
|
183
|
+
tempTrie.add(topic, fn);
|
|
184
|
+
tempTrie.match(t, (matchedFn) => matchedFn(data.payload));
|
|
153
185
|
}
|
|
154
186
|
}
|
|
155
187
|
publish(topic, payload, opts = {}, deviceId) {
|
|
@@ -173,10 +205,396 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
173
205
|
this.redisPub.publish('dolphin-rt', jsonMessage);
|
|
174
206
|
}
|
|
175
207
|
}
|
|
208
|
+
// ============================================
|
|
209
|
+
// v2.0 NEW: High-Frequency Methods
|
|
210
|
+
// ============================================
|
|
211
|
+
/**
|
|
212
|
+
* pubPush: अति उच्च गतिको डाटाको लागि (IoT Sensors, Live Graphs)
|
|
213
|
+
* - No JSON.stringify, No Redis, No ACL चेक
|
|
214
|
+
* - सिधै Trie मा भएका Subscribers लाई पठाउने
|
|
215
|
+
* - Memory-efficient: सीमित मात्र बफर राख्छ
|
|
216
|
+
*/
|
|
217
|
+
pubPush(topic, payload) {
|
|
218
|
+
let buffer;
|
|
219
|
+
let isBinary = false;
|
|
220
|
+
if (Buffer.isBuffer(payload) || payload instanceof Uint8Array) {
|
|
221
|
+
buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
222
|
+
isBinary = true;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
buffer = Buffer.from(this.toJSON(payload, true));
|
|
226
|
+
}
|
|
227
|
+
// सिधै टपिक मिल्ने सबैलाई पठाउने (Zero overhead)
|
|
228
|
+
this.trie.match(topic, (fn) => {
|
|
229
|
+
fn(buffer);
|
|
230
|
+
});
|
|
231
|
+
// वैकल्पिक: पछि subPull को लागि बफरमा राख्ने
|
|
232
|
+
const maxBuffer = this.config.maxBufferPerTopic || this.MAX_BUFFER_SIZE;
|
|
233
|
+
if (maxBuffer > 0) {
|
|
234
|
+
let topicBuffer = this.highFreqBuffers.get(topic) || [];
|
|
235
|
+
topicBuffer.push({ data: isBinary ? buffer : payload, ts: Date.now(), isBinary });
|
|
236
|
+
if (topicBuffer.length > maxBuffer) {
|
|
237
|
+
topicBuffer.shift();
|
|
238
|
+
}
|
|
239
|
+
this.highFreqBuffers.set(topic, topicBuffer);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* subPull: क्लाइन्टले मागेपछि मात्र डाटा दिने (Data Saving)
|
|
244
|
+
* @param deviceId - कसलाई पठाउने
|
|
245
|
+
* @param topic - कुन टपिकको डाटा चाहियो
|
|
246
|
+
* @param count - कति वटा पछिल्ला डाटा चाहियो (default: 10)
|
|
247
|
+
*/
|
|
248
|
+
subPull(deviceId, topic, count = 10) {
|
|
249
|
+
const buffer = this.highFreqBuffers.get(topic);
|
|
250
|
+
if (!buffer || buffer.length === 0) {
|
|
251
|
+
this.sendTo(deviceId, {
|
|
252
|
+
type: 'PULL_EMPTY',
|
|
253
|
+
topic,
|
|
254
|
+
message: 'No data available',
|
|
255
|
+
timestamp: Date.now()
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// पछिल्लो 'count' वटा डाटा मात्र लिने
|
|
260
|
+
const lastData = buffer.slice(-Math.min(count, buffer.length));
|
|
261
|
+
this.sendTo(deviceId, {
|
|
262
|
+
type: 'PULL_RESPONSE',
|
|
263
|
+
topic,
|
|
264
|
+
count: lastData.length,
|
|
265
|
+
totalAvailable: buffer.length,
|
|
266
|
+
payload: lastData.map(d => d.data),
|
|
267
|
+
serverTime: Date.now()
|
|
268
|
+
});
|
|
269
|
+
this.log(`subPull: ${deviceId} pulled ${lastData.length} items from ${topic}`);
|
|
270
|
+
}
|
|
271
|
+
// ============================================
|
|
272
|
+
// v2.0 NEW: File Transfer Methods
|
|
273
|
+
// ============================================
|
|
274
|
+
/**
|
|
275
|
+
* pubFile: ठूलो फाइललाई टुक्रा-टुक्रा (Chunks) मा पठाउन तयार गर्ने
|
|
276
|
+
* - फाइललाई पूरै मेमोरीमा नराखी 'Stream' तयार पार्ने
|
|
277
|
+
* - हरेक टुक्रा 64KB (हल्का)
|
|
278
|
+
*/
|
|
279
|
+
pubFile(fileId, filePath, chunkSize) {
|
|
280
|
+
const fs = require('fs');
|
|
281
|
+
if (!fs.existsSync(filePath)) {
|
|
282
|
+
this.log(`pubFile: File not found - ${filePath}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
const stats = fs.statSync(filePath);
|
|
286
|
+
const finalChunkSize = chunkSize || this.config.defaultChunkSize || this.DEFAULT_CHUNK_SIZE;
|
|
287
|
+
// FIX: Extract just the filename, not the full path (supports both Windows and Unix paths)
|
|
288
|
+
const filename = filePath.split(/[/\\]/).pop() || filePath;
|
|
289
|
+
const metadata = {
|
|
290
|
+
path: filePath,
|
|
291
|
+
size: stats.size,
|
|
292
|
+
chunkSize: finalChunkSize,
|
|
293
|
+
totalChunks: Math.ceil(stats.size / finalChunkSize),
|
|
294
|
+
name: filename, // Use just the filename, not the full path
|
|
295
|
+
createdAt: Date.now()
|
|
296
|
+
};
|
|
297
|
+
this.fileRegistry.set(fileId, metadata);
|
|
298
|
+
// सबै अनलाइन डिभाइसलाई खबर गर्ने
|
|
299
|
+
this.publish('file/announce', {
|
|
300
|
+
type: 'FILE_AVAILABLE',
|
|
301
|
+
fileId,
|
|
302
|
+
name: metadata.name,
|
|
303
|
+
size: metadata.size,
|
|
304
|
+
totalChunks: metadata.totalChunks,
|
|
305
|
+
chunkSize: metadata.chunkSize,
|
|
306
|
+
timestamp: Date.now()
|
|
307
|
+
}, { retain: true });
|
|
308
|
+
this.log(`pubFile: Registered ${fileId} - ${metadata.name} (${metadata.totalChunks} chunks)`);
|
|
309
|
+
return metadata;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* subFile: फाइलको निश्चित टुक्रा (Chunk) तान्ने - Resume Support सहित
|
|
313
|
+
* @param deviceId - कसलाई पठाउने
|
|
314
|
+
* @param fileId - कुन फाइल
|
|
315
|
+
* @param startChunk - कुन Chunk बाट सुरु गर्ने (Resume को लागि)
|
|
316
|
+
*/
|
|
317
|
+
async subFile(deviceId, fileId, startChunk = 0) {
|
|
318
|
+
const file = this.fileRegistry.get(fileId);
|
|
319
|
+
if (!file) {
|
|
320
|
+
this.sendTo(deviceId, {
|
|
321
|
+
type: 'FILE_ERROR',
|
|
322
|
+
fileId,
|
|
323
|
+
error: 'File not found',
|
|
324
|
+
timestamp: Date.now()
|
|
325
|
+
});
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const fs = require('fs');
|
|
329
|
+
if (!fs.existsSync(file.path)) {
|
|
330
|
+
this.sendTo(deviceId, {
|
|
331
|
+
type: 'FILE_ERROR',
|
|
332
|
+
fileId,
|
|
333
|
+
error: 'File no longer exists on server',
|
|
334
|
+
timestamp: Date.now()
|
|
335
|
+
});
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
// यदि startChunk अन्तिम Chunk भन्दा बढी छ भने
|
|
339
|
+
if (startChunk >= file.totalChunks) {
|
|
340
|
+
this.sendTo(deviceId, {
|
|
341
|
+
type: 'FILE_COMPLETE',
|
|
342
|
+
fileId,
|
|
343
|
+
totalChunks: file.totalChunks,
|
|
344
|
+
size: file.size,
|
|
345
|
+
timestamp: Date.now()
|
|
346
|
+
});
|
|
347
|
+
this.emit('file:complete', { fileId, deviceId });
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const fd = fs.openSync(file.path, 'r');
|
|
352
|
+
const buffer = Buffer.alloc(file.chunkSize);
|
|
353
|
+
// निश्चित स्थानबाट डाटा पढ्ने (Seek)
|
|
354
|
+
const offset = startChunk * file.chunkSize;
|
|
355
|
+
const bytesRead = fs.readSync(fd, buffer, 0, file.chunkSize, offset);
|
|
356
|
+
fs.closeSync(fd);
|
|
357
|
+
const isLast = startChunk === file.totalChunks - 1;
|
|
358
|
+
const finalData = bytesRead < file.chunkSize ? buffer.slice(0, bytesRead) : buffer;
|
|
359
|
+
this.sendTo(deviceId, {
|
|
360
|
+
type: 'FILE_CHUNK',
|
|
361
|
+
fileId,
|
|
362
|
+
name: file.name,
|
|
363
|
+
chunkIndex: startChunk,
|
|
364
|
+
totalChunks: file.totalChunks,
|
|
365
|
+
offset: offset,
|
|
366
|
+
size: bytesRead,
|
|
367
|
+
data: this.config.useBinaryProtocol ? finalData : finalData.toString('base64'),
|
|
368
|
+
isLast,
|
|
369
|
+
nextChunk: isLast ? null : startChunk + 1,
|
|
370
|
+
timestamp: Date.now()
|
|
371
|
+
});
|
|
372
|
+
// प्रगति सेभ गर्ने (Resume को लागि)
|
|
373
|
+
this.saveFileProgress(deviceId, fileId, startChunk);
|
|
374
|
+
// फाइल पूरा भयो भने event emit गर्ने
|
|
375
|
+
if (isLast) {
|
|
376
|
+
this.emit('file:complete', { fileId, deviceId });
|
|
377
|
+
this.log(`subFile: ${deviceId} completed ${file.name}`);
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
this.log(`subFile error:`, err);
|
|
383
|
+
this.sendTo(deviceId, {
|
|
384
|
+
type: 'FILE_ERROR',
|
|
385
|
+
fileId,
|
|
386
|
+
error: String(err),
|
|
387
|
+
timestamp: Date.now()
|
|
388
|
+
});
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
176
392
|
/**
|
|
177
|
-
*
|
|
393
|
+
* resumeFile: पहिले रोकिएको ठाउँबाट फाइल फेरि सुरु गर्ने
|
|
178
394
|
*/
|
|
395
|
+
async resumeFile(deviceId, fileId) {
|
|
396
|
+
const lastChunk = this.getFileProgress(deviceId, fileId);
|
|
397
|
+
const nextChunk = lastChunk + 1;
|
|
398
|
+
this.log(`resumeFile: ${deviceId} resuming ${fileId} from chunk ${nextChunk}`);
|
|
399
|
+
return this.subFile(deviceId, fileId, nextChunk);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* saveFileProgress: डाउनलोड प्रगति सेभ गर्ने
|
|
403
|
+
*/
|
|
404
|
+
saveFileProgress(deviceId, fileId, lastChunk) {
|
|
405
|
+
if (!this.fileProgress.has(deviceId)) {
|
|
406
|
+
this.fileProgress.set(deviceId, new Map());
|
|
407
|
+
}
|
|
408
|
+
this.fileProgress.get(deviceId).set(fileId, lastChunk);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* getFileProgress: पहिलेको प्रगति पुनः प्राप्त गर्ने (Resume को लागि)
|
|
412
|
+
*/
|
|
413
|
+
getFileProgress(deviceId, fileId) {
|
|
414
|
+
return this.fileProgress.get(deviceId)?.get(fileId) ?? -1;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* getFileInfo: फाइलको जानकारी लिने
|
|
418
|
+
*/
|
|
419
|
+
getFileInfo(fileId) {
|
|
420
|
+
return this.fileRegistry.get(fileId);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* listFiles: सबै उपलब्ध फाइलहरूको सूची
|
|
424
|
+
*/
|
|
425
|
+
listFiles() {
|
|
426
|
+
return Array.from(this.fileRegistry.entries()).map(([id, meta]) => ({
|
|
427
|
+
fileId: id,
|
|
428
|
+
name: meta.name,
|
|
429
|
+
size: meta.size,
|
|
430
|
+
totalChunks: meta.totalChunks
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
// ============================================
|
|
434
|
+
// v2.0 NEW: P2P Stream Pass
|
|
435
|
+
// ============================================
|
|
436
|
+
/**
|
|
437
|
+
* announceToPeers: फाइलको उपलब्धता सबै पीयरलाई जानकारी दिने
|
|
438
|
+
*/
|
|
439
|
+
announceToPeers(fileId, availableAtDeviceId) {
|
|
440
|
+
if (!this.config.enableP2P)
|
|
441
|
+
return;
|
|
442
|
+
if (!this.peerRegistry.has(fileId)) {
|
|
443
|
+
this.peerRegistry.set(fileId, new Set());
|
|
444
|
+
}
|
|
445
|
+
this.peerRegistry.get(fileId).add(availableAtDeviceId);
|
|
446
|
+
this.broadcast('p2p/announce', {
|
|
447
|
+
type: 'PEER_AVAILABLE',
|
|
448
|
+
fileId,
|
|
449
|
+
source: availableAtDeviceId,
|
|
450
|
+
peers: Array.from(this.peerRegistry.get(fileId) || []),
|
|
451
|
+
timestamp: Date.now()
|
|
452
|
+
}, { exclude: [availableAtDeviceId] });
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* getPeersForFile: फाइल भएका पीयरहरूको सूची
|
|
456
|
+
*/
|
|
457
|
+
getPeersForFile(fileId) {
|
|
458
|
+
return Array.from(this.peerRegistry.get(fileId) || []);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* requestFromPeer: पीयरबाट सिधै डाटा माग गर्ने
|
|
462
|
+
*/
|
|
463
|
+
requestFromPeer(deviceId, peerId, fileId, chunkIndex) {
|
|
464
|
+
const peerSocket = this.getSocket(peerId);
|
|
465
|
+
if (peerSocket && peerSocket.readyState === 1) {
|
|
466
|
+
peerSocket.send(JSON.stringify({
|
|
467
|
+
type: 'P2P_REQUEST',
|
|
468
|
+
from: deviceId,
|
|
469
|
+
fileId,
|
|
470
|
+
chunkIndex,
|
|
471
|
+
timestamp: Date.now()
|
|
472
|
+
}));
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* sendToPeer: पीयरलाई सिधै डाटा पठाउने (Server Pass-through)
|
|
479
|
+
*/
|
|
480
|
+
sendToPeer(fromDeviceId, toDeviceId, payload) {
|
|
481
|
+
const targetSocket = this.getSocket(toDeviceId);
|
|
482
|
+
if (targetSocket && targetSocket.readyState === 1) {
|
|
483
|
+
const message = this.config.useBinaryProtocol
|
|
484
|
+
? (0, djson_1.toBuffer)(payload)
|
|
485
|
+
: JSON.stringify({
|
|
486
|
+
type: 'P2P_DATA',
|
|
487
|
+
from: fromDeviceId,
|
|
488
|
+
data: payload,
|
|
489
|
+
timestamp: Date.now()
|
|
490
|
+
});
|
|
491
|
+
targetSocket.send(message);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
// ============================================
|
|
497
|
+
// v2.0 NEW: Enhanced Socket Methods
|
|
498
|
+
// ============================================
|
|
499
|
+
/**
|
|
500
|
+
* isReady: डिभाइस अनलाइन छ र मेसेज लिन तयार छ कि छैन चेक गर्ने
|
|
501
|
+
*/
|
|
502
|
+
isReady(deviceId) {
|
|
503
|
+
const device = this.devices.get(deviceId);
|
|
504
|
+
return !!(device?.socket && device.socket.readyState === 1);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* isOnline: डिभाइस अनलाइन छ कि छैन (साधारण चेक)
|
|
508
|
+
*/
|
|
509
|
+
isOnline(deviceId) {
|
|
510
|
+
return this.devices.has(deviceId);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* sendTo: सिधै डिभाइसलाई मेसेज पठाउने (No Pub/Sub overhead)
|
|
514
|
+
*/
|
|
515
|
+
sendTo(deviceId, payload) {
|
|
516
|
+
if (!this.isReady(deviceId))
|
|
517
|
+
return false;
|
|
518
|
+
try {
|
|
519
|
+
const device = this.devices.get(deviceId);
|
|
520
|
+
const data = this.config.useBinaryProtocol
|
|
521
|
+
? (0, djson_1.toBuffer)(payload)
|
|
522
|
+
: JSON.stringify(payload);
|
|
523
|
+
device.socket.send(data);
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
console.error(`[Realtime] Send failed to ${deviceId}:`, err);
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* kick: खराब वा अनधिकृत डिभाइसलाई हटाउने
|
|
533
|
+
*/
|
|
534
|
+
kick(deviceId, reason = "Disconnected by server") {
|
|
535
|
+
const device = this.devices.get(deviceId);
|
|
536
|
+
if (device?.socket) {
|
|
537
|
+
this.sendTo(deviceId, { type: 'KICK', message: reason });
|
|
538
|
+
device.socket.close();
|
|
539
|
+
this.unregister(deviceId);
|
|
540
|
+
this.log(`Kicked ${deviceId}: ${reason}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* broadcastToGroup: कुनै विशेष ग्रुपलाई मात्र मेसेज पठाउने
|
|
545
|
+
*/
|
|
546
|
+
broadcastToGroup(groupName, payload) {
|
|
547
|
+
for (const [id, device] of this.devices) {
|
|
548
|
+
if (device.metadata?.group === groupName && this.isReady(id)) {
|
|
549
|
+
this.sendTo(id, payload);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* getOnlineDevices: सबै अनलाइन डिभाइसहरूको लिस्ट दिने
|
|
555
|
+
*/
|
|
556
|
+
getOnlineDevices() {
|
|
557
|
+
return Array.from(this.devices.entries()).map(([id, d]) => ({
|
|
558
|
+
id,
|
|
559
|
+
lastSeen: d.lastSeen,
|
|
560
|
+
group: d.metadata?.group || 'default'
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* ping: डिभाइसलाई Alive छ कि छैन भनेर चेक गर्न Ping पठाउने
|
|
565
|
+
*/
|
|
566
|
+
ping(deviceId) {
|
|
567
|
+
const device = this.devices.get(deviceId);
|
|
568
|
+
if (device?.socket && typeof device.socket.ping === 'function') {
|
|
569
|
+
device.socket.ping();
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
// ============================================
|
|
575
|
+
// v2.0 NEW: Private Messaging
|
|
576
|
+
// ============================================
|
|
577
|
+
/**
|
|
578
|
+
* privateSub: केवल आफ्नो निजी च्यानलमा आउने मेसेज सुन्नको लागि
|
|
579
|
+
*/
|
|
580
|
+
privateSub(deviceId, fn) {
|
|
581
|
+
const privateTopic = `phone/signaling/${deviceId}`;
|
|
582
|
+
this.subscribe(privateTopic, fn, deviceId);
|
|
583
|
+
this.log(`[Private] ${deviceId} subscribed to private channel`);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* privatePub: कुनै विशेष डिभाइसको निजी च्यानलमा मात्र मेसेज पठाउन
|
|
587
|
+
*/
|
|
588
|
+
privatePub(targetId, payload, opts = {}) {
|
|
589
|
+
const privateTopic = `phone/signaling/${targetId}`;
|
|
590
|
+
this.publish(privateTopic, payload, opts, 'SYSTEM');
|
|
591
|
+
this.log(`[Private] Message sent to ${targetId}`);
|
|
592
|
+
}
|
|
593
|
+
// ============================================
|
|
594
|
+
// Existing Methods (preserved for compatibility)
|
|
595
|
+
// ============================================
|
|
179
596
|
async handle(raw, socket, deviceId) {
|
|
597
|
+
// ... (existing handle method remains the same)
|
|
180
598
|
if (raw.length > (this.config.maxMessageSize || 256 * 1024))
|
|
181
599
|
return;
|
|
182
600
|
const ctx = {
|
|
@@ -196,13 +614,10 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
196
614
|
}
|
|
197
615
|
try {
|
|
198
616
|
const rawStr = raw.toString('utf8');
|
|
199
|
-
this.log('Raw string:', rawStr);
|
|
200
617
|
let topic = null;
|
|
201
618
|
let payload = null;
|
|
202
|
-
// First try: Direct JSON parse
|
|
203
619
|
try {
|
|
204
620
|
const parsed = JSON.parse(rawStr);
|
|
205
|
-
this.log('Direct JSON parse result:', JSON.stringify(parsed));
|
|
206
621
|
if (parsed.type === 'pub' && parsed.topic) {
|
|
207
622
|
topic = parsed.topic;
|
|
208
623
|
payload = parsed.payload;
|
|
@@ -213,120 +628,70 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
213
628
|
}
|
|
214
629
|
}
|
|
215
630
|
catch (e) {
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Try to decode as base64 first
|
|
227
|
-
try {
|
|
228
|
-
decodedBuffer = Buffer.from(decoded.raw, 'base64');
|
|
229
|
-
decodedString = decodedBuffer.toString('utf8');
|
|
230
|
-
this.log('Decoded as base64');
|
|
231
|
-
}
|
|
232
|
-
catch (err) {
|
|
233
|
-
this.log('Failed to decode as base64');
|
|
234
|
-
}
|
|
235
|
-
// If base64 decoding produced a valid JSON string, parse it
|
|
236
|
-
if (decodedString && (decodedString.trim().startsWith('{') || decodedString.trim().startsWith('['))) {
|
|
237
|
-
try {
|
|
238
|
-
const parsed = JSON.parse(decodedString);
|
|
239
|
-
if (parsed.type === 'pub' && parsed.topic) {
|
|
240
|
-
topic = parsed.topic;
|
|
241
|
-
payload = parsed.payload;
|
|
242
|
-
}
|
|
243
|
-
else if (parsed.topic) {
|
|
244
|
-
topic = parsed.topic;
|
|
245
|
-
payload = parsed.payload;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
catch (err) {
|
|
249
|
-
this.log('Failed to parse base64 decoded string as JSON');
|
|
250
|
-
}
|
|
631
|
+
// BUG FIX #2: When raw JSON parse fails, the message may be base64 or hex encoded.
|
|
632
|
+
// djson(Buffer) bypasses autoDetect for Buffer inputs and returns { raw: '...' } without
|
|
633
|
+
// trying base64/hex decode. So we must manually detect and decode base64/hex here.
|
|
634
|
+
let innerStr = null;
|
|
635
|
+
// Try base64 decode: rawStr must match base64 char set and be length % 4 === 0
|
|
636
|
+
if (/^[A-Za-z0-9+/=]+$/.test(rawStr) && rawStr.length % 4 === 0) {
|
|
637
|
+
try {
|
|
638
|
+
const b64Decoded = Buffer.from(rawStr, 'base64').toString('utf8');
|
|
639
|
+
if (b64Decoded.includes('{')) {
|
|
640
|
+
innerStr = b64Decoded;
|
|
251
641
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
else if (parsed.topic) {
|
|
265
|
-
topic = parsed.topic;
|
|
266
|
-
payload = parsed.payload;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
this.log('Failed to decode as hex or parse result');
|
|
272
|
-
}
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
// not base64
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// Try hex decode: rawStr must be even-length hex chars only
|
|
648
|
+
if (!innerStr && /^[0-9a-fA-F]+$/.test(rawStr) && rawStr.length % 2 === 0) {
|
|
649
|
+
try {
|
|
650
|
+
const hexDecoded = Buffer.from(rawStr, 'hex').toString('utf8');
|
|
651
|
+
if (hexDecoded.includes('{')) {
|
|
652
|
+
innerStr = hexDecoded;
|
|
273
653
|
}
|
|
274
654
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
else if (parsed.topic) {
|
|
287
|
-
topic = parsed.topic;
|
|
288
|
-
payload = parsed.payload;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
this.log('Failed to parse buffer as JSON');
|
|
293
|
-
}
|
|
655
|
+
catch {
|
|
656
|
+
// not hex
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// If we successfully decoded, try to parse the inner JSON
|
|
660
|
+
if (innerStr) {
|
|
661
|
+
try {
|
|
662
|
+
const inner = JSON.parse(innerStr);
|
|
663
|
+
if (inner.type === 'pub' && inner.topic) {
|
|
664
|
+
topic = inner.topic;
|
|
665
|
+
payload = inner.payload;
|
|
294
666
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const parsed = JSON.parse(decoded.utf8);
|
|
299
|
-
if (parsed.type === 'pub' && parsed.topic) {
|
|
300
|
-
topic = parsed.topic;
|
|
301
|
-
payload = parsed.payload;
|
|
302
|
-
}
|
|
303
|
-
else if (parsed.topic) {
|
|
304
|
-
topic = parsed.topic;
|
|
305
|
-
payload = parsed.payload;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
309
|
-
this.log('Failed to parse utf8 field as JSON');
|
|
310
|
-
}
|
|
667
|
+
else if (inner.topic) {
|
|
668
|
+
topic = inner.topic;
|
|
669
|
+
payload = inner.payload;
|
|
311
670
|
}
|
|
312
671
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
topic = decoded.topic;
|
|
316
|
-
payload = decoded.payload;
|
|
672
|
+
catch {
|
|
673
|
+
// inner JSON parse failed
|
|
317
674
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
675
|
+
}
|
|
676
|
+
// Fallback: try djson for other custom formats
|
|
677
|
+
if (!topic) {
|
|
678
|
+
const decoded = (0, djson_1.djson)(rawStr);
|
|
679
|
+
if (decoded && typeof decoded === 'object') {
|
|
680
|
+
if (decoded.type === 'pub' && decoded.topic) {
|
|
681
|
+
topic = decoded.topic;
|
|
682
|
+
payload = decoded.payload;
|
|
683
|
+
}
|
|
684
|
+
else if (decoded.topic) {
|
|
685
|
+
topic = decoded.topic;
|
|
686
|
+
payload = decoded.payload;
|
|
687
|
+
}
|
|
321
688
|
}
|
|
322
689
|
}
|
|
323
690
|
}
|
|
324
691
|
if (topic && payload !== null) {
|
|
325
|
-
this.log('PUBLISHING - Topic:', topic);
|
|
326
692
|
this.publish(topic, payload, {}, deviceId);
|
|
327
693
|
}
|
|
328
694
|
else {
|
|
329
|
-
this.log('No topic found, emitting as data/raw');
|
|
330
695
|
if (rawStr.trim().startsWith('{') || rawStr.trim().startsWith('[')) {
|
|
331
696
|
try {
|
|
332
697
|
const data = JSON.parse(rawStr);
|
|
@@ -371,6 +736,17 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
371
736
|
this.plugins.set(plugin.name, plugin);
|
|
372
737
|
}
|
|
373
738
|
register(deviceId, socket, metadata) {
|
|
739
|
+
// Handle reconnection: remove old ghost connection
|
|
740
|
+
if (this.devices.has(deviceId)) {
|
|
741
|
+
this.log(`[Reconnect] Device ${deviceId} reconnecting, cleaning up old...`);
|
|
742
|
+
const oldDevice = this.devices.get(deviceId);
|
|
743
|
+
if (oldDevice?.socket && oldDevice.socket !== socket) {
|
|
744
|
+
try {
|
|
745
|
+
oldDevice.socket.close();
|
|
746
|
+
}
|
|
747
|
+
catch { }
|
|
748
|
+
}
|
|
749
|
+
}
|
|
374
750
|
this.devices.set(deviceId, {
|
|
375
751
|
lastSeen: Date.now(),
|
|
376
752
|
socket,
|
|
@@ -423,41 +799,50 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
423
799
|
}
|
|
424
800
|
getStats() {
|
|
425
801
|
return {
|
|
802
|
+
version: '2.0',
|
|
426
803
|
cacheSize: this.jsonCache.size,
|
|
427
804
|
devices: this.devices.size,
|
|
428
805
|
retained: this.retained.size,
|
|
429
806
|
plugins: this.plugins.size,
|
|
430
|
-
cacheEnabled: this.config.enableJSONCache || false
|
|
807
|
+
cacheEnabled: this.config.enableJSONCache || false,
|
|
808
|
+
// v2.0 stats
|
|
809
|
+
highFreqBuffers: this.highFreqBuffers.size,
|
|
810
|
+
files: this.fileRegistry.size,
|
|
811
|
+
activeTransfers: this.fileProgress.size,
|
|
812
|
+
peers: this.peerRegistry.size
|
|
431
813
|
};
|
|
432
814
|
}
|
|
433
815
|
/**
|
|
434
816
|
* Clean up resources - Call this when shutting down
|
|
435
817
|
*/
|
|
436
818
|
async destroy() {
|
|
437
|
-
// Clear all intervals
|
|
438
819
|
if (this.cleanupInterval) {
|
|
439
820
|
clearInterval(this.cleanupInterval);
|
|
440
821
|
}
|
|
441
822
|
if (this.cacheCleanupInterval) {
|
|
442
823
|
clearInterval(this.cacheCleanupInterval);
|
|
443
824
|
}
|
|
444
|
-
|
|
825
|
+
if (this.bufferCleanupInterval) {
|
|
826
|
+
clearInterval(this.bufferCleanupInterval);
|
|
827
|
+
}
|
|
445
828
|
if (this.redisPub) {
|
|
446
829
|
await this.redisPub.quit();
|
|
447
830
|
}
|
|
448
831
|
if (this.redisSub) {
|
|
449
832
|
await this.redisSub.quit();
|
|
450
833
|
}
|
|
451
|
-
// Clear all maps
|
|
452
834
|
this.trie = new trie_1.TopicTrie();
|
|
453
835
|
this.retained.clear();
|
|
454
836
|
this.devices.clear();
|
|
455
837
|
this.plugins.clear();
|
|
456
838
|
this.pending.clear();
|
|
457
839
|
this.jsonCache.clear();
|
|
458
|
-
|
|
840
|
+
this.highFreqBuffers.clear();
|
|
841
|
+
this.fileRegistry.clear();
|
|
842
|
+
this.fileProgress.clear();
|
|
843
|
+
this.peerRegistry.clear();
|
|
459
844
|
this.removeAllListeners();
|
|
460
|
-
this.log('RealtimeCore destroyed');
|
|
845
|
+
this.log('RealtimeCore v2 destroyed');
|
|
461
846
|
}
|
|
462
847
|
startCleanup() {
|
|
463
848
|
this.cleanupInterval = setInterval(() => {
|
|
@@ -482,4 +867,15 @@ class RealtimeCore extends events_1.EventEmitter {
|
|
|
482
867
|
}
|
|
483
868
|
}
|
|
484
869
|
exports.RealtimeCore = RealtimeCore;
|
|
870
|
+
// Export for use
|
|
871
|
+
var trie_2 = require("./trie");
|
|
872
|
+
Object.defineProperty(exports, "TopicTrie", { enumerable: true, get: function () { return trie_2.TopicTrie; } });
|
|
873
|
+
var codec_2 = require("./codec");
|
|
874
|
+
Object.defineProperty(exports, "encode", { enumerable: true, get: function () { return codec_2.encode; } });
|
|
875
|
+
Object.defineProperty(exports, "decode", { enumerable: true, get: function () { return codec_2.decode; } });
|
|
876
|
+
Object.defineProperty(exports, "getSize", { enumerable: true, get: function () { return codec_2.getSize; } });
|
|
877
|
+
var djson_2 = require("../djson/djson");
|
|
878
|
+
Object.defineProperty(exports, "djson", { enumerable: true, get: function () { return djson_2.djson; } });
|
|
879
|
+
Object.defineProperty(exports, "toBuffer", { enumerable: true, get: function () { return djson_2.toBuffer; } });
|
|
880
|
+
Object.defineProperty(exports, "toBase64", { enumerable: true, get: function () { return djson_2.toBase64; } });
|
|
485
881
|
//# sourceMappingURL=core.js.map
|