@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
@@ -1,689 +0,0 @@
1
- // Initialize WebRTC globals if we're in Node environment
2
- (async function initializeWebRTCGlobals() {
3
- // Only run in Node environment
4
- if (typeof window !== 'undefined') return;
5
-
6
- try {
7
- // Dynamically import wrtc
8
- const wrtc = require('@roamhq/wrtc');
9
-
10
- // Set all WebRTC globals
11
- if (typeof global.MediaStream === 'undefined' && wrtc.MediaStream) {
12
- global.MediaStream = wrtc.MediaStream;
13
- console.log('Global MediaStream initialized');
14
- }
15
-
16
- if (typeof global.RTCDataChannel === 'undefined' && wrtc.RTCDataChannel) {
17
- global.RTCDataChannel = wrtc.RTCDataChannel;
18
- console.log('Global RTCDataChannel initialized');
19
- }
20
-
21
- if (typeof global.RTCPeerConnection === 'undefined' && wrtc.RTCPeerConnection) {
22
- global.RTCPeerConnection = wrtc.RTCPeerConnection;
23
- console.log('Global RTCPeerConnection initialized');
24
- }
25
-
26
- if (typeof global.RTCSessionDescription === 'undefined' && wrtc.RTCSessionDescription) {
27
- global.RTCSessionDescription = wrtc.RTCSessionDescription;
28
- console.log('Global RTCSessionDescription initialized');
29
- }
30
-
31
- if (typeof global.RTCIceCandidate === 'undefined' && wrtc.RTCIceCandidate) {
32
- global.RTCIceCandidate = wrtc.RTCIceCandidate;
33
- console.log('Global RTCIceCandidate initialized');
34
- }
35
-
36
- console.log('WebRTC globals successfully initialized');
37
- } catch (error) {
38
- console.error('Failed to initialize WebRTC globals:', error);
39
- }
40
- })().catch(err => console.error('Error in WebRTC globals initialization:', err));
41
-
42
- import { WebRTCConnectionOptions } from '../../types/webrtc.types';
43
-
44
- export interface PeerConnectionState {
45
- pc: RTCPeerConnection | null;
46
- isShuttingDown: boolean;
47
- isReconnecting: boolean;
48
- currentConnectionId: number;
49
- pendingCandidates: RTCIceCandidateInit[];
50
- activeDataChannel: RTCDataChannel | null;
51
- activeMediaStream?: MediaStream | null;
52
- useStun?: boolean;
53
- channelPrefix?: string;
54
- targetTwinId?: string;
55
- isInitiator?: boolean;
56
- connectionType?: 'datachannel' | 'mediastream';
57
- }
58
-
59
- export interface PeerConnectionCallbacks {
60
- onCreateConnection: (
61
- pc: RTCPeerConnection,
62
- connectionId: number,
63
- isInitiator: boolean
64
- ) => Promise<RTCDataChannel | MediaStream | null>;
65
-
66
- onCloseCleanup: () => void;
67
- onSubscribe: () => Promise<void>;
68
- sendSignalingMessage: (type: string, data: any) => Promise<void>;
69
- setupMessageHandling: (messageHandler: (message: any) => void) => void;
70
- removeMessageHandling: (messageHandler: (message: any) => void) => void;
71
- reportConnectionState: (info: string) => void;
72
- reportError: (info: string, error?: any) => void;
73
- retryConnection: (retryCount: number, useStun?: boolean, delay?: number) => Promise<unknown>;
74
- safeReject: (error: Error) => void;
75
- }
76
-
77
- /**
78
- * Interface for RTCPeerConnection with optional experimental properties
79
- */
80
- interface ExtendedRTCPeerConnection extends RTCPeerConnection {
81
- // This is an experimental property that might not be available in all browsers/environments
82
- onicecandidatepairchange?: (event: Event) => void;
83
- }
84
-
85
- /**
86
- * Create a new RTCPeerConnection with or without STUN servers based on flags.
87
- */
88
- export function createPeerConnection(useStun: boolean): RTCPeerConnection {
89
- const pc = new RTCPeerConnection({
90
- iceServers: useStun
91
- ? [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }]
92
- : [],
93
- });
94
- return pc;
95
- }
96
-
97
- /**
98
- * Set up ICE candidate handlers and connection state handlers
99
- * on the newly created RTCPeerConnection.
100
- */
101
- export function handleIceAndConnectionState(
102
- state: PeerConnectionState,
103
- pc: RTCPeerConnection,
104
- callbacks: PeerConnectionCallbacks,
105
- connectionId: number
106
- ) {
107
- const extendedPc = pc as ExtendedRTCPeerConnection;
108
-
109
- pc.onicecandidate = async event => {
110
- if (!pc) return;
111
- // Only send ICE if this is still the active connection
112
- if (event.candidate && state.currentConnectionId === connectionId) {
113
- try {
114
- // Log local ICE candidate info
115
- logIceCandidate('Local ICE candidate', event.candidate);
116
- await callbacks.sendSignalingMessage('ice', event.candidate);
117
- } catch (err) {
118
- // Swallow errors from ICE sending attempts
119
- callbacks.reportError('Error sending ICE candidate:', err);
120
- }
121
- } else if (event.candidate === null) {
122
- callbacks.reportConnectionState('ICE gathering complete');
123
- }
124
- };
125
-
126
- // Add event handler for ICE candidate pair selection - this is an experimental feature
127
- // and might not be available in all browsers/environments
128
- extendedPc.onicecandidatepairchange = (_event: Event) => {
129
- callbacks.reportConnectionState('ICE candidate pair changed');
130
-
131
- // Get stats to determine the selected candidate pair
132
- pc.getStats()
133
- .then(stats => {
134
- // Use any type for the stats objects to avoid TypeScript errors
135
- // The RTCStatsReport interface varies across browsers and environments
136
- const statsValues: any[] = [];
137
- (stats as any).forEach((stat: any) => {
138
- statsValues.push(stat);
139
- });
140
-
141
- // Find candidate pair and transport stats
142
- const transportStats = statsValues.find(s => s.type === 'transport');
143
- if (transportStats) {
144
- callbacks.reportConnectionState(
145
- `Transport selected: ${transportStats.selectedCandidatePairId}`
146
- );
147
- }
148
-
149
- const selectedPair = statsValues.find(s => s.type === 'candidate-pair' && s.selected);
150
- if (selectedPair) {
151
- callbacks.reportConnectionState(`Selected ICE candidate pair [${selectedPair.id}]:
152
- local=${selectedPair.localCandidateId},
153
- remote=${selectedPair.remoteCandidateId},
154
- state=${selectedPair.state},
155
- priority=${selectedPair.priority}`);
156
-
157
- // Find the local and remote candidate details
158
- const localCandidate = statsValues.find(
159
- s => s.type === 'local-candidate' && s.id === selectedPair.localCandidateId
160
- );
161
-
162
- const remoteCandidate = statsValues.find(
163
- s => s.type === 'remote-candidate' && s.id === selectedPair.remoteCandidateId
164
- );
165
-
166
- if (localCandidate) {
167
- logIceCandidateStats('Selected local ICE candidate', localCandidate);
168
- }
169
-
170
- if (remoteCandidate) {
171
- logIceCandidateStats('Selected remote ICE candidate', remoteCandidate);
172
- }
173
- }
174
- })
175
- .catch(err => {
176
- callbacks.reportError('Error getting ICE candidate stats:', err);
177
- });
178
- };
179
-
180
- pc.onconnectionstatechange = () => {
181
- if (!pc) return;
182
- callbacks.reportConnectionState(`Connection state: ${pc.connectionState}`);
183
- };
184
-
185
- pc.oniceconnectionstatechange = () => {
186
- if (!pc) return;
187
- callbacks.reportConnectionState(`ICE connection state: ${pc.iceConnectionState}`);
188
-
189
- // When ICE connection state becomes 'connected', log the selected candidate pair
190
- if (pc.iceConnectionState === 'connected') {
191
- pc.getStats()
192
- .then(stats => {
193
- // Convert stats to array for easier processing
194
- const statsValues: any[] = [];
195
- (stats as any).forEach((stat: any) => {
196
- statsValues.push(stat);
197
- });
198
-
199
- const selectedPair = statsValues.find(s => s.type === 'candidate-pair' && s.selected);
200
- if (!selectedPair) return;
201
-
202
- const localCandidate = statsValues.find(
203
- s => s.type === 'local-candidate' && s.id === selectedPair.localCandidateId
204
- );
205
-
206
- const remoteCandidate = statsValues.find(
207
- s => s.type === 'remote-candidate' && s.id === selectedPair.remoteCandidateId
208
- );
209
-
210
- if (selectedPair && localCandidate && remoteCandidate) {
211
- callbacks.reportConnectionState('*** ICE CONNECTION ESTABLISHED ***');
212
- callbacks.reportConnectionState(`Selected candidates:
213
- Local: ${localCandidate.ip}:${localCandidate.port} (${localCandidate.protocol}) [${localCandidate.candidateType}]
214
- Remote: ${remoteCandidate.ip}:${remoteCandidate.port} (${remoteCandidate.protocol}) [${remoteCandidate.candidateType}]
215
- Round-trip time: ${selectedPair.currentRoundTripTime ? (selectedPair.currentRoundTripTime * 1000).toFixed(2) + 'ms' : 'unknown'}`);
216
- }
217
- })
218
- .catch(err => {
219
- callbacks.reportError('Error getting ICE candidate stats:', err);
220
- });
221
- }
222
- };
223
- }
224
-
225
- /**
226
- * Helper function to log ICE candidate details from RTCIceCandidate object
227
- */
228
- function logIceCandidate(label: string, candidate: RTCIceCandidate) {
229
- console.log(`${label}: ${candidate.candidate}`);
230
-
231
- // Extract additional details for more verbose logging
232
- // Example format: candidate:1467250027 1 udp 2122260223 192.168.0.1 56789 typ host generation 0
233
- try {
234
- const parts = candidate.candidate.split(' ');
235
- if (parts.length > 7) {
236
- const protocol = parts[2];
237
- const ip = parts[4];
238
- const port = parts[5];
239
- const type = parts[7];
240
- console.log(`Details: IP=${ip}, Port=${port}, Protocol=${protocol}, Type=${type}`);
241
- }
242
- } catch (e) {
243
- // Ignore parsing errors
244
- }
245
- }
246
-
247
- /**
248
- * Helper function to log ICE candidate details from stats object
249
- */
250
- function logIceCandidateStats(label: string, candidate: any) {
251
- if (!candidate) return;
252
-
253
- // Log basic information
254
- console.log(
255
- `${label}: ${candidate.ip}:${candidate.port} (${candidate.protocol}) [${candidate.candidateType}]`
256
- );
257
-
258
- // Log additional details if available
259
- if (candidate.url) console.log(`STUN/TURN server: ${candidate.url}`);
260
- if (candidate.priority) console.log(`Priority: ${candidate.priority}`);
261
- if (candidate.networkType) console.log(`Network type: ${candidate.networkType}`);
262
- }
263
-
264
- /**
265
- * Attempts to start a WebRTC connection, sets up the PeerConnection,
266
- * and calls data-channel creation logic when needed.
267
- */
268
- export async function attemptConnection(
269
- state: PeerConnectionState,
270
- options: WebRTCConnectionOptions,
271
- retryCount: number,
272
- currentUseStun: boolean,
273
- delay: number,
274
- INITIAL_RETRY_DELAY: number,
275
- CONNECTION_TIMEOUT: number,
276
- MAX_RETRY_DELAY: number,
277
- callbacks: PeerConnectionCallbacks
278
- ): Promise<any> {
279
- if (state.isShuttingDown) {
280
- throw new Error('Connection attempt cancelled - shutting down');
281
- }
282
-
283
- return new Promise((resolve, reject) => {
284
- let connectionEstablished = false;
285
- let timeoutId: NodeJS.Timeout | null = null;
286
- let localConnection: RTCDataChannel | MediaStream | null = null;
287
- const connectionId = ++state.currentConnectionId;
288
- const isInitiator = state.isInitiator ?? options.isInitiator ?? true;
289
- const connectionType =
290
- state.connectionType ||
291
- (options.mediaOptions?.isMediaConnection ? 'mediastream' : 'datachannel');
292
-
293
- // Create message handler for WebRTC signaling
294
- const messageHandler = async (message: any) => {
295
- try {
296
- if (state.isShuttingDown) {
297
- return; // Don't process messages if shutting down
298
- }
299
-
300
- const channelId = `${state.channelPrefix}-${state.targetTwinId}`;
301
- const messageType = message.data?.type;
302
-
303
- switch (messageType) {
304
- case `${channelId}:answer`:
305
- if (isInitiator && state.pc) {
306
- callbacks.reportConnectionState(
307
- 'Received answer, current signaling state: ' + state.pc.signalingState
308
- );
309
- if (state.pc.signalingState === 'have-local-offer') {
310
- await state.pc.setRemoteDescription(message.data.data);
311
- callbacks.reportConnectionState('Set remote description successfully');
312
-
313
- // Apply any pending ICE candidates now that we have a remote description
314
- if (state.pendingCandidates.length > 0) {
315
- callbacks.reportConnectionState(
316
- `Applying ${state.pendingCandidates.length} pending ICE candidates after answer`
317
- );
318
- for (const candidate of state.pendingCandidates) {
319
- try {
320
- await state.pc.addIceCandidate(candidate);
321
- } catch (err) {
322
- callbacks.reportError('Error applying pending ICE candidate:', err);
323
- }
324
- }
325
- state.pendingCandidates = [];
326
- }
327
- } else {
328
- callbacks.reportConnectionState(
329
- `Ignoring answer - wrong state: ${state.pc.signalingState}`
330
- );
331
- }
332
- }
333
- break;
334
-
335
- case `${channelId}:offer`:
336
- if (!isInitiator && state.pc) {
337
- callbacks.reportConnectionState(
338
- 'Received offer, current signaling state: ' + state.pc.signalingState
339
- );
340
-
341
- // Only process the offer if we're in a stable state or have-remote-offer
342
- if (
343
- state.pc.signalingState === 'stable' ||
344
- state.pc.signalingState === 'have-remote-offer'
345
- ) {
346
- // If we already have a remote offer, we need to rollback first
347
- if (state.pc.signalingState === 'have-remote-offer') {
348
- callbacks.reportConnectionState('Rolling back previous offer');
349
- await state.pc.setLocalDescription({ type: 'rollback' });
350
- }
351
-
352
- await state.pc.setRemoteDescription(message.data.data);
353
- callbacks.reportConnectionState('Set remote description successfully');
354
-
355
- callbacks.reportConnectionState('Creating answer...');
356
- const answer = await state.pc.createAnswer();
357
- await state.pc.setLocalDescription(answer);
358
-
359
- await callbacks.sendSignalingMessage('answer', answer);
360
-
361
- // Apply any pending ICE candidates now that we have a remote description
362
- if (state.pendingCandidates.length > 0) {
363
- callbacks.reportConnectionState(
364
- `Applying ${state.pendingCandidates.length} pending ICE candidates after offer`
365
- );
366
- for (const candidate of state.pendingCandidates) {
367
- try {
368
- await state.pc.addIceCandidate(candidate);
369
- } catch (err) {
370
- callbacks.reportError('Error applying pending ICE candidate:', err);
371
- }
372
- }
373
- state.pendingCandidates = [];
374
- }
375
- } else {
376
- callbacks.reportConnectionState(
377
- `Ignoring offer - wrong state: ${state.pc.signalingState}`
378
- );
379
- }
380
- }
381
- break;
382
-
383
- case `${channelId}:ice`:
384
- if (state.pc) {
385
- if (state.pc.remoteDescription) {
386
- try {
387
- await state.pc.addIceCandidate(message.data.data);
388
- } catch (err) {
389
- callbacks.reportError('Error adding ICE candidate:', err);
390
- }
391
- } else {
392
- callbacks.reportConnectionState('Storing ICE candidate for later');
393
- state.pendingCandidates.push(message.data.data);
394
- }
395
- }
396
- break;
397
- }
398
- } catch (error) {
399
- callbacks.reportError('Error handling WebRTC message:', error);
400
- }
401
- };
402
-
403
- const cleanup = () => {
404
- callbacks.reportConnectionState('Running cleanup...');
405
- if (timeoutId) clearTimeout(timeoutId);
406
- callbacks.removeMessageHandling(messageHandler);
407
- if (localConnection instanceof RTCDataChannel) {
408
- localConnection.onopen = null;
409
- localConnection.onclose = null;
410
- localConnection.onerror = null;
411
- localConnection.close();
412
- } else if (localConnection instanceof MediaStream) {
413
- localConnection.getTracks().forEach(track => track.stop());
414
- }
415
- if (state.pc) {
416
- state.pc.onicecandidate = null;
417
- state.pc.onconnectionstatechange = null;
418
- state.pc.oniceconnectionstatechange = null;
419
- try {
420
- (state.pc as RTCPeerConnection).close();
421
- } catch (err) {
422
- callbacks.reportError('Error closing peer connection', err);
423
- }
424
- state.pc = null;
425
- }
426
- state.pendingCandidates = [];
427
- state.activeDataChannel = null;
428
- state.activeMediaStream = null;
429
-
430
- // Also do any local cleanup
431
- callbacks.onCloseCleanup();
432
- };
433
-
434
- const safeReject = (error: Error) => {
435
- cleanup();
436
- reject(error);
437
- };
438
-
439
- const retryConnection = () => {
440
- if (state.isShuttingDown) {
441
- safeReject(new Error('Connection attempt cancelled - shutting down'));
442
- return;
443
- }
444
- callbacks.reportConnectionState(
445
- `Connection attempt ${retryCount + 1} failed ${currentUseStun ? 'with' : 'without'} STUN`
446
- );
447
- cleanup();
448
-
449
- // The original code manipulates STUN usage on first retry,
450
- // then uses exponential backoff.
451
- if (retryCount === 0) {
452
- callbacks.reportConnectionState(
453
- `Retrying with ${!currentUseStun ? 'STUN' : 'direct'} connection...`
454
- );
455
- callbacks
456
- .retryConnection(retryCount + 1, !currentUseStun)
457
- .then(res => resolve(res))
458
- .catch(err => reject(err));
459
- return;
460
- }
461
-
462
- // Otherwise, wait and retry with original STUN setting
463
- callbacks.reportConnectionState(
464
- `Retrying in ${Math.floor(delay / 1000)} seconds... (attempt ${retryCount + 1})`
465
- );
466
- setTimeout(() => {
467
- const nextDelay = Math.min(MAX_RETRY_DELAY, INITIAL_RETRY_DELAY * Math.pow(2, retryCount));
468
- callbacks
469
- .retryConnection(retryCount + 1, currentUseStun, Math.min(nextDelay, MAX_RETRY_DELAY))
470
- .then(res => resolve(res))
471
- .catch(err => reject(err));
472
- }, delay);
473
- };
474
-
475
- const initConnection = async () => {
476
- if (typeof RTCPeerConnection === 'undefined') {
477
- callbacks.reportError(
478
- 'RTCPeerConnection is not available. WebRTC globals initialization may have failed.'
479
- );
480
- throw new Error(
481
- 'RTCPeerConnection is not available. Cannot proceed with WebRTC connection.'
482
- );
483
- }
484
-
485
- // Set up message handling for signaling
486
- callbacks.setupMessageHandling(messageHandler);
487
-
488
- // Original code: await subscribeTwin(targetTwinId)
489
- // delegated to callback
490
- await callbacks.onSubscribe();
491
-
492
- callbacks.reportConnectionState(
493
- `Attempting data connection ${currentUseStun ? 'with' : 'without'} STUN servers...`
494
- );
495
- state.pc = createPeerConnection(currentUseStun);
496
- const pc = state.pc!;
497
-
498
- // Set up ICE handling
499
- handleIceAndConnectionState(state, pc, callbacks, connectionId);
500
-
501
- // Defer connection creation to original file (keeping specific code there)
502
- try {
503
- localConnection = await callbacks.onCreateConnection(pc, connectionId, isInitiator);
504
-
505
- if (localConnection instanceof RTCDataChannel) {
506
- state.activeDataChannel = localConnection;
507
- } else if (localConnection instanceof MediaStream) {
508
- state.activeMediaStream = localConnection;
509
- }
510
-
511
- // Monitor connection state changes
512
- pc.onconnectionstatechange = () => {
513
- if (!pc) return;
514
- callbacks.reportConnectionState(`Connection state: ${pc.connectionState}`);
515
- if (pc.connectionState === 'connected') {
516
- connectionEstablished = true;
517
- if (timeoutId) clearTimeout(timeoutId);
518
-
519
- // For MediaStreams, we need to wait for tracks to be received
520
- if (connectionType === 'mediastream') {
521
- // Check if we have received any tracks
522
- const hasTracks = pc.getReceivers().some(receiver => receiver.track);
523
- if (!hasTracks) {
524
- callbacks.reportConnectionState('Connected but no tracks received yet');
525
- return;
526
- }
527
- }
528
-
529
- // If we have an active connection, resolve the promise
530
- if (connectionEstablished && !state.isShuttingDown) {
531
- const result = {
532
- _rawPeerConnection: pc,
533
- close: () => {
534
- state.isShuttingDown = true;
535
- cleanup();
536
- },
537
- };
538
-
539
- // Add connection-specific properties
540
- if (connectionType === 'datachannel') {
541
- Object.assign(result, {
542
- dataChannel: {
543
- send: () => {}, // This will be replaced by the persistent channel
544
- onMessage: () => {}, // This will be replaced by the persistent channel
545
- offMessage: () => {}, // This will be replaced by the persistent channel
546
- onClose: () => {}, // This will be replaced by the persistent channel
547
- offClose: () => {}, // This will be replaced by the persistent channel
548
- close: () => {
549
- state.isShuttingDown = true;
550
- cleanup();
551
- },
552
- },
553
- _rawDataChannel: state.activeDataChannel,
554
- });
555
- } else if (connectionType === 'mediastream') {
556
- Object.assign(result, {
557
- mediaStream: {
558
- getTracks: () => [], // This will be replaced by the persistent media stream
559
- onTrack: () => {}, // This will be replaced by the persistent media stream
560
- offTrack: () => {}, // This will be replaced by the persistent media stream
561
- onClose: () => {}, // This will be replaced by the persistent media stream
562
- offClose: () => {}, // This will be replaced by the persistent media stream
563
- close: () => {
564
- state.isShuttingDown = true;
565
- cleanup();
566
- },
567
- addTrack: () => {}, // This will be replaced by the persistent media stream
568
- },
569
- _rawMediaStream: state.activeMediaStream,
570
- });
571
- }
572
-
573
- resolve(result);
574
- }
575
- } else if (
576
- pc.connectionState === 'failed' ||
577
- pc.connectionState === 'disconnected' ||
578
- pc.connectionState === 'closed'
579
- ) {
580
- if (connectionEstablished) {
581
- callbacks.reportConnectionState('Connection lost, initiating retry...');
582
- retryConnection();
583
- }
584
- }
585
- };
586
-
587
- // Set up ICE connection state monitoring
588
- pc.oniceconnectionstatechange = () => {
589
- if (!pc) return;
590
- callbacks.reportConnectionState(`ICE connection state: ${pc.iceConnectionState}`);
591
- if (pc.iceConnectionState === 'failed' && connectionEstablished) {
592
- callbacks.reportConnectionState('ICE Connection failed, initiating retry...');
593
- retryConnection();
594
- }
595
- };
596
-
597
- // Create and send offer for initiator
598
- if (isInitiator) {
599
- try {
600
- callbacks.reportConnectionState('Creating offer...');
601
- const offer = await pc.createOffer();
602
- callbacks.reportConnectionState('Setting local description...');
603
- await pc.setLocalDescription(offer);
604
- callbacks.reportConnectionState('Sending offer...');
605
- await callbacks.sendSignalingMessage('offer', offer);
606
- } catch (error) {
607
- callbacks.reportError('Error creating/sending offer:', error);
608
- retryConnection();
609
- return;
610
- }
611
- }
612
-
613
- // Timeout to detect initial connection failure
614
- timeoutId = setTimeout(() => {
615
- if (!connectionEstablished) {
616
- callbacks.reportConnectionState(
617
- `Connection attempt timed out ${currentUseStun ? 'with' : 'without'} STUN`
618
- );
619
- retryConnection();
620
- }
621
- }, CONNECTION_TIMEOUT);
622
- } catch (error) {
623
- callbacks.reportError('Error creating connection in callback', error);
624
- retryConnection();
625
- return;
626
- }
627
- };
628
-
629
- // Start everything
630
- initConnection().catch(error => {
631
- callbacks.reportError('Unhandled error in connection attempt', error);
632
- safeReject(error);
633
- });
634
- });
635
- }
636
-
637
- /**
638
- * The reconnect trigger logic, abstracted here.
639
- */
640
- export function triggerReconnect(
641
- state: PeerConnectionState,
642
- attempt: number,
643
- RECONNECT_DELAY: number,
644
- callbacks: PeerConnectionCallbacks
645
- ) {
646
- // These checks are now handled in the webrtc-datachannel.ts file
647
- // to maintain consistent state between the two files
648
-
649
- state.currentConnectionId++;
650
- callbacks.reportConnectionState(`Starting reconnection process (attempt ${attempt})`);
651
-
652
- // Force close any active data channel
653
- if (state.activeDataChannel && typeof state.activeDataChannel.close === 'function') {
654
- try {
655
- state.activeDataChannel.close();
656
- } catch (err) {
657
- callbacks.reportError('Error closing active data channel:', err);
658
- }
659
- state.activeDataChannel = null;
660
- }
661
-
662
- // Clean up any existing peer connection
663
- if (state.pc) {
664
- try {
665
- (state.pc as RTCPeerConnection).close();
666
- } catch (err) {
667
- callbacks.reportError('Error closing old peer connection:', err);
668
- }
669
- state.pc = null;
670
- }
671
-
672
- const reconnectAttempt = () => {
673
- // The original code tries with toggled useStun or direct
674
- callbacks
675
- .retryConnection(0, !(state.useStun ?? true))
676
- .then(() => {
677
- callbacks.reportConnectionState(`Reconnection successful (attempt ${attempt})`);
678
- })
679
- .catch(error => {
680
- callbacks.reportError(`Reconnection failed (attempt ${attempt})`, error);
681
- setTimeout(
682
- reconnectAttempt,
683
- Math.min(100 * Math.pow(2, attempt), 5000) // backoff
684
- );
685
- });
686
- };
687
-
688
- setTimeout(reconnectAttempt, RECONNECT_DELAY);
689
- }