@phystack/hub-client 4.5.19-dev → 4.5.20-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,345 @@
1
+ /**
2
+ * WebRTC Reconnection Test
3
+ *
4
+ * Tests that WebRTC DataChannel handles disconnection and reconnection seamlessly:
5
+ * - Messages continue flowing after reconnection
6
+ * - Handlers persist across reconnections (no need to reattach)
7
+ * - Messages sent during disconnection are buffered and delivered
8
+ *
9
+ * Usage:
10
+ * Terminal 1 (Responder - start first):
11
+ * PHYHUB_DIRECT=true DEVICE_ID=<id> ACCESS_KEY=<key> PEER_TWIN_ID=<peer> ROLE=responder \
12
+ * node dist/test/webrtc-reconnect-test.js
13
+ *
14
+ * Terminal 2 (Initiator):
15
+ * PHYHUB_DIRECT=true DEVICE_ID=<id> ACCESS_KEY=<key> PEER_TWIN_ID=<peer> ROLE=initiator \
16
+ * node dist/test/webrtc-reconnect-test.js
17
+ */
18
+
19
+ import { PhyHubClient } from '../index';
20
+ import { PhygridDataChannel } from '../services/webrtc/types';
21
+
22
+ const TEST_DURATION_MS = 120000; // 2 minutes
23
+ const MESSAGE_INTERVAL_MS = 2000;
24
+ const DISCONNECT_AFTER_MESSAGES = 5;
25
+
26
+ interface TestMessage {
27
+ seq: number;
28
+ timestamp: number;
29
+ phase: 'initial' | 'after-reconnect';
30
+ from: string;
31
+ }
32
+
33
+ interface TestStats {
34
+ messagesSent: number;
35
+ messagesReceived: number;
36
+ reconnectCount: number;
37
+ errors: string[];
38
+ phases: Set<string>;
39
+ }
40
+
41
+ function sleep(ms: number): Promise<void> {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
45
+ async function runInitiatorTest(peerTwinId: string): Promise<void> {
46
+ console.log('\n=== INITIATOR: Reconnection Test ===\n');
47
+
48
+ const stats: TestStats = {
49
+ messagesSent: 0,
50
+ messagesReceived: 0,
51
+ reconnectCount: 0,
52
+ errors: [],
53
+ phases: new Set(),
54
+ };
55
+
56
+ // Connect to PhyHub
57
+ console.log('[INIT] Connecting to PhyHub...');
58
+ const client = await PhyHubClient.connect();
59
+ console.log('[INIT] Connected!\n');
60
+
61
+ const manager = await client.getWebRTCManager({ verbose: true });
62
+
63
+ // Track reconnection events
64
+ manager.on('connected', (data) => {
65
+ console.log(`[EVENT] Connected to ${data.targetTwinId}`);
66
+ });
67
+
68
+ manager.on('disconnected', (data) => {
69
+ console.log(`[EVENT] Disconnected from ${data.targetTwinId}`);
70
+ });
71
+
72
+ manager.on('reconnecting', (data) => {
73
+ stats.reconnectCount++;
74
+ console.log(`[EVENT] Reconnecting to ${data.targetTwinId} (attempt ${data.attempt})`);
75
+ });
76
+
77
+ manager.on('reconnected', (data) => {
78
+ console.log(`[EVENT] Reconnected to ${data.targetTwinId}`);
79
+ });
80
+
81
+ manager.on('error', (data) => {
82
+ stats.errors.push(data.error.message);
83
+ console.error(`[EVENT] Error:`, data.error.message);
84
+ });
85
+
86
+ // Create data channel
87
+ console.log(`[INIT] Creating DataChannel to: ${peerTwinId}`);
88
+ const channel = await client.getDataChannel(peerTwinId);
89
+ console.log('[INIT] DataChannel created!\n');
90
+
91
+ // Setup message handler ONCE - it should persist across reconnections
92
+ channel.onMessage((data: TestMessage) => {
93
+ stats.messagesReceived++;
94
+ stats.phases.add(data.phase);
95
+ console.log(`[RECV] seq=${data.seq} phase=${data.phase} from=${data.from}`);
96
+ });
97
+
98
+ // Test phases
99
+ let phase: 'initial' | 'after-reconnect' = 'initial';
100
+ let seq = 0;
101
+
102
+ console.log('[TEST] Starting message loop...\n');
103
+ console.log('[INFO] The peer will simulate a disconnect after receiving messages.');
104
+ console.log('[INFO] Messages should continue after reconnection automatically.\n');
105
+
106
+ const startTime = Date.now();
107
+
108
+ while (Date.now() - startTime < TEST_DURATION_MS) {
109
+ seq++;
110
+ const msg: TestMessage = {
111
+ seq,
112
+ timestamp: Date.now(),
113
+ phase,
114
+ from: 'initiator',
115
+ };
116
+
117
+ // Use channel.send - it should buffer if disconnected
118
+ channel.send(msg);
119
+ stats.messagesSent++;
120
+ console.log(`[SEND] seq=${seq} phase=${phase}`);
121
+
122
+ // After reconnect events, switch phase
123
+ if (stats.reconnectCount > 0 && phase === 'initial') {
124
+ phase = 'after-reconnect';
125
+ console.log('\n[PHASE] Switched to "after-reconnect" phase\n');
126
+ }
127
+
128
+ await sleep(MESSAGE_INTERVAL_MS);
129
+
130
+ // Check if we've received messages from both phases (success condition)
131
+ if (stats.phases.has('after-reconnect') && stats.messagesReceived > 10) {
132
+ console.log('\n[SUCCESS] Received messages from both phases!');
133
+ break;
134
+ }
135
+ }
136
+
137
+ // Print results
138
+ console.log('\n' + '='.repeat(50));
139
+ console.log('Test Results (Initiator)');
140
+ console.log('='.repeat(50));
141
+ console.log(`Messages sent: ${stats.messagesSent}`);
142
+ console.log(`Messages received: ${stats.messagesReceived}`);
143
+ console.log(`Reconnect events: ${stats.reconnectCount}`);
144
+ console.log(`Phases seen: ${Array.from(stats.phases).join(', ')}`);
145
+ console.log(`Errors: ${stats.errors.length > 0 ? stats.errors.join(', ') : 'None'}`);
146
+ console.log('='.repeat(50));
147
+
148
+ if (stats.phases.has('after-reconnect') && stats.messagesReceived > 5) {
149
+ console.log('\n[PASS] Reconnection test passed!');
150
+ } else {
151
+ console.log('\n[FAIL] Reconnection test failed - did not receive messages after reconnect');
152
+ }
153
+
154
+ channel.close();
155
+ }
156
+
157
+ async function runResponderTest(peerTwinId: string): Promise<void> {
158
+ console.log('\n=== RESPONDER: Reconnection Test ===\n');
159
+
160
+ const stats: TestStats = {
161
+ messagesSent: 0,
162
+ messagesReceived: 0,
163
+ reconnectCount: 0,
164
+ errors: [],
165
+ phases: new Set(),
166
+ };
167
+
168
+ // Connect to PhyHub
169
+ console.log('[INIT] Connecting to PhyHub...');
170
+ const client = await PhyHubClient.connect();
171
+ console.log('[INIT] Connected!\n');
172
+
173
+ const manager = await client.getWebRTCManager({ verbose: true });
174
+
175
+ // Track reconnection events
176
+ manager.on('connected', (data) => {
177
+ console.log(`[EVENT] Connected to ${data.targetTwinId}`);
178
+ });
179
+
180
+ manager.on('disconnected', (data) => {
181
+ console.log(`[EVENT] Disconnected from ${data.targetTwinId}`);
182
+ });
183
+
184
+ manager.on('reconnecting', (data) => {
185
+ stats.reconnectCount++;
186
+ console.log(`[EVENT] Reconnecting to ${data.targetTwinId} (attempt ${data.attempt})`);
187
+ });
188
+
189
+ manager.on('reconnected', (data) => {
190
+ console.log(`[EVENT] Reconnected to ${data.targetTwinId}`);
191
+ });
192
+
193
+ manager.on('error', (data) => {
194
+ stats.errors.push(data.error.message);
195
+ console.error(`[EVENT] Error:`, data.error.message);
196
+ });
197
+
198
+ console.log(`[INIT] Waiting for DataChannel from: ${peerTwinId}\n`);
199
+
200
+ return new Promise((resolve, reject) => {
201
+ const timeout = setTimeout(() => {
202
+ reject(new Error('Timeout waiting for connection'));
203
+ }, TEST_DURATION_MS);
204
+
205
+ let phase: 'initial' | 'after-reconnect' = 'initial';
206
+ let disconnectSimulated = false;
207
+
208
+ // Setup handler ONCE - this is the key test:
209
+ // The handler should persist across any reconnections
210
+ client.onDataChannel(peerTwinId, (ch: PhygridDataChannel) => {
211
+ console.log('[INIT] DataChannel received!\n');
212
+
213
+ // Setup message handler - should only need to do this ONCE
214
+ ch.onMessage((data: TestMessage) => {
215
+ stats.messagesReceived++;
216
+ stats.phases.add(data.phase);
217
+ console.log(`[RECV] seq=${data.seq} phase=${data.phase} from=${data.from}`);
218
+
219
+ // Echo back
220
+ const response: TestMessage = {
221
+ seq: stats.messagesSent + 1,
222
+ timestamp: Date.now(),
223
+ phase,
224
+ from: 'responder',
225
+ };
226
+ ch.send(response);
227
+ stats.messagesSent++;
228
+ console.log(`[SEND] seq=${response.seq} phase=${phase}`);
229
+
230
+ // Simulate disconnect after receiving some messages
231
+ if (stats.messagesReceived === DISCONNECT_AFTER_MESSAGES && !disconnectSimulated) {
232
+ disconnectSimulated = true;
233
+ console.log('\n[TEST] Simulating network disconnect...');
234
+ console.log('[TEST] Closing peer connection to trigger reconnection...\n');
235
+
236
+ // Access the internal peer connection and close it to simulate disconnect
237
+ // This should trigger automatic reconnection
238
+ try {
239
+ const webrtcManager = (client as any).webrtcManager;
240
+ if (webrtcManager) {
241
+ const handler = webrtcManager.dataChannelHandlers?.get(peerTwinId);
242
+ if (handler) {
243
+ const pcManager = (handler as any).pcManager;
244
+ if (pcManager) {
245
+ const pc = pcManager.getPeerConnection();
246
+ if (pc) {
247
+ console.log('[TEST] Closing RTCPeerConnection...');
248
+ pc.close();
249
+ }
250
+ }
251
+ }
252
+ }
253
+ } catch (err) {
254
+ console.error('[TEST] Error simulating disconnect:', err);
255
+ }
256
+ }
257
+
258
+ // After reconnect, update phase
259
+ if (stats.reconnectCount > 0 && phase === 'initial') {
260
+ phase = 'after-reconnect';
261
+ console.log('\n[PHASE] Switched to "after-reconnect" phase\n');
262
+ }
263
+
264
+ // Success condition: received messages in both phases
265
+ if (stats.phases.has('after-reconnect') && stats.messagesReceived > 10) {
266
+ clearTimeout(timeout);
267
+
268
+ // Print results
269
+ console.log('\n' + '='.repeat(50));
270
+ console.log('Test Results (Responder)');
271
+ console.log('='.repeat(50));
272
+ console.log(`Messages sent: ${stats.messagesSent}`);
273
+ console.log(`Messages received: ${stats.messagesReceived}`);
274
+ console.log(`Reconnect events: ${stats.reconnectCount}`);
275
+ console.log(`Phases seen: ${Array.from(stats.phases).join(', ')}`);
276
+ console.log(`Errors: ${stats.errors.length > 0 ? stats.errors.join(', ') : 'None'}`);
277
+ console.log('='.repeat(50));
278
+
279
+ if (stats.phases.has('after-reconnect')) {
280
+ console.log('\n[PASS] Reconnection test passed!');
281
+ } else {
282
+ console.log('\n[FAIL] Reconnection test failed');
283
+ }
284
+
285
+ resolve();
286
+ }
287
+ });
288
+
289
+ ch.onClose(() => {
290
+ console.log('[EVENT] Channel closed');
291
+ });
292
+ }).catch(reject);
293
+ });
294
+ }
295
+
296
+ async function main(): Promise<void> {
297
+ console.log('='.repeat(60));
298
+ console.log('WebRTC Reconnection Test');
299
+ console.log('='.repeat(60));
300
+
301
+ // Validate environment
302
+ if (process.env.PHYHUB_DIRECT !== 'true') {
303
+ console.error('ERROR: Set PHYHUB_DIRECT=true');
304
+ process.exit(1);
305
+ }
306
+
307
+ const deviceId = process.env.DEVICE_ID || process.env.PHYGRID_DEVICE_ID;
308
+ const accessKey = process.env.ACCESS_KEY || process.env.PHYGRID_DEVICE_KEY;
309
+ const peerTwinId = process.env.PEER_TWIN_ID;
310
+ const role = process.env.ROLE?.toLowerCase();
311
+
312
+ if (!deviceId || !accessKey || !peerTwinId) {
313
+ console.error('ERROR: Missing DEVICE_ID, ACCESS_KEY, or PEER_TWIN_ID');
314
+ process.exit(1);
315
+ }
316
+
317
+ if (role !== 'initiator' && role !== 'responder') {
318
+ console.error('ERROR: ROLE must be "initiator" or "responder"');
319
+ process.exit(1);
320
+ }
321
+
322
+ console.log('\nConfiguration:');
323
+ console.log(` Device ID: ${deviceId}`);
324
+ console.log(` Peer Twin ID: ${peerTwinId}`);
325
+ console.log(` Role: ${role}`);
326
+ console.log('');
327
+
328
+ try {
329
+ if (role === 'initiator') {
330
+ await runInitiatorTest(peerTwinId);
331
+ } else {
332
+ await runResponderTest(peerTwinId);
333
+ }
334
+ process.exit(0);
335
+ } catch (error) {
336
+ console.error('\n[FAILED]', error);
337
+ process.exit(1);
338
+ }
339
+ }
340
+
341
+ if (require.main === module) {
342
+ main();
343
+ }
344
+
345
+ export { runInitiatorTest, runResponderTest };
@@ -0,0 +1,254 @@
1
+ /**
2
+ * WebRTC Test Harness
3
+ *
4
+ * A utility for testing WebRTC DataChannel and MediaStream connections
5
+ * between two twins using direct PhyHub connection (bypassing phyos).
6
+ *
7
+ * Prerequisites:
8
+ * 1. Get device credentials from CLI:
9
+ * phy dev list
10
+ * phy dev select # Select a device
11
+ * phy dev develop --export # Get PHYGRID_DEVICE_ID and PHYGRID_DEVICE_KEY
12
+ *
13
+ * 2. You need TWO devices for testing WebRTC between them
14
+ *
15
+ * Usage (two terminals):
16
+ *
17
+ * Terminal 1 (Device A - initiator):
18
+ * ```
19
+ * PHYHUB_DIRECT=true \
20
+ * DEVICE_ID=<device-a-id> \
21
+ * ACCESS_KEY=<device-a-key> \
22
+ * PEER_TWIN_ID=<device-b-id> \
23
+ * ROLE=initiator \
24
+ * npx ts-node src/test/webrtc-test-harness.ts
25
+ * ```
26
+ *
27
+ * Terminal 2 (Device B - responder):
28
+ * ```
29
+ * PHYHUB_DIRECT=true \
30
+ * DEVICE_ID=<device-b-id> \
31
+ * ACCESS_KEY=<device-b-key> \
32
+ * PEER_TWIN_ID=<device-a-id> \
33
+ * ROLE=responder \
34
+ * npx ts-node src/test/webrtc-test-harness.ts
35
+ * ```
36
+ *
37
+ * Environment variables:
38
+ * PHYHUB_DIRECT - Must be 'true' to enable direct connection
39
+ * DEVICE_ID - Your device ID (also used as twin ID)
40
+ * ACCESS_KEY - Your device access key
41
+ * PEER_TWIN_ID - The twin ID of the peer to connect to
42
+ * ROLE - 'initiator' or 'responder'
43
+ * PHYHUB_URL - PhyHub URL (optional, defaults to EU region)
44
+ * PHYHUB_REGION - 'eu', 'us', or 'local' (optional)
45
+ */
46
+
47
+ import { PhyHubClient } from '../index';
48
+ import { PhygridDataChannel } from '../services/webrtc/types';
49
+
50
+ async function runInitiator(peerTwinId: string): Promise<void> {
51
+ console.log('\n=== Running as INITIATOR ===\n');
52
+
53
+ // Connect to PhyHub
54
+ console.log('Connecting to PhyHub...');
55
+ const client = await PhyHubClient.connect();
56
+ console.log('Connected!\n');
57
+
58
+ // Get WebRTC manager with verbose mode
59
+ const manager = await client.getWebRTCManager({ verbose: true });
60
+
61
+ // Setup event listeners
62
+ manager.on('connected', (data) => {
63
+ console.log(`[EVENT] Connected to ${data.targetTwinId} (${data.connectionType})`);
64
+ });
65
+ manager.on('disconnected', (data) => {
66
+ console.log(`[EVENT] Disconnected from ${data.targetTwinId}`);
67
+ });
68
+ manager.on('error', (data) => {
69
+ console.error(`[EVENT] Error:`, data.error);
70
+ });
71
+ manager.on('reconnecting', (data) => {
72
+ console.log(`[EVENT] Reconnecting to ${data.targetTwinId} (attempt ${data.attempt})`);
73
+ });
74
+
75
+ console.log(`Creating DataChannel to peer: ${peerTwinId}`);
76
+ console.log('Waiting for responder to be ready...\n');
77
+
78
+ try {
79
+ const channel = await client.getDataChannel(peerTwinId);
80
+ console.log('DataChannel created successfully!\n');
81
+
82
+ // Setup message handler
83
+ channel.onMessage((data) => {
84
+ console.log('[RECEIVED]', data);
85
+ });
86
+
87
+ // Send test messages
88
+ console.log('Sending test messages...\n');
89
+
90
+ for (let i = 1; i <= 5; i++) {
91
+ const msg = { type: 'test', count: i, timestamp: Date.now() };
92
+ console.log('[SENDING]', msg);
93
+ channel.send(msg);
94
+ await sleep(1000);
95
+ }
96
+
97
+ // Wait for responses
98
+ console.log('\nWaiting for responses (10 seconds)...');
99
+ await sleep(10000);
100
+
101
+ console.log('\nTest complete. Closing channel...');
102
+ channel.close();
103
+ } catch (error) {
104
+ console.error('Failed to create DataChannel:', error);
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ async function runResponder(peerTwinId: string): Promise<void> {
110
+ console.log('\n=== Running as RESPONDER ===\n');
111
+
112
+ // Connect to PhyHub
113
+ console.log('Connecting to PhyHub...');
114
+ const client = await PhyHubClient.connect();
115
+ console.log('Connected!\n');
116
+
117
+ // Get WebRTC manager with verbose mode
118
+ const manager = await client.getWebRTCManager({ verbose: true });
119
+
120
+ // Setup event listeners
121
+ manager.on('connected', (data) => {
122
+ console.log(`[EVENT] Connected to ${data.targetTwinId} (${data.connectionType})`);
123
+ });
124
+ manager.on('disconnected', (data) => {
125
+ console.log(`[EVENT] Disconnected from ${data.targetTwinId}`);
126
+ });
127
+ manager.on('error', (data) => {
128
+ console.error(`[EVENT] Error:`, data.error);
129
+ });
130
+
131
+ console.log(`Waiting for DataChannel from peer: ${peerTwinId}`);
132
+ console.log('Ready to accept connection...\n');
133
+
134
+ return new Promise((resolve, reject) => {
135
+ const timeout = setTimeout(() => {
136
+ reject(new Error('Timeout waiting for incoming connection (60s)'));
137
+ }, 60000);
138
+
139
+ client.onDataChannel(peerTwinId, (channel: PhygridDataChannel) => {
140
+ clearTimeout(timeout);
141
+ console.log('DataChannel received!\n');
142
+
143
+ let messageCount = 0;
144
+
145
+ channel.onMessage((data) => {
146
+ console.log('[RECEIVED]', data);
147
+ messageCount++;
148
+
149
+ // Echo back with modification
150
+ const response = {
151
+ type: 'echo',
152
+ original: data,
153
+ respondedAt: Date.now(),
154
+ };
155
+ console.log('[SENDING]', response);
156
+ channel.send(response);
157
+ });
158
+
159
+ channel.onClose(() => {
160
+ console.log(`\nChannel closed. Received ${messageCount} messages.`);
161
+ resolve();
162
+ });
163
+ }).catch(reject);
164
+ });
165
+ }
166
+
167
+ function sleep(ms: number): Promise<void> {
168
+ return new Promise((resolve) => setTimeout(resolve, ms));
169
+ }
170
+
171
+ async function main(): Promise<void> {
172
+ console.log('='.repeat(60));
173
+ console.log('WebRTC Test Harness');
174
+ console.log('='.repeat(60));
175
+
176
+ // Validate environment
177
+ if (process.env.PHYHUB_DIRECT !== 'true') {
178
+ console.error(`
179
+ ERROR: Direct connection mode not enabled.
180
+
181
+ Set PHYHUB_DIRECT=true to enable direct PhyHub connection.
182
+ `);
183
+ process.exit(1);
184
+ }
185
+
186
+ const deviceId = process.env.DEVICE_ID || process.env.PHYGRID_DEVICE_ID;
187
+ const accessKey = process.env.ACCESS_KEY || process.env.PHYGRID_DEVICE_KEY;
188
+ const peerTwinId = process.env.PEER_TWIN_ID;
189
+ const role = process.env.ROLE?.toLowerCase();
190
+
191
+ if (!deviceId || !accessKey) {
192
+ console.error(`
193
+ ERROR: Missing device credentials.
194
+
195
+ Required environment variables:
196
+ DEVICE_ID (or PHYGRID_DEVICE_ID) - Your device ID
197
+ ACCESS_KEY (or PHYGRID_DEVICE_KEY) - Your device access key
198
+
199
+ To get these, run:
200
+ phy dev list
201
+ phy dev select
202
+ phy dev develop --export
203
+ `);
204
+ process.exit(1);
205
+ }
206
+
207
+ if (!peerTwinId) {
208
+ console.error(`
209
+ ERROR: Missing peer twin ID.
210
+
211
+ Set PEER_TWIN_ID to the twin ID of the device you want to connect to.
212
+ `);
213
+ process.exit(1);
214
+ }
215
+
216
+ if (role !== 'initiator' && role !== 'responder') {
217
+ console.error(`
218
+ ERROR: Invalid or missing ROLE.
219
+
220
+ Set ROLE to either:
221
+ - 'initiator' - The side that creates the DataChannel
222
+ - 'responder' - The side that waits for the DataChannel
223
+ `);
224
+ process.exit(1);
225
+ }
226
+
227
+ console.log('\nConfiguration:');
228
+ console.log(` Device ID: ${deviceId}`);
229
+ console.log(` Peer Twin ID: ${peerTwinId}`);
230
+ console.log(` Role: ${role}`);
231
+ console.log(` PhyHub URL: ${process.env.PHYHUB_URL || '(default EU)'}`);
232
+ console.log('');
233
+
234
+ try {
235
+ if (role === 'initiator') {
236
+ await runInitiator(peerTwinId);
237
+ } else {
238
+ await runResponder(peerTwinId);
239
+ }
240
+
241
+ console.log('\n[SUCCESS] Test completed successfully!');
242
+ process.exit(0);
243
+ } catch (error) {
244
+ console.error('\n[FAILED] Test failed:', error);
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ // Run if executed directly
250
+ if (require.main === module) {
251
+ main();
252
+ }
253
+
254
+ export { runInitiator, runResponder };