dolphin-server-modules 1.5.6 → 1.6.0

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.
@@ -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
- if (t === topic)
152
- fn(data.payload);
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
- * Handle raw data from a socket with DJSON integration
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
- this.log('Direct JSON parse failed, trying DJSON');
217
- // Second try: DJSON
218
- const decoded = (0, djson_1.djson)(raw);
219
- this.log('DJSON decoded:', JSON.stringify(decoded, null, 2));
220
- if (decoded && typeof decoded === 'object') {
221
- // Check for raw field (base64 or hex encoded data)
222
- if (decoded.raw && typeof decoded.raw === 'string') {
223
- this.log('Found raw field, attempting to decode');
224
- let decodedBuffer = null;
225
- let decodedString = null;
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
- // If not handled yet, try hex decoding
253
- if (!topic && /^[0-9a-fA-F]+$/.test(decoded.raw)) {
254
- try {
255
- decodedBuffer = Buffer.from(decoded.raw, 'hex');
256
- decodedString = decodedBuffer.toString('utf8');
257
- this.log('Decoded as hex');
258
- if (decodedString && (decodedString.trim().startsWith('{') || decodedString.trim().startsWith('['))) {
259
- const parsed = JSON.parse(decodedString);
260
- if (parsed.type === 'pub' && parsed.topic) {
261
- topic = parsed.topic;
262
- payload = parsed.payload;
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
- // Check for _type field (hex/base64 from DJSON)
276
- if (!topic && (decoded._type === 'hex' || decoded._type === 'base64')) {
277
- // Try to get from buffer
278
- if (decoded.buffer && Buffer.isBuffer(decoded.buffer)) {
279
- const bufferStr = decoded.buffer.toString('utf8');
280
- try {
281
- const parsed = JSON.parse(bufferStr);
282
- if (parsed.type === 'pub' && parsed.topic) {
283
- topic = parsed.topic;
284
- payload = parsed.payload;
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
- // Try utf8 field
296
- if (!topic && decoded.utf8) {
297
- try {
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
- // Check for direct pub format in DJSON output
314
- if (!topic && decoded.type === 'pub' && decoded.topic) {
315
- topic = decoded.topic;
316
- payload = decoded.payload;
672
+ catch {
673
+ // inner JSON parse failed
317
674
  }
318
- else if (!topic && decoded.topic) {
319
- topic = decoded.topic;
320
- payload = decoded.payload;
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
- // Close Redis connections
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
- // Remove all listeners
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