@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,467 @@
1
+ /**
2
+ * PeerConnectionManager
3
+ *
4
+ * Manages RTCPeerConnection lifecycle including:
5
+ * - Connection creation and teardown
6
+ * - ICE candidate gathering and exchange
7
+ * - Connection state monitoring
8
+ * - Automatic reconnection with exponential backoff
9
+ */
10
+
11
+ import { ensureWebRTCGlobals } from './webrtc-globals';
12
+ import {
13
+ TwinMessagingInterface,
14
+ PeerConnectionConfig,
15
+ SignalingMessageType,
16
+ DEFAULT_WEBRTC_OPTIONS,
17
+ } from './types';
18
+
19
+ export interface PeerConnectionManagerOptions {
20
+ connectionTimeout: number;
21
+ initialRetryDelay: number;
22
+ maxRetryDelay: number;
23
+ }
24
+
25
+ export class PeerConnectionManager {
26
+ private pc: RTCPeerConnection | null = null;
27
+ private config: PeerConnectionConfig;
28
+ private twinMessaging: TwinMessagingInterface;
29
+ private options: PeerConnectionManagerOptions;
30
+
31
+ private isShuttingDown = false;
32
+ private isReconnecting = false;
33
+ private connectionId = 0;
34
+ private pendingCandidates: RTCIceCandidateInit[] = [];
35
+ private connectionTimeout: NodeJS.Timeout | null = null;
36
+ private messageHandler: ((message: any) => void) | null = null;
37
+
38
+ constructor(
39
+ config: PeerConnectionConfig,
40
+ twinMessaging: TwinMessagingInterface,
41
+ options: Partial<PeerConnectionManagerOptions> = {}
42
+ ) {
43
+ this.config = config;
44
+ this.twinMessaging = twinMessaging;
45
+ this.options = {
46
+ connectionTimeout: options.connectionTimeout ?? DEFAULT_WEBRTC_OPTIONS.connectionTimeout,
47
+ initialRetryDelay: options.initialRetryDelay ?? DEFAULT_WEBRTC_OPTIONS.initialRetryDelay,
48
+ maxRetryDelay: options.maxRetryDelay ?? DEFAULT_WEBRTC_OPTIONS.maxRetryDelay,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Start the WebRTC connection process.
54
+ * Returns the RTCPeerConnection once connected.
55
+ */
56
+ async connect(): Promise<RTCPeerConnection> {
57
+ await ensureWebRTCGlobals();
58
+
59
+ if (this.pc && this.pc.connectionState === 'connected') {
60
+ return this.pc;
61
+ }
62
+
63
+ return this.attemptConnection(0, this.config.useStun, this.options.initialRetryDelay);
64
+ }
65
+
66
+ /**
67
+ * Get the current RTCPeerConnection (may be null if not connected)
68
+ */
69
+ getPeerConnection(): RTCPeerConnection | null {
70
+ return this.pc;
71
+ }
72
+
73
+ /**
74
+ * Check if the connection is currently active
75
+ */
76
+ isConnected(): boolean {
77
+ return this.pc?.connectionState === 'connected';
78
+ }
79
+
80
+ /**
81
+ * Close the connection and clean up resources
82
+ */
83
+ close(): void {
84
+ this.isShuttingDown = true;
85
+ this.cleanup();
86
+ }
87
+
88
+ /**
89
+ * Trigger a manual reconnection
90
+ */
91
+ reconnect(): void {
92
+ if (this.isReconnecting || this.isShuttingDown) {
93
+ return;
94
+ }
95
+ this.triggerReconnect(0);
96
+ }
97
+
98
+ // ===========================================================================
99
+ // Private Methods
100
+ // ===========================================================================
101
+
102
+ private async attemptConnection(
103
+ retryCount: number,
104
+ useStun: boolean,
105
+ delay: number
106
+ ): Promise<RTCPeerConnection> {
107
+ if (this.isShuttingDown) {
108
+ throw new Error('Connection attempt cancelled - shutting down');
109
+ }
110
+
111
+ return new Promise((resolve, reject) => {
112
+ const currentConnectionId = ++this.connectionId;
113
+ let connectionEstablished = false;
114
+
115
+ // Create peer connection FIRST, before setting up message handler
116
+ console.log(`[PeerConnectionManager] Creating peer connection (useStun=${useStun}, isInitiator=${this.config.isInitiator})`);
117
+ this.pc = this.createPeerConnection(useStun);
118
+ const pc = this.pc;
119
+ console.log(`[PeerConnectionManager] Peer connection created: ${pc.connectionState}`);
120
+
121
+ // Setup message handler for signaling
122
+ this.setupMessageHandler(currentConnectionId);
123
+
124
+ // Subscribe to the target twin
125
+ this.twinMessaging.subscribe(this.config.targetTwinId).catch(err => {
126
+ console.error('[PeerConnectionManager] Failed to subscribe to twin:', err);
127
+ });
128
+
129
+ // Setup ICE handling
130
+ this.setupIceHandling(pc, currentConnectionId);
131
+
132
+ // Setup connection state monitoring
133
+ pc.onconnectionstatechange = () => {
134
+ this.config.onSignalingStateChange?.(pc.signalingState);
135
+
136
+ if (pc.connectionState === 'connected') {
137
+ connectionEstablished = true;
138
+ this.clearConnectionTimeout();
139
+ this.config.onConnected();
140
+ resolve(pc);
141
+ } else if (
142
+ pc.connectionState === 'failed' ||
143
+ pc.connectionState === 'disconnected' ||
144
+ pc.connectionState === 'closed'
145
+ ) {
146
+ if (connectionEstablished && !this.isShuttingDown) {
147
+ this.config.onDisconnected();
148
+ this.triggerReconnect(0);
149
+ } else if (!connectionEstablished) {
150
+ this.handleConnectionFailure(retryCount, useStun, delay, resolve, reject);
151
+ }
152
+ }
153
+ };
154
+
155
+ pc.oniceconnectionstatechange = () => {
156
+ this.config.onIceStateChange?.(pc.iceConnectionState);
157
+
158
+ if (pc.iceConnectionState === 'failed' && connectionEstablished) {
159
+ this.triggerReconnect(0);
160
+ }
161
+ };
162
+
163
+ // Call the setup callback (for adding data channels/tracks) before creating offer
164
+ if (this.config.onPeerConnectionCreated) {
165
+ try {
166
+ const result = this.config.onPeerConnectionCreated(pc);
167
+ if (result instanceof Promise) {
168
+ result.catch(err => console.error('[PeerConnectionManager] Error in onPeerConnectionCreated:', err));
169
+ }
170
+ } catch (err) {
171
+ console.error('[PeerConnectionManager] Error in onPeerConnectionCreated:', err);
172
+ }
173
+ }
174
+
175
+ // Start the offer/answer exchange if we're the initiator
176
+ if (this.config.isInitiator) {
177
+ this.createAndSendOffer(pc).catch(err => {
178
+ console.error('[PeerConnectionManager] Error creating offer:', err);
179
+ this.handleConnectionFailure(retryCount, useStun, delay, resolve, reject);
180
+ });
181
+ }
182
+
183
+ // Set connection timeout
184
+ this.connectionTimeout = setTimeout(() => {
185
+ if (!connectionEstablished) {
186
+ console.log(`[PeerConnectionManager] Connection timeout (${useStun ? 'with' : 'without'} STUN)`);
187
+ this.handleConnectionFailure(retryCount, useStun, delay, resolve, reject);
188
+ }
189
+ }, this.options.connectionTimeout);
190
+ });
191
+ }
192
+
193
+ private createPeerConnection(useStun: boolean): RTCPeerConnection {
194
+ const iceServers = useStun
195
+ ? this.config.stunServers.map(url => ({ urls: url }))
196
+ : [];
197
+
198
+ return new RTCPeerConnection({ iceServers });
199
+ }
200
+
201
+ private setupIceHandling(pc: RTCPeerConnection, connectionId: number): void {
202
+ pc.onicecandidate = async event => {
203
+ if (this.connectionId !== connectionId) return;
204
+
205
+ console.log('[PeerConnectionManager] ICE candidate:', event.candidate?.candidate || 'gathering complete');
206
+
207
+ this.config.onIceCandidate?.(event.candidate);
208
+
209
+ if (event.candidate) {
210
+ try {
211
+ console.log('[PeerConnectionManager] Sending ICE candidate');
212
+ await this.sendSignalingMessage('ice', event.candidate);
213
+ } catch (err) {
214
+ console.error('[PeerConnectionManager] Error sending ICE candidate:', err);
215
+ }
216
+ }
217
+ };
218
+
219
+ pc.onicegatheringstatechange = () => {
220
+ console.log('[PeerConnectionManager] ICE gathering state:', pc.iceGatheringState);
221
+ };
222
+
223
+ pc.oniceconnectionstatechange = () => {
224
+ console.log('[PeerConnectionManager] ICE connection state:', pc.iceConnectionState);
225
+ };
226
+ }
227
+
228
+ private setupMessageHandler(connectionId: number): void {
229
+ const ownTwinId = this.twinMessaging.getOwnTwinId();
230
+
231
+ // Remove old handler if exists
232
+ if (this.messageHandler) {
233
+ this.twinMessaging.offMessage(ownTwinId, this.messageHandler);
234
+ }
235
+
236
+ this.messageHandler = async (message: any) => {
237
+ if (this.isShuttingDown || this.connectionId !== connectionId) return;
238
+
239
+ // Filter: only process messages FROM the target peer
240
+ if (message.sourceTwinId !== this.config.targetTwinId) return;
241
+
242
+ try {
243
+ await this.handleSignalingMessage(message);
244
+ } catch (err) {
245
+ console.error('[PeerConnectionManager] Error handling signaling message:', err);
246
+ }
247
+ };
248
+
249
+ // Register on OWN twinId, filter by sourceTwinId in callback
250
+ this.twinMessaging.onMessage(ownTwinId, this.messageHandler);
251
+ }
252
+
253
+ private async handleSignalingMessage(message: any): Promise<void> {
254
+ if (!this.pc) return;
255
+
256
+ const messageType = message.data?.type as string;
257
+ if (!messageType) return;
258
+
259
+ // Extract the signal type (offer/answer/ice) from the message type
260
+ // Message type format: channel-{twinId}:{signalType}
261
+ // We match on the signal type suffix since the channelId varies by perspective
262
+ const prefix = `${this.config.channelPrefix}-`;
263
+ if (!messageType.startsWith(prefix)) return;
264
+
265
+ const signalType = messageType.split(':').pop();
266
+ console.log(`[PeerConnectionManager] Received signaling message: ${signalType}`);
267
+
268
+ switch (signalType) {
269
+ case 'offer':
270
+ await this.handleOffer(message.data.data);
271
+ break;
272
+
273
+ case 'answer':
274
+ await this.handleAnswer(message.data.data);
275
+ break;
276
+
277
+ case 'ice':
278
+ await this.handleIceCandidate(message.data.data);
279
+ break;
280
+ }
281
+ }
282
+
283
+ private async handleOffer(offer: RTCSessionDescriptionInit): Promise<void> {
284
+ if (!this.pc || this.config.isInitiator) {
285
+ return;
286
+ }
287
+
288
+ const pc = this.pc;
289
+
290
+ if (pc.signalingState === 'stable' || pc.signalingState === 'have-remote-offer') {
291
+ if (pc.signalingState === 'have-remote-offer') {
292
+ await pc.setLocalDescription({ type: 'rollback' });
293
+ }
294
+
295
+ console.log('[PeerConnectionManager] Setting remote description (offer)');
296
+ await pc.setRemoteDescription(offer);
297
+ console.log(`[PeerConnectionManager] Remote description set. signalingState=${pc.signalingState}`);
298
+ await this.applyPendingCandidates();
299
+
300
+ console.log('[PeerConnectionManager] Creating answer...');
301
+ const answer = await pc.createAnswer();
302
+ console.log(`[PeerConnectionManager] Answer created. type=${answer.type}`);
303
+
304
+ console.log('[PeerConnectionManager] Setting local description (answer)');
305
+ await pc.setLocalDescription(answer);
306
+ console.log(`[PeerConnectionManager] Local description set. signalingState=${pc.signalingState}, iceGatheringState=${pc.iceGatheringState}`);
307
+
308
+ await this.sendSignalingMessage('answer', answer);
309
+ console.log('[PeerConnectionManager] Answer sent');
310
+ }
311
+ }
312
+
313
+ private async handleAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
314
+ if (!this.pc || !this.config.isInitiator) return;
315
+
316
+ const pc = this.pc;
317
+
318
+ if (pc.signalingState === 'have-local-offer') {
319
+ await pc.setRemoteDescription(answer);
320
+ await this.applyPendingCandidates();
321
+ }
322
+ }
323
+
324
+ private async handleIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
325
+ if (!this.pc) return;
326
+
327
+ if (this.pc.remoteDescription) {
328
+ try {
329
+ await this.pc.addIceCandidate(candidate);
330
+ } catch (err) {
331
+ console.error('[PeerConnectionManager] Error adding ICE candidate:', err);
332
+ }
333
+ } else {
334
+ this.pendingCandidates.push(candidate);
335
+ }
336
+ }
337
+
338
+ private async applyPendingCandidates(): Promise<void> {
339
+ if (!this.pc || this.pendingCandidates.length === 0) return;
340
+
341
+ for (const candidate of this.pendingCandidates) {
342
+ try {
343
+ await this.pc.addIceCandidate(candidate);
344
+ } catch (err) {
345
+ console.error('[PeerConnectionManager] Error applying pending ICE candidate:', err);
346
+ }
347
+ }
348
+
349
+ this.pendingCandidates = [];
350
+ }
351
+
352
+ private async createAndSendOffer(pc: RTCPeerConnection): Promise<void> {
353
+ console.log('[PeerConnectionManager] Creating offer...');
354
+ const offer = await pc.createOffer();
355
+ console.log(`[PeerConnectionManager] Offer created. type=${offer.type}`);
356
+
357
+ console.log('[PeerConnectionManager] Setting local description (offer)');
358
+ await pc.setLocalDescription(offer);
359
+ console.log(`[PeerConnectionManager] Local description set. signalingState=${pc.signalingState}, iceGatheringState=${pc.iceGatheringState}`);
360
+
361
+ await this.sendSignalingMessage('offer', offer);
362
+ console.log('[PeerConnectionManager] Offer sent');
363
+ }
364
+
365
+ private async sendSignalingMessage(
366
+ type: SignalingMessageType,
367
+ data: RTCSessionDescriptionInit | RTCIceCandidateInit | RTCIceCandidate
368
+ ): Promise<void> {
369
+ const channelId = `${this.config.channelPrefix}-${this.config.targetTwinId}`;
370
+
371
+ await this.twinMessaging.sendMessage(this.config.targetTwinId, {
372
+ type: `${channelId}:${type}`,
373
+ data,
374
+ });
375
+ }
376
+
377
+ private handleConnectionFailure(
378
+ retryCount: number,
379
+ currentUseStun: boolean,
380
+ delay: number,
381
+ resolve: (pc: RTCPeerConnection) => void,
382
+ reject: (error: Error) => void
383
+ ): void {
384
+ this.cleanup();
385
+
386
+ if (this.isShuttingDown) {
387
+ reject(new Error('Connection cancelled - shutting down'));
388
+ return;
389
+ }
390
+
391
+ // First retry: toggle STUN setting
392
+ if (retryCount === 0) {
393
+ this.attemptConnection(1, !currentUseStun, this.options.initialRetryDelay)
394
+ .then(resolve)
395
+ .catch(reject);
396
+ return;
397
+ }
398
+
399
+ // Subsequent retries: exponential backoff
400
+ const nextDelay = Math.min(this.options.maxRetryDelay, delay * 2);
401
+
402
+ setTimeout(() => {
403
+ this.attemptConnection(retryCount + 1, currentUseStun, nextDelay)
404
+ .then(resolve)
405
+ .catch(reject);
406
+ }, delay);
407
+ }
408
+
409
+ private triggerReconnect(attempt: number): void {
410
+ if (this.isReconnecting || this.isShuttingDown) return;
411
+
412
+ this.isReconnecting = true;
413
+ this.config.onReconnecting?.(attempt);
414
+
415
+ this.cleanup();
416
+
417
+ const reconnectDelay = Math.min(
418
+ this.options.maxRetryDelay,
419
+ this.options.initialRetryDelay * Math.pow(2, attempt)
420
+ );
421
+
422
+ setTimeout(() => {
423
+ this.attemptConnection(0, !this.config.useStun, this.options.initialRetryDelay)
424
+ .then(() => {
425
+ this.isReconnecting = false;
426
+ this.config.onReconnected?.(attempt);
427
+ })
428
+ .catch(err => {
429
+ console.error(`[PeerConnectionManager] Reconnection attempt ${attempt} failed:`, err);
430
+ this.triggerReconnect(attempt + 1);
431
+ });
432
+ }, reconnectDelay);
433
+ }
434
+
435
+ private clearConnectionTimeout(): void {
436
+ if (this.connectionTimeout) {
437
+ clearTimeout(this.connectionTimeout);
438
+ this.connectionTimeout = null;
439
+ }
440
+ }
441
+
442
+ private cleanup(): void {
443
+ this.clearConnectionTimeout();
444
+
445
+ if (this.messageHandler) {
446
+ const ownTwinId = this.twinMessaging.getOwnTwinId();
447
+ this.twinMessaging.offMessage(ownTwinId, this.messageHandler);
448
+ this.messageHandler = null;
449
+ }
450
+
451
+ if (this.pc) {
452
+ this.pc.onicecandidate = null;
453
+ this.pc.onconnectionstatechange = null;
454
+ this.pc.oniceconnectionstatechange = null;
455
+
456
+ try {
457
+ this.pc.close();
458
+ } catch (err) {
459
+ console.error('[PeerConnectionManager] Error closing peer connection:', err);
460
+ }
461
+
462
+ this.pc = null;
463
+ }
464
+
465
+ this.pendingCandidates = [];
466
+ }
467
+ }