@phystack/hub-client 4.5.19-dev → 4.5.21-dev

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.
Files changed (119) hide show
  1. package/dist/index.d.ts +22 -28
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +252 -378
  4. package/dist/index.js.map +1 -1
  5. package/dist/peripheral-twin.d.ts +34 -0
  6. package/dist/peripheral-twin.d.ts.map +1 -0
  7. package/dist/peripheral-twin.js +234 -0
  8. package/dist/peripheral-twin.js.map +1 -0
  9. package/dist/services/phyhub-connection.service.d.ts +1 -0
  10. package/dist/services/phyhub-connection.service.d.ts.map +1 -1
  11. package/dist/services/phyhub-connection.service.js +17 -0
  12. package/dist/services/phyhub-connection.service.js.map +1 -1
  13. package/dist/services/phyhub-direct-connection.service.d.ts +21 -0
  14. package/dist/services/phyhub-direct-connection.service.d.ts.map +1 -0
  15. package/dist/services/phyhub-direct-connection.service.js +101 -0
  16. package/dist/services/phyhub-direct-connection.service.js.map +1 -0
  17. package/dist/services/webrtc/data-channel-handler.d.ts +45 -0
  18. package/dist/services/webrtc/data-channel-handler.d.ts.map +1 -0
  19. package/dist/services/webrtc/data-channel-handler.js +260 -0
  20. package/dist/services/webrtc/data-channel-handler.js.map +1 -0
  21. package/dist/services/webrtc/index.d.ts +8 -0
  22. package/dist/services/webrtc/index.d.ts.map +1 -0
  23. package/dist/services/webrtc/index.js +18 -0
  24. package/dist/services/webrtc/index.js.map +1 -0
  25. package/dist/services/webrtc/media-stream-handler.d.ts +57 -0
  26. package/dist/services/webrtc/media-stream-handler.d.ts.map +1 -0
  27. package/dist/services/webrtc/media-stream-handler.js +383 -0
  28. package/dist/services/webrtc/media-stream-handler.js.map +1 -0
  29. package/dist/services/webrtc/peer-connection-manager.d.ts +40 -0
  30. package/dist/services/webrtc/peer-connection-manager.d.ts.map +1 -0
  31. package/dist/services/webrtc/peer-connection-manager.js +336 -0
  32. package/dist/services/webrtc/peer-connection-manager.js.map +1 -0
  33. package/dist/services/webrtc/types.d.ts +134 -0
  34. package/dist/services/webrtc/types.d.ts.map +1 -0
  35. package/dist/services/webrtc/types.js +12 -0
  36. package/dist/services/webrtc/types.js.map +1 -0
  37. package/dist/services/webrtc/webrtc-globals.d.ts +4 -0
  38. package/dist/services/webrtc/webrtc-globals.d.ts.map +1 -0
  39. package/dist/services/webrtc/webrtc-globals.js +72 -0
  40. package/dist/services/webrtc/webrtc-globals.js.map +1 -0
  41. package/dist/services/webrtc/webrtc-manager.d.ts +35 -0
  42. package/dist/services/webrtc/webrtc-manager.d.ts.map +1 -0
  43. package/dist/services/webrtc/webrtc-manager.js +274 -0
  44. package/dist/services/webrtc/webrtc-manager.js.map +1 -0
  45. package/dist/test/communication-comprehensive-test.d.ts +8 -0
  46. package/dist/test/communication-comprehensive-test.d.ts.map +1 -0
  47. package/dist/test/communication-comprehensive-test.js +356 -0
  48. package/dist/test/communication-comprehensive-test.js.map +1 -0
  49. package/dist/test/webrtc-channel-names-test.d.ts +2 -0
  50. package/dist/test/webrtc-channel-names-test.d.ts.map +1 -0
  51. package/dist/test/webrtc-channel-names-test.js +177 -0
  52. package/dist/test/webrtc-channel-names-test.js.map +1 -0
  53. package/dist/test/webrtc-comprehensive-test.d.ts +2 -0
  54. package/dist/test/webrtc-comprehensive-test.d.ts.map +1 -0
  55. package/dist/test/webrtc-comprehensive-test.js +328 -0
  56. package/dist/test/webrtc-comprehensive-test.js.map +1 -0
  57. package/dist/test/webrtc-reconnect-test.d.ts +4 -0
  58. package/dist/test/webrtc-reconnect-test.d.ts.map +1 -0
  59. package/dist/test/webrtc-reconnect-test.js +244 -0
  60. package/dist/test/webrtc-reconnect-test.js.map +1 -0
  61. package/dist/test/webrtc-test-harness.d.ts +4 -0
  62. package/dist/test/webrtc-test-harness.d.ts.map +1 -0
  63. package/dist/test/webrtc-test-harness.js +169 -0
  64. package/dist/test/webrtc-test-harness.js.map +1 -0
  65. package/dist/twin-messaging.d.ts +20 -0
  66. package/dist/twin-messaging.d.ts.map +1 -0
  67. package/dist/twin-messaging.js +94 -0
  68. package/dist/twin-messaging.js.map +1 -0
  69. package/dist/twin-registry.d.ts +9 -0
  70. package/dist/twin-registry.d.ts.map +1 -0
  71. package/dist/twin-registry.js +26 -0
  72. package/dist/twin-registry.js.map +1 -0
  73. package/dist/types/index.d.ts +4 -0
  74. package/dist/types/index.d.ts.map +1 -0
  75. package/dist/types/index.js +20 -0
  76. package/dist/types/index.js.map +1 -0
  77. package/dist/types/twin.types.d.ts +62 -14
  78. package/dist/types/twin.types.d.ts.map +1 -1
  79. package/dist/types/twin.types.js +8 -1
  80. package/dist/types/twin.types.js.map +1 -1
  81. package/docs/webrtc-howto.md +398 -0
  82. package/docs/webrtc-test.md +330 -0
  83. package/package.json +3 -3
  84. package/scripts/webrtc-test.sh +401 -0
  85. package/src/index.ts +378 -568
  86. package/src/peripheral-twin.ts +337 -0
  87. package/src/services/phyhub-connection.service.ts +24 -0
  88. package/src/services/phyhub-direct-connection.service.ts +159 -0
  89. package/src/services/webrtc/data-channel-handler.ts +362 -0
  90. package/src/services/webrtc/index.ts +36 -0
  91. package/src/services/webrtc/media-stream-handler.ts +536 -0
  92. package/src/services/webrtc/peer-connection-manager.ts +467 -0
  93. package/src/services/webrtc/types.ts +273 -0
  94. package/src/services/webrtc/webrtc-globals.ts +108 -0
  95. package/src/services/webrtc/webrtc-manager.ts +490 -0
  96. package/src/test/communication-comprehensive-test.ts +533 -0
  97. package/src/test/webrtc-channel-names-test.ts +266 -0
  98. package/src/test/webrtc-comprehensive-test.ts +494 -0
  99. package/src/test/webrtc-reconnect-test.ts +345 -0
  100. package/src/test/webrtc-test-harness.ts +254 -0
  101. package/src/twin-messaging.ts +184 -0
  102. package/src/twin-registry.ts +39 -0
  103. package/src/types/index.ts +3 -0
  104. package/src/types/twin.types.ts +80 -14
  105. package/dist/services/webrtc/datachannel.d.ts +0 -10
  106. package/dist/services/webrtc/datachannel.d.ts.map +0 -1
  107. package/dist/services/webrtc/datachannel.js +0 -290
  108. package/dist/services/webrtc/datachannel.js.map +0 -1
  109. package/dist/services/webrtc/mediastream.d.ts +0 -10
  110. package/dist/services/webrtc/mediastream.d.ts.map +0 -1
  111. package/dist/services/webrtc/mediastream.js +0 -396
  112. package/dist/services/webrtc/mediastream.js.map +0 -1
  113. package/dist/services/webrtc/peer-connection-ice.d.ts +0 -32
  114. package/dist/services/webrtc/peer-connection-ice.d.ts.map +0 -1
  115. package/dist/services/webrtc/peer-connection-ice.js +0 -483
  116. package/dist/services/webrtc/peer-connection-ice.js.map +0 -1
  117. package/src/services/webrtc/datachannel.ts +0 -421
  118. package/src/services/webrtc/mediastream.ts +0 -602
  119. package/src/services/webrtc/peer-connection-ice.ts +0 -689
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Communication Comprehensive Test Suite
3
+ *
4
+ * Tests ALL communication methods between two twins:
5
+ *
6
+ * Hub-based (stateless):
7
+ * 1. Events - fire-and-forget messaging via instance.emit/on
8
+ * 2. Actions - request-response with callbacks
9
+ *
10
+ * WebRTC P2P (stateful):
11
+ * 3. DataChannel - default channel
12
+ * 4. DataChannel - named channels
13
+ * 5. MediaStream - connection establishment
14
+ *
15
+ * Usage (two terminals or two hosts via webrtc-test.sh):
16
+ *
17
+ * Terminal 1 (initiator):
18
+ * ```
19
+ * PHYHUB_DIRECT=true \
20
+ * DEVICE_ID=<device-a-id> \
21
+ * ACCESS_KEY=<device-a-key> \
22
+ * PEER_TWIN_ID=<device-b-twin-id> \
23
+ * ROLE=initiator \
24
+ * node dist/test/communication-comprehensive-test.js
25
+ * ```
26
+ *
27
+ * Terminal 2 (responder):
28
+ * ```
29
+ * PHYHUB_DIRECT=true \
30
+ * DEVICE_ID=<device-b-id> \
31
+ * ACCESS_KEY=<device-b-key> \
32
+ * PEER_TWIN_ID=<device-a-twin-id> \
33
+ * ROLE=responder \
34
+ * node dist/test/communication-comprehensive-test.js
35
+ * ```
36
+ */
37
+
38
+ import { PhyHubClient, Instance } from '../index';
39
+ import { PhygridDataChannel } from '../services/webrtc/types';
40
+
41
+ interface TestResult {
42
+ name: string;
43
+ passed: boolean;
44
+ details: string;
45
+ duration: number;
46
+ }
47
+
48
+ const MESSAGES_PER_TEST = 3;
49
+ const results: TestResult[] = [];
50
+
51
+ function log(msg: string): void {
52
+ console.log(`[${new Date().toISOString().split('T')[1].slice(0, 8)}] ${msg}`);
53
+ }
54
+
55
+ function sleep(ms: number): Promise<void> {
56
+ return new Promise((resolve) => setTimeout(resolve, ms));
57
+ }
58
+
59
+ function addResult(name: string, passed: boolean, details: string, startTime: number): void {
60
+ const duration = Date.now() - startTime;
61
+ results.push({ name, passed, details, duration });
62
+ const status = passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
63
+ log(`[${status}] ${name}: ${details} (${duration}ms)`);
64
+ }
65
+
66
+ // =============================================================================
67
+ // Test 1: Events (Fire-and-Forget via Hub)
68
+ // =============================================================================
69
+ async function testEvents(
70
+ client: PhyHubClient,
71
+ instance: Instance,
72
+ peerTwinId: string,
73
+ isInitiator: boolean
74
+ ): Promise<void> {
75
+ const testName = 'Events (Hub-based)';
76
+ const startTime = Date.now();
77
+ log(`\n=== Test 1: ${testName} ===`);
78
+
79
+ try {
80
+ let receivedCount = 0;
81
+ const receivedMessages: any[] = [];
82
+
83
+ // Subscribe to peer's messages
84
+ await client.subscribeTwin(peerTwinId);
85
+ log(`Subscribed to peer twin: ${peerTwinId}`);
86
+
87
+ // Register event listener
88
+ instance.on('test-event', (data: any) => {
89
+ receivedCount++;
90
+ receivedMessages.push(data);
91
+ log(`[EVENT RECEIVED] ${JSON.stringify(data)}`);
92
+ });
93
+
94
+ if (isInitiator) {
95
+ // Wait for responder to be ready
96
+ log('Waiting for responder to set up listeners...');
97
+ await sleep(3000);
98
+
99
+ // Send test events to peer
100
+ log('Sending test events...');
101
+ for (let i = 0; i < MESSAGES_PER_TEST; i++) {
102
+ const eventData = { type: 'test', count: i + 1, timestamp: Date.now() };
103
+ log(`[SENDING EVENT] ${JSON.stringify(eventData)}`);
104
+ // Fire-and-forget: no response expected, no promise to catch
105
+ instance.to(peerTwinId).emit('test-event', eventData);
106
+ await sleep(500);
107
+ }
108
+
109
+ // Wait for responses/echoes
110
+ await sleep(5000);
111
+
112
+ // Check if we received echo events from responder
113
+ const passed = receivedCount >= MESSAGES_PER_TEST;
114
+ addResult(testName, passed,
115
+ `Sent ${MESSAGES_PER_TEST}, received echoes: ${receivedCount}`, startTime);
116
+ } else {
117
+ // Responder: Listen for events and echo them back
118
+ log('Waiting for events from initiator...');
119
+
120
+ // Set up echo listener
121
+ instance.on('test-event', (data: any) => {
122
+ // Echo back to initiator (fire-and-forget: no promise to catch)
123
+ const echoData = { ...data, echo: true, echoedAt: Date.now() };
124
+ log(`[ECHOING EVENT] ${JSON.stringify(echoData)}`);
125
+ instance.to(peerTwinId).emit('test-event', echoData);
126
+ });
127
+
128
+ // Wait for test to complete
129
+ await sleep(10000);
130
+
131
+ const passed = receivedCount >= MESSAGES_PER_TEST;
132
+ addResult(testName, passed,
133
+ `Received ${receivedCount} events from initiator`, startTime);
134
+ }
135
+ } catch (error: any) {
136
+ addResult(testName, false, error.message, startTime);
137
+ }
138
+ }
139
+
140
+ // =============================================================================
141
+ // Test 2: Actions (Request-Response via Hub)
142
+ // =============================================================================
143
+ async function testActions(
144
+ _client: PhyHubClient,
145
+ instance: Instance,
146
+ peerTwinId: string,
147
+ isInitiator: boolean
148
+ ): Promise<void> {
149
+ const testName = 'Actions (Hub-based)';
150
+ const startTime = Date.now();
151
+ log(`\n=== Test 2: ${testName} ===`);
152
+
153
+ try {
154
+ let successfulActions = 0;
155
+ let failedActions = 0;
156
+
157
+ if (isInitiator) {
158
+ // Wait for responder to set up action handlers
159
+ log('Waiting for responder to set up action handlers...');
160
+ await sleep(3000);
161
+
162
+ // Test: instance.to(peerTwinId).emit() with callback (action pattern)
163
+ log('Testing instance.to(peerTwinId).emit() with callback (action pattern)...');
164
+ for (let i = 0; i < MESSAGES_PER_TEST; i++) {
165
+ const actionType = 'test-action';
166
+ const actionPayload = {
167
+ command: 'process',
168
+ value: i * 10,
169
+ timestamp: Date.now()
170
+ };
171
+
172
+ log(`[SENDING ACTION ${i + 1}] ${actionType}: ${JSON.stringify(actionPayload)}`);
173
+
174
+ try {
175
+ // emit with callback returns a Promise (action pattern)
176
+ const result = await instance.to(peerTwinId).emit(actionType, actionPayload, () => {});
177
+ log(`[ACTION RESPONSE] ${JSON.stringify(result)}`);
178
+ if (result?.status === 'success') {
179
+ successfulActions++;
180
+ } else {
181
+ failedActions++;
182
+ }
183
+ } catch (err: any) {
184
+ log(`[ACTION ERROR] ${err.message || JSON.stringify(err)}`);
185
+ failedActions++;
186
+ }
187
+
188
+ await sleep(500);
189
+ }
190
+
191
+ const passed = successfulActions >= MESSAGES_PER_TEST;
192
+ addResult(testName, passed,
193
+ `Success: ${successfulActions}, Failed: ${failedActions}`, startTime);
194
+ } else {
195
+ // Responder: Handle requests using instance.on()
196
+ log('Setting up request handlers via instance.on()...');
197
+
198
+ // Register request handler - the respond callback allows sending a response
199
+ instance.on('test-action', (message: any, respond?: (result: any) => void) => {
200
+ log(`[REQUEST RECEIVED] ${JSON.stringify(message)}`);
201
+ // Send success response
202
+ if (respond) {
203
+ respond({ status: 'success', message: 'Request processed successfully' });
204
+ }
205
+ });
206
+
207
+ // Wait for test to complete
208
+ await sleep(15000);
209
+
210
+ addResult(testName, true, 'Request handler registered and responded', startTime);
211
+ }
212
+ } catch (error: any) {
213
+ addResult(testName, false, error.message, startTime);
214
+ }
215
+ }
216
+
217
+ // =============================================================================
218
+ // Test 3: DataChannel Default (Unnamed)
219
+ // =============================================================================
220
+ async function testDataChannelDefault(
221
+ client: PhyHubClient,
222
+ peerTwinId: string,
223
+ isInitiator: boolean
224
+ ): Promise<void> {
225
+ const testName = 'DataChannel Default (P2P)';
226
+ const startTime = Date.now();
227
+ log(`\n=== Test 3: ${testName} ===`);
228
+
229
+ try {
230
+ let channel: PhygridDataChannel;
231
+ let receivedCount = 0;
232
+
233
+ if (isInitiator) {
234
+ channel = await client.getDataChannel(peerTwinId);
235
+ log(`Created default channel. Name: ${channel.getChannelName()}`);
236
+
237
+ if (channel.getChannelName() !== 'default') {
238
+ throw new Error(`Expected channel name 'default', got '${channel.getChannelName()}'`);
239
+ }
240
+
241
+ channel.onMessage((data) => {
242
+ receivedCount++;
243
+ log(`[DC RECEIVED] ${JSON.stringify(data)}`);
244
+ });
245
+
246
+ await sleep(2000);
247
+
248
+ for (let i = 0; i < MESSAGES_PER_TEST; i++) {
249
+ const msg = { test: 'default', num: i, timestamp: Date.now() };
250
+ log(`[DC SENDING] ${JSON.stringify(msg)}`);
251
+ channel.send(msg);
252
+ await sleep(300);
253
+ }
254
+
255
+ await sleep(3000);
256
+
257
+ addResult(testName, receivedCount >= MESSAGES_PER_TEST,
258
+ `Sent ${MESSAGES_PER_TEST}, received echoes: ${receivedCount}`, startTime);
259
+
260
+ channel.close();
261
+ } else {
262
+ await new Promise<void>((resolve, reject) => {
263
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 30000);
264
+
265
+ client.onDataChannel(peerTwinId, (ch) => {
266
+ clearTimeout(timeout);
267
+ channel = ch;
268
+ log(`Received default channel. Name: ${ch.getChannelName()}`);
269
+
270
+ ch.onMessage((data) => {
271
+ receivedCount++;
272
+ log(`[DC RECEIVED] ${JSON.stringify(data)}`);
273
+ ch.send({ echo: data });
274
+ });
275
+
276
+ setTimeout(() => {
277
+ addResult(testName, receivedCount >= MESSAGES_PER_TEST,
278
+ `Received ${receivedCount} messages`, startTime);
279
+ resolve();
280
+ }, 8000);
281
+ });
282
+ });
283
+ }
284
+ } catch (error: any) {
285
+ addResult(testName, false, error.message, startTime);
286
+ }
287
+ }
288
+
289
+ // =============================================================================
290
+ // Test 4: DataChannel Named Channels
291
+ // =============================================================================
292
+ async function testDataChannelNamed(
293
+ client: PhyHubClient,
294
+ peerTwinId: string,
295
+ isInitiator: boolean
296
+ ): Promise<void> {
297
+ const testName = 'DataChannel Named (P2P)';
298
+ const startTime = Date.now();
299
+ log(`\n=== Test 4: ${testName} ===`);
300
+
301
+ const channelNames = ['control', 'data'];
302
+ const receivedByChannel: Map<string, number> = new Map();
303
+
304
+ try {
305
+ if (isInitiator) {
306
+ const channels: PhygridDataChannel[] = [];
307
+
308
+ for (const name of channelNames) {
309
+ const ch = await client.getDataChannel(peerTwinId, name);
310
+ log(`Created channel '${name}'. Reported name: ${ch.getChannelName()}`);
311
+
312
+ if (ch.getChannelName() !== name) {
313
+ throw new Error(`Expected channel name '${name}', got '${ch.getChannelName()}'`);
314
+ }
315
+
316
+ receivedByChannel.set(name, 0);
317
+ ch.onMessage(() => {
318
+ receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1);
319
+ });
320
+ channels.push(ch);
321
+ }
322
+
323
+ await sleep(3000);
324
+
325
+ for (let i = 0; i < channels.length; i++) {
326
+ const ch = channels[i];
327
+ for (let j = 0; j < MESSAGES_PER_TEST; j++) {
328
+ ch.send({ channel: channelNames[i], num: j });
329
+ await sleep(100);
330
+ }
331
+ }
332
+
333
+ await sleep(3000);
334
+
335
+ let allPassed = true;
336
+ for (const name of channelNames) {
337
+ const count = receivedByChannel.get(name) || 0;
338
+ if (count < MESSAGES_PER_TEST) allPassed = false;
339
+ }
340
+
341
+ const details = channelNames.map(n => `${n}:${receivedByChannel.get(n)}`).join(', ');
342
+ addResult(testName, allPassed, details, startTime);
343
+
344
+ channels.forEach(ch => ch.close());
345
+ } else {
346
+ const promises = channelNames.map(name => new Promise<void>((resolve, reject) => {
347
+ const timeout = setTimeout(() => reject(new Error(`Timeout for ${name}`)), 30000);
348
+ receivedByChannel.set(name, 0);
349
+
350
+ client.onDataChannel(peerTwinId, (ch) => {
351
+ clearTimeout(timeout);
352
+ log(`Received channel '${name}'. Reported: ${ch.getChannelName()}`);
353
+
354
+ ch.onMessage((data) => {
355
+ receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1);
356
+ ch.send({ echo: data });
357
+ });
358
+
359
+ setTimeout(resolve, 10000);
360
+ }, name);
361
+ }));
362
+
363
+ await Promise.all(promises);
364
+
365
+ const details = channelNames.map(n => `${n}:${receivedByChannel.get(n)}`).join(', ');
366
+ const allPassed = channelNames.every(n => (receivedByChannel.get(n) || 0) >= MESSAGES_PER_TEST);
367
+ addResult(testName, allPassed, details, startTime);
368
+ }
369
+ } catch (error: any) {
370
+ addResult(testName, false, error.message, startTime);
371
+ }
372
+ }
373
+
374
+ // =============================================================================
375
+ // Test 5: MediaStream Connection
376
+ // =============================================================================
377
+ async function testMediaStreamConnection(
378
+ client: PhyHubClient,
379
+ peerTwinId: string,
380
+ isInitiator: boolean
381
+ ): Promise<void> {
382
+ const testName = 'MediaStream (P2P)';
383
+ const startTime = Date.now();
384
+ log(`\n=== Test 5: ${testName} ===`);
385
+
386
+ try {
387
+ if (isInitiator) {
388
+ const { stream, close } = await client.getMediaStream(peerTwinId);
389
+ log(`MediaStream created. Target: ${stream.getTargetTwinId()}, Channel: ${stream.getChannelName()}`);
390
+
391
+ if (stream.getChannelName() !== 'default') {
392
+ throw new Error(`Expected channel name 'default', got '${stream.getChannelName()}'`);
393
+ }
394
+
395
+ stream.onTrack((track) => {
396
+ log(`Received track: ${track.kind}`);
397
+ });
398
+
399
+ await sleep(5000);
400
+
401
+ addResult(testName, true,
402
+ `Connected to ${stream.getTargetTwinId()}`, startTime);
403
+
404
+ close();
405
+ } else {
406
+ await new Promise<void>((resolve, reject) => {
407
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 30000);
408
+
409
+ client.onMediaStream(peerTwinId, (stream) => {
410
+ clearTimeout(timeout);
411
+ log(`MediaStream received. Channel: ${stream.getChannelName()}`);
412
+
413
+ stream.onTrack((track) => {
414
+ log(`Track received: ${track.kind}`);
415
+ });
416
+
417
+ setTimeout(() => {
418
+ addResult(testName, true,
419
+ `Connected from ${stream.getTargetTwinId()}`, startTime);
420
+ resolve();
421
+ }, 5000);
422
+ });
423
+ });
424
+ }
425
+ } catch (error: any) {
426
+ addResult(testName, false, error.message, startTime);
427
+ }
428
+ }
429
+
430
+ // =============================================================================
431
+ // Main
432
+ // =============================================================================
433
+ async function main(): Promise<void> {
434
+ console.log('='.repeat(65));
435
+ console.log(' Communication Comprehensive Test Suite');
436
+ console.log(' Hub-based: Events, Actions | P2P: DataChannels, MediaStreams');
437
+ console.log('='.repeat(65));
438
+
439
+ if (process.env.PHYHUB_DIRECT !== 'true') {
440
+ console.error('ERROR: Set PHYHUB_DIRECT=true');
441
+ process.exit(1);
442
+ }
443
+
444
+ const peerTwinId = process.env.PEER_TWIN_ID;
445
+ const role = process.env.ROLE?.toLowerCase();
446
+ const testFilter = process.env.TEST;
447
+
448
+ if (!peerTwinId) {
449
+ console.error('ERROR: Set PEER_TWIN_ID');
450
+ process.exit(1);
451
+ }
452
+
453
+ if (role !== 'initiator' && role !== 'responder') {
454
+ console.error('ERROR: Set ROLE to initiator or responder');
455
+ process.exit(1);
456
+ }
457
+
458
+ const isInitiator = role === 'initiator';
459
+ log(`Role: ${role}`);
460
+ log(`Peer Twin ID: ${peerTwinId}`);
461
+ if (testFilter) log(`Running only: ${testFilter}`);
462
+
463
+ try {
464
+ log('\nConnecting to PhyHub...');
465
+ const client = await PhyHubClient.connect();
466
+ log('Connected!');
467
+
468
+ const instance = await client.getInstance();
469
+ log(`Instance ID: ${instance.id}\n`);
470
+
471
+ // Define tests
472
+ type TestFunction = (
473
+ client: PhyHubClient,
474
+ instanceOrPeer: any,
475
+ peerOrBool: any,
476
+ isInitiator?: boolean
477
+ ) => Promise<void>;
478
+
479
+ const tests: Array<{ name: string; fn: TestFunction; needsInstance: boolean }> = [
480
+ { name: 'events', fn: testEvents as TestFunction, needsInstance: true },
481
+ { name: 'actions', fn: testActions as TestFunction, needsInstance: true },
482
+ { name: 'datachannel', fn: testDataChannelDefault as TestFunction, needsInstance: false },
483
+ { name: 'datachannel-named', fn: testDataChannelNamed as TestFunction, needsInstance: false },
484
+ { name: 'mediastream', fn: testMediaStreamConnection as TestFunction, needsInstance: false },
485
+ ];
486
+
487
+ for (const test of tests) {
488
+ if (!testFilter || test.name === testFilter) {
489
+ if (test.needsInstance) {
490
+ await test.fn(client, instance, peerTwinId, isInitiator);
491
+ } else {
492
+ await test.fn(client, peerTwinId, isInitiator, undefined);
493
+ }
494
+ await sleep(2000);
495
+ }
496
+ }
497
+
498
+ // Print summary
499
+ console.log('\n' + '='.repeat(65));
500
+ console.log('TEST SUMMARY');
501
+ console.log('='.repeat(65));
502
+ console.log('Test'.padEnd(40) + 'Status'.padEnd(10) + 'Time');
503
+ console.log('-'.repeat(65));
504
+
505
+ let passed = 0;
506
+ let failed = 0;
507
+ for (const r of results) {
508
+ const status = r.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
509
+ console.log(r.name.padEnd(40) + status.padEnd(15) + `${r.duration}ms`);
510
+ if (r.passed) passed++;
511
+ else failed++;
512
+ }
513
+
514
+ console.log('-'.repeat(65));
515
+ const totalTime = results.reduce((sum, r) => sum + r.duration, 0);
516
+ console.log(`Total: ${passed} passed, ${failed} failed (${totalTime}ms)`);
517
+ console.log('='.repeat(65));
518
+
519
+ // Give time for final cleanup
520
+ await sleep(1000);
521
+
522
+ process.exit(failed > 0 ? 1 : 0);
523
+ } catch (error) {
524
+ console.error('\n[FATAL]', error);
525
+ process.exit(1);
526
+ }
527
+ }
528
+
529
+ if (require.main === module) {
530
+ main();
531
+ }
532
+
533
+ export { testEvents, testActions, testDataChannelDefault, testDataChannelNamed, testMediaStreamConnection };