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.
- package/DOLPHIN_MASTER_GUIDE_NEPALI.md +507 -210
- package/README.md +1 -0
- package/TUTORIAL_NEPALI.md +14 -1
- package/dist/curd/crud.d.ts +5 -1
- package/dist/curd/crud.js +2 -2
- package/dist/curd/crud.js.map +1 -1
- 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 +3 -2
|
@@ -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
|
|
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
|
-
|
|
103
|
-
|
|
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 = '
|
|
106
|
-
const
|
|
169
|
+
const topic = 'sensor/live';
|
|
170
|
+
const binaryData = Buffer.from([0x01, 0x02, 0x03, 0x04]);
|
|
107
171
|
realtime.subscribe(topic, fn);
|
|
108
|
-
|
|
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(
|
|
174
|
+
expect(fn).toHaveBeenCalledWith(binaryData);
|
|
118
175
|
});
|
|
119
|
-
it('should
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
realtime.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
topic
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
134
|
-
|
|
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
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
realtime.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|