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.
@@ -1,39 +1,95 @@
1
1
  "use strict";
2
2
  /// <reference types="jest" />
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
4
37
  const trie_1 = require("./trie");
5
38
  const core_1 = require("./core");
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
6
41
  class MockWebSocket {
7
42
  readyState = 1;
8
43
  messages = [];
9
44
  closed = false;
45
+ pingCalled = false;
10
46
  send(data) {
11
47
  this.messages.push(data);
12
48
  }
13
- ping() { }
49
+ ping() {
50
+ this.pingCalled = true;
51
+ }
14
52
  close() {
15
53
  this.closed = true;
16
54
  this.readyState = 3;
17
55
  }
18
56
  }
19
- describe('Realtime Module - Tests', () => {
57
+ describe('Realtime Module v2 - Tests', () => {
20
58
  let trie;
21
59
  let realtime;
22
60
  let createdInstances = [];
61
+ let testFileId;
62
+ let testFilePath;
63
+ beforeAll(() => {
64
+ // Create a test file for file transfer tests
65
+ testFileId = 'test-file-001';
66
+ testFilePath = path.join(__dirname, 'test-temp-file.bin');
67
+ const testData = Buffer.alloc(1024 * 500, 'A'); // 500KB test file (needs 8 chunks of 64KB to allow chunk index 6 resume test)
68
+ fs.writeFileSync(testFilePath, testData);
69
+ });
70
+ afterAll(() => {
71
+ // Clean up test file
72
+ if (fs.existsSync(testFilePath)) {
73
+ fs.unlinkSync(testFilePath);
74
+ }
75
+ });
23
76
  beforeEach(() => {
24
77
  trie = new trie_1.TopicTrie();
25
78
  realtime = new core_1.RealtimeCore({
26
79
  maxMessageSize: 1024 * 1024,
27
80
  enableJSONCache: true,
28
81
  useBinaryProtocol: false,
29
- debug: false
82
+ debug: false,
83
+ enableP2P: true,
84
+ maxBufferPerTopic: 100,
85
+ defaultChunkSize: 64 * 1024
30
86
  });
31
87
  createdInstances.push(realtime);
32
88
  });
33
89
  afterEach(async () => {
34
90
  jest.clearAllMocks();
35
91
  jest.restoreAllMocks();
36
- // Clean up all created instances to prevent worker process leaks
92
+ // Clean up all created instances
37
93
  for (const instance of createdInstances) {
38
94
  if (instance && typeof instance.destroy === 'function') {
39
95
  await instance.destroy();
@@ -42,9 +98,11 @@ describe('Realtime Module - Tests', () => {
42
98
  createdInstances = [];
43
99
  });
44
100
  afterAll(async () => {
45
- // Final cleanup and wait for any pending intervals
46
101
  await new Promise(resolve => setTimeout(resolve, 200));
47
102
  });
103
+ // ============================================
104
+ // TopicTrie Tests (Existing)
105
+ // ============================================
48
106
  describe('TopicTrie', () => {
49
107
  it('should match exact topics', () => {
50
108
  const fn = jest.fn();
@@ -79,6 +137,9 @@ describe('Realtime Module - Tests', () => {
79
137
  expect(fn2).toHaveBeenCalledTimes(1);
80
138
  });
81
139
  });
140
+ // ============================================
141
+ // Pub/Sub Tests (Existing)
142
+ // ============================================
82
143
  describe('Publish/Subscribe', () => {
83
144
  it('should publish and receive messages', (done) => {
84
145
  const testTopic = 'test/topic';
@@ -99,59 +160,299 @@ describe('Realtime Module - Tests', () => {
99
160
  expect(fn).toHaveBeenCalledWith(testPayload);
100
161
  });
101
162
  });
102
- describe('Direct Publish via Handle', () => {
103
- it('should handle pub format JSON payload', async () => {
163
+ // ============================================
164
+ // v2 NEW: pubPush / subPull Tests
165
+ // ============================================
166
+ describe('High-Frequency: pubPush / subPull', () => {
167
+ it('should push binary data with pubPush', () => {
104
168
  const fn = jest.fn();
105
- const topic = 'direct/test';
106
- const testPayload = { sensor: 'temp', value: 25 };
169
+ const topic = 'sensor/live';
170
+ const binaryData = Buffer.from([0x01, 0x02, 0x03, 0x04]);
107
171
  realtime.subscribe(topic, fn);
108
- const message = Buffer.from(JSON.stringify({
109
- type: 'pub',
110
- topic: topic,
111
- payload: testPayload
112
- }));
113
- await realtime.handle(message, null, 'device-001');
114
- // Wait for async operations
115
- await new Promise(resolve => setTimeout(resolve, 50));
172
+ realtime.pubPush(topic, binaryData);
116
173
  expect(fn).toHaveBeenCalledTimes(1);
117
- expect(fn).toHaveBeenCalledWith(testPayload);
174
+ expect(fn).toHaveBeenCalledWith(binaryData);
118
175
  });
119
- it('should handle base64 encoded pub message', async () => {
120
- const fn = jest.fn();
121
- const topic = 'base64/test';
122
- const testPayload = { command: 'start', value: 100 };
123
- realtime.subscribe(topic, fn);
124
- const originalMessage = {
125
- type: 'pub',
126
- topic: topic,
127
- payload: testPayload
128
- };
129
- const jsonStr = JSON.stringify(originalMessage);
130
- const base64Message = Buffer.from(jsonStr).toString('base64');
131
- await realtime.handle(Buffer.from(base64Message), null, 'device-002');
176
+ it('should pull buffered data with subPull', async () => {
177
+ const deviceId = 'test-device-001';
178
+ const mockSocket = new MockWebSocket();
179
+ const topic = 'sensor/history';
180
+ realtime.register(deviceId, mockSocket);
181
+ // Push multiple data points
182
+ for (let i = 1; i <= 25; i++) {
183
+ realtime.pubPush(topic, Buffer.from([i]));
184
+ }
185
+ // Pull last 10 items
186
+ realtime.subPull(deviceId, topic, 10);
132
187
  await new Promise(resolve => setTimeout(resolve, 50));
133
- expect(fn).toHaveBeenCalledTimes(1);
134
- expect(fn).toHaveBeenCalledWith(testPayload);
188
+ expect(mockSocket.messages.length).toBeGreaterThan(0);
189
+ const lastMessage = JSON.parse(mockSocket.messages[mockSocket.messages.length - 1]);
190
+ expect(lastMessage.type).toBe('PULL_RESPONSE');
191
+ expect(lastMessage.topic).toBe(topic);
192
+ expect(lastMessage.count).toBeLessThanOrEqual(10);
135
193
  });
136
- it('should handle hex encoded pub message', async () => {
137
- const fn = jest.fn();
138
- const topic = 'hex/test';
139
- const testPayload = { command: 'toggle', state: true };
140
- realtime.subscribe(topic, fn);
141
- const originalMessage = {
142
- type: 'pub',
143
- topic: topic,
144
- payload: testPayload
145
- };
146
- const jsonStr = JSON.stringify(originalMessage);
147
- const hexMessage = Buffer.from(jsonStr).toString('hex');
148
- await realtime.handle(Buffer.from(hexMessage), null, 'device-003');
149
- await new Promise(resolve => setTimeout(resolve, 50));
150
- expect(fn).toHaveBeenCalledTimes(1);
151
- expect(fn).toHaveBeenCalledWith(testPayload);
194
+ it('should handle empty buffer in subPull', () => {
195
+ const deviceId = 'test-device-002';
196
+ const mockSocket = new MockWebSocket();
197
+ const topic = 'empty/topic';
198
+ realtime.register(deviceId, mockSocket);
199
+ realtime.subPull(deviceId, topic, 10);
200
+ const lastMessage = JSON.parse(mockSocket.messages[0]);
201
+ expect(lastMessage.type).toBe('PULL_EMPTY');
202
+ expect(lastMessage.message).toBe('No data available');
203
+ });
204
+ });
205
+ // ============================================
206
+ // v2 NEW: File Transfer Tests
207
+ // ============================================
208
+ describe('File Transfer: pubFile / subFile', () => {
209
+ it('should publish a file with pubFile', () => {
210
+ const fileId = 'test-pub-file';
211
+ const metadata = realtime.pubFile(fileId, testFilePath);
212
+ expect(metadata).not.toBeNull();
213
+ expect(metadata?.name).toBe('test-temp-file.bin');
214
+ expect(metadata?.size).toBe(1024 * 500);
215
+ expect(metadata?.totalChunks).toBeGreaterThan(0);
216
+ });
217
+ it('should return null for non-existent file', () => {
218
+ const result = realtime.pubFile('missing-file', '/path/to/nonexistent.bin');
219
+ expect(result).toBeNull();
220
+ });
221
+ it('should get file info', () => {
222
+ const fileId = 'test-info-file';
223
+ realtime.pubFile(fileId, testFilePath);
224
+ const info = realtime.getFileInfo(fileId);
225
+ expect(info).toBeDefined();
226
+ expect(info?.size).toBe(1024 * 500);
227
+ });
228
+ it('should list all available files', () => {
229
+ realtime.pubFile('file-1', testFilePath);
230
+ realtime.pubFile('file-2', testFilePath);
231
+ const files = realtime.listFiles();
232
+ expect(files.length).toBeGreaterThanOrEqual(2);
233
+ expect(files[0]).toHaveProperty('fileId');
234
+ expect(files[0]).toHaveProperty('name');
235
+ expect(files[0]).toHaveProperty('size');
236
+ });
237
+ it('should download file chunks with subFile', async () => {
238
+ const deviceId = 'download-device';
239
+ const mockSocket = new MockWebSocket();
240
+ const fileId = 'test-download-file';
241
+ realtime.register(deviceId, mockSocket);
242
+ realtime.pubFile(fileId, testFilePath);
243
+ // Download first chunk
244
+ const result = await realtime.subFile(deviceId, fileId, 0);
245
+ expect(result).toBe(true);
246
+ expect(mockSocket.messages.length).toBeGreaterThan(0);
247
+ const message = JSON.parse(mockSocket.messages[0]);
248
+ expect(message.type).toBe('FILE_CHUNK');
249
+ expect(message.fileId).toBe(fileId);
250
+ expect(message.chunkIndex).toBe(0);
251
+ expect(message.totalChunks).toBeDefined();
252
+ });
253
+ it('should handle file not found in subFile', async () => {
254
+ const deviceId = 'error-device';
255
+ const mockSocket = new MockWebSocket();
256
+ realtime.register(deviceId, mockSocket);
257
+ const result = await realtime.subFile(deviceId, 'non-existent-file', 0);
258
+ expect(result).toBe(false);
259
+ const message = JSON.parse(mockSocket.messages[0]);
260
+ expect(message.type).toBe('FILE_ERROR');
261
+ expect(message.error).toBe('File not found');
262
+ });
263
+ });
264
+ // ============================================
265
+ // v2 NEW: Resume Feature Tests
266
+ // ============================================
267
+ describe('Resume Feature', () => {
268
+ it('should save and get file progress', () => {
269
+ const deviceId = 'resume-device';
270
+ const fileId = 'resume-file';
271
+ // Save progress at chunk 5
272
+ realtime.saveFileProgress(deviceId, fileId, 5);
273
+ const progress = realtime.getFileProgress(deviceId, fileId);
274
+ expect(progress).toBe(5);
275
+ });
276
+ it('should return -1 for no progress', () => {
277
+ const progress = realtime.getFileProgress('unknown-device', 'unknown-file');
278
+ expect(progress).toBe(-1);
279
+ });
280
+ it('should resume file from last chunk', async () => {
281
+ const deviceId = 'resume-device-2';
282
+ const mockSocket = new MockWebSocket();
283
+ const fileId = 'test-resume-file';
284
+ realtime.register(deviceId, mockSocket);
285
+ realtime.pubFile(fileId, testFilePath);
286
+ // Simulate partial download (chunk 5 completed)
287
+ realtime.saveFileProgress(deviceId, fileId, 5);
288
+ // Resume from chunk 6
289
+ const result = await realtime.resumeFile(deviceId, fileId);
290
+ expect(result).toBe(true);
291
+ const message = JSON.parse(mockSocket.messages[0]);
292
+ expect(message.type).toBe('FILE_CHUNK');
293
+ expect(message.chunkIndex).toBe(6); // Should start from chunk 6
294
+ });
295
+ it('should handle complete file in resume', async () => {
296
+ const deviceId = 'complete-device';
297
+ const mockSocket = new MockWebSocket();
298
+ const fileId = 'test-complete-file';
299
+ realtime.register(deviceId, mockSocket);
300
+ const metadata = realtime.pubFile(fileId, testFilePath);
301
+ if (metadata) {
302
+ // Save progress at last chunk
303
+ realtime.saveFileProgress(deviceId, fileId, metadata.totalChunks);
304
+ const result = await realtime.resumeFile(deviceId, fileId);
305
+ expect(result).toBe(true);
306
+ const message = JSON.parse(mockSocket.messages[0]);
307
+ expect(message.type).toBe('FILE_COMPLETE');
308
+ }
309
+ });
310
+ });
311
+ // ============================================
312
+ // v2 NEW: Private Messaging Tests
313
+ // ============================================
314
+ describe('Private Messaging', () => {
315
+ it('should send private message to specific device', (done) => {
316
+ const deviceId = 'private-device-001';
317
+ realtime.privateSub(deviceId, (payload) => {
318
+ expect(payload).toEqual({ secret: 'OTP: 123456' });
319
+ done();
320
+ });
321
+ realtime.privatePub(deviceId, { secret: 'OTP: 123456' });
322
+ });
323
+ it('should not deliver private message to wrong device', () => {
324
+ const fn1 = jest.fn();
325
+ const fn2 = jest.fn();
326
+ const deviceId1 = 'user-001';
327
+ const deviceId2 = 'user-002';
328
+ realtime.privateSub(deviceId1, fn1);
329
+ realtime.privateSub(deviceId2, fn2);
330
+ realtime.privatePub(deviceId1, { message: 'for user 1 only' });
331
+ expect(fn1).toHaveBeenCalledTimes(1);
332
+ expect(fn2).toHaveBeenCalledTimes(0);
152
333
  });
153
334
  });
335
+ // ============================================
336
+ // v2 NEW: Socket Helper Tests
337
+ // ============================================
338
+ describe('Socket Helpers', () => {
339
+ it('should check if device is ready', () => {
340
+ const deviceId = 'ready-device';
341
+ const mockSocket = new MockWebSocket();
342
+ expect(realtime.isReady(deviceId)).toBe(false);
343
+ realtime.register(deviceId, mockSocket);
344
+ expect(realtime.isReady(deviceId)).toBe(true);
345
+ });
346
+ it('should check if device is online', () => {
347
+ const deviceId = 'online-device';
348
+ expect(realtime.isOnline(deviceId)).toBe(false);
349
+ realtime.register(deviceId);
350
+ expect(realtime.isOnline(deviceId)).toBe(true);
351
+ });
352
+ it('should send direct message with sendTo', () => {
353
+ const deviceId = 'direct-device';
354
+ const mockSocket = new MockWebSocket();
355
+ const testPayload = { direct: 'message' };
356
+ realtime.register(deviceId, mockSocket);
357
+ const result = realtime.sendTo(deviceId, testPayload);
358
+ expect(result).toBe(true);
359
+ expect(mockSocket.messages.length).toBeGreaterThan(0);
360
+ });
361
+ it('should return false for sendTo when device not ready', () => {
362
+ const result = realtime.sendTo('non-existent-device', { data: 'test' });
363
+ expect(result).toBe(false);
364
+ });
365
+ it('should kick device', () => {
366
+ const deviceId = 'kick-device';
367
+ const mockSocket = new MockWebSocket();
368
+ realtime.register(deviceId, mockSocket);
369
+ expect(realtime.isOnline(deviceId)).toBe(true);
370
+ realtime.kick(deviceId, 'Test kick');
371
+ expect(mockSocket.closed).toBe(true);
372
+ expect(realtime.isOnline(deviceId)).toBe(false);
373
+ });
374
+ it('should broadcast to group', () => {
375
+ const device1 = new MockWebSocket();
376
+ const device2 = new MockWebSocket();
377
+ const device3 = new MockWebSocket();
378
+ realtime.register('admin-1', device1, { group: 'admin' });
379
+ realtime.register('admin-2', device2, { group: 'admin' });
380
+ realtime.register('user-1', device3, { group: 'user' });
381
+ realtime.broadcastToGroup('admin', { alert: 'Server update' });
382
+ expect(device1.messages.length).toBeGreaterThan(0);
383
+ expect(device2.messages.length).toBeGreaterThan(0);
384
+ expect(device3.messages.length).toBe(0);
385
+ });
386
+ it('should get online devices list', () => {
387
+ realtime.register('device-a', new MockWebSocket(), { group: 'group1' });
388
+ realtime.register('device-b', new MockWebSocket(), { group: 'group2' });
389
+ const devices = realtime.getOnlineDevices();
390
+ expect(devices.length).toBe(2);
391
+ expect(devices[0]).toHaveProperty('id');
392
+ expect(devices[0]).toHaveProperty('lastSeen');
393
+ expect(devices[0]).toHaveProperty('group');
394
+ });
395
+ it('should ping device', () => {
396
+ const deviceId = 'ping-device';
397
+ const mockSocket = new MockWebSocket();
398
+ realtime.register(deviceId, mockSocket);
399
+ const result = realtime.ping(deviceId);
400
+ expect(result).toBe(true);
401
+ expect(mockSocket.pingCalled).toBe(true);
402
+ });
403
+ });
404
+ // ============================================
405
+ // v2 NEW: P2P Tests
406
+ // ============================================
407
+ describe('P2P Features', () => {
408
+ it('should announce file to peers', () => {
409
+ const fileId = 'p2p-file';
410
+ const sourceDevice = 'source-device';
411
+ const peerDevice = new MockWebSocket();
412
+ realtime.register('peer-device', peerDevice);
413
+ realtime.announceToPeers(fileId, sourceDevice);
414
+ const peers = realtime.getPeersForFile(fileId);
415
+ expect(peers).toContain(sourceDevice);
416
+ });
417
+ it('should request chunk from peer', () => {
418
+ const peerDevice = new MockWebSocket();
419
+ const requestingDevice = 'requesting-device';
420
+ const fileId = 'p2p-file';
421
+ realtime.register('peer-001', peerDevice);
422
+ const result = realtime.requestFromPeer(requestingDevice, 'peer-001', fileId, 5);
423
+ expect(result).toBe(true);
424
+ const message = JSON.parse(peerDevice.messages[0]);
425
+ expect(message.type).toBe('P2P_REQUEST');
426
+ expect(message.fileId).toBe(fileId);
427
+ expect(message.chunkIndex).toBe(5);
428
+ });
429
+ it('should send data to peer', () => {
430
+ const targetDevice = new MockWebSocket();
431
+ const fromDevice = 'sender-device';
432
+ const testData = { chunk: 'data' };
433
+ realtime.register('target-peer', targetDevice);
434
+ const result = realtime.sendToPeer(fromDevice, 'target-peer', testData);
435
+ expect(result).toBe(true);
436
+ const message = JSON.parse(targetDevice.messages[0]);
437
+ expect(message.type).toBe('P2P_DATA');
438
+ expect(message.from).toBe(fromDevice);
439
+ });
440
+ });
441
+ // ============================================
442
+ // Device Management Tests (Enhanced)
443
+ // ============================================
154
444
  describe('Device Management', () => {
445
+ it('should handle reconnection (remove old ghost connection)', () => {
446
+ const deviceId = 'reconnect-device';
447
+ const oldSocket = new MockWebSocket();
448
+ const newSocket = new MockWebSocket();
449
+ realtime.register(deviceId, oldSocket);
450
+ expect(realtime.getSocket(deviceId)).toBe(oldSocket);
451
+ // Reconnect with new socket
452
+ realtime.register(deviceId, newSocket);
453
+ expect(oldSocket.closed).toBe(true); // Old socket should be closed
454
+ expect(realtime.getSocket(deviceId)).toBe(newSocket);
455
+ });
155
456
  it('should register and track devices', () => {
156
457
  const deviceId = 'device-001';
157
458
  const mockSocket = new MockWebSocket();
@@ -176,6 +477,9 @@ describe('Realtime Module - Tests', () => {
176
477
  expect(mockSocket.closed).toBe(true);
177
478
  });
178
479
  });
480
+ // ============================================
481
+ // Broadcast Tests (Existing + Enhanced)
482
+ // ============================================
179
483
  describe('Broadcast', () => {
180
484
  it('should broadcast to all devices', () => {
181
485
  const device1 = new MockWebSocket();
@@ -196,6 +500,9 @@ describe('Realtime Module - Tests', () => {
196
500
  expect(device2.messages.length).toBeGreaterThan(0);
197
501
  });
198
502
  });
503
+ // ============================================
504
+ // Performance Tests
505
+ // ============================================
199
506
  describe('Performance', () => {
200
507
  it('should respect max message size', () => {
201
508
  const largePayload = { data: 'x'.repeat(1024 * 1024 + 1) };
@@ -218,7 +525,22 @@ describe('Realtime Module - Tests', () => {
218
525
  const stats = cachedRealtime.getStats();
219
526
  expect(stats.cacheEnabled).toBe(true);
220
527
  });
528
+ it('should provide accurate stats', () => {
529
+ realtime.register('stat-device-1', new MockWebSocket());
530
+ realtime.register('stat-device-2', new MockWebSocket());
531
+ realtime.pubFile('stat-file', testFilePath);
532
+ const stats = realtime.getStats();
533
+ expect(stats.version).toBe('2.0');
534
+ expect(stats.devices).toBe(2);
535
+ expect(stats.files).toBeGreaterThan(0);
536
+ expect(stats).toHaveProperty('highFreqBuffers');
537
+ expect(stats).toHaveProperty('activeTransfers');
538
+ expect(stats).toHaveProperty('peers');
539
+ });
221
540
  });
541
+ // ============================================
542
+ // ACL Tests (Existing)
543
+ // ============================================
222
544
  describe('ACL', () => {
223
545
  it('should enforce subscribe ACL', () => {
224
546
  const aclRealtime = new core_1.RealtimeCore({
@@ -254,6 +576,9 @@ describe('Realtime Module - Tests', () => {
254
576
  }).toThrow('ACL deny');
255
577
  });
256
578
  });
579
+ // ============================================
580
+ // Error Handling Tests (Existing + Enhanced)
581
+ // ============================================
257
582
  describe('Error Handling', () => {
258
583
  it('should handle socket send errors', () => {
259
584
  const faultySocket = new MockWebSocket();
@@ -266,6 +591,68 @@ describe('Realtime Module - Tests', () => {
266
591
  expect(faultySocket.send).toHaveBeenCalled();
267
592
  consoleSpy.mockRestore();
268
593
  });
594
+ it('should handle destroy cleanup', async () => {
595
+ const testRealtime = new core_1.RealtimeCore({ debug: false });
596
+ createdInstances.push(testRealtime);
597
+ testRealtime.register('cleanup-device', new MockWebSocket());
598
+ await testRealtime.destroy();
599
+ const stats = testRealtime.getStats();
600
+ expect(stats.devices).toBe(0);
601
+ });
602
+ });
603
+ // ============================================
604
+ // Direct Publish via Handle Tests (Existing)
605
+ // ============================================
606
+ describe('Direct Publish via Handle', () => {
607
+ it('should handle pub format JSON payload', async () => {
608
+ const fn = jest.fn();
609
+ const topic = 'direct/test';
610
+ const testPayload = { sensor: 'temp', value: 25 };
611
+ realtime.subscribe(topic, fn);
612
+ const message = Buffer.from(JSON.stringify({
613
+ type: 'pub',
614
+ topic: topic,
615
+ payload: testPayload
616
+ }));
617
+ await realtime.handle(message, null, 'device-001');
618
+ await new Promise(resolve => setTimeout(resolve, 50));
619
+ expect(fn).toHaveBeenCalledTimes(1);
620
+ expect(fn).toHaveBeenCalledWith(testPayload);
621
+ });
622
+ it('should handle base64 encoded pub message', async () => {
623
+ const fn = jest.fn();
624
+ const topic = 'base64/test';
625
+ const testPayload = { command: 'start', value: 100 };
626
+ realtime.subscribe(topic, fn);
627
+ const originalMessage = {
628
+ type: 'pub',
629
+ topic: topic,
630
+ payload: testPayload
631
+ };
632
+ const jsonStr = JSON.stringify(originalMessage);
633
+ const base64Message = Buffer.from(jsonStr).toString('base64');
634
+ await realtime.handle(Buffer.from(base64Message), null, 'device-002');
635
+ await new Promise(resolve => setTimeout(resolve, 50));
636
+ expect(fn).toHaveBeenCalledTimes(1);
637
+ expect(fn).toHaveBeenCalledWith(testPayload);
638
+ });
639
+ it('should handle hex encoded pub message', async () => {
640
+ const fn = jest.fn();
641
+ const topic = 'hex/test';
642
+ const testPayload = { command: 'toggle', state: true };
643
+ realtime.subscribe(topic, fn);
644
+ const originalMessage = {
645
+ type: 'pub',
646
+ topic: topic,
647
+ payload: testPayload
648
+ };
649
+ const jsonStr = JSON.stringify(originalMessage);
650
+ const hexMessage = Buffer.from(jsonStr).toString('hex');
651
+ await realtime.handle(Buffer.from(hexMessage), null, 'device-003');
652
+ await new Promise(resolve => setTimeout(resolve, 50));
653
+ expect(fn).toHaveBeenCalledTimes(1);
654
+ expect(fn).toHaveBeenCalledWith(testPayload);
655
+ });
269
656
  });
270
657
  });
271
658
  //# sourceMappingURL=realtime.test.js.map