@xtr-dev/rondevu-client 0.18.10 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -117
- package/dist/api/batcher.d.ts +83 -0
- package/dist/api/batcher.js +155 -0
- package/dist/api/client.d.ts +198 -0
- package/dist/api/client.js +400 -0
- package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
- package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
- package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
- package/dist/{connection.js → connections/base.js} +65 -14
- package/dist/connections/config.d.ts +51 -0
- package/dist/{connection-config.js → connections/config.js} +20 -0
- package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
- package/dist/connections/offerer.d.ts +108 -0
- package/dist/connections/offerer.js +306 -0
- package/dist/core/ice-config.d.ts +35 -0
- package/dist/core/ice-config.js +111 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +22 -0
- package/dist/core/offer-pool.d.ts +113 -0
- package/dist/core/offer-pool.js +281 -0
- package/dist/core/peer.d.ts +155 -0
- package/dist/core/peer.js +252 -0
- package/dist/core/polling-manager.d.ts +71 -0
- package/dist/core/polling-manager.js +122 -0
- package/dist/core/rondevu-errors.d.ts +59 -0
- package/dist/core/rondevu-errors.js +75 -0
- package/dist/core/rondevu-types.d.ts +125 -0
- package/dist/core/rondevu-types.js +6 -0
- package/dist/core/rondevu.d.ts +296 -0
- package/dist/core/rondevu.js +472 -0
- package/dist/crypto/adapter.d.ts +53 -0
- package/dist/crypto/node.d.ts +57 -0
- package/dist/crypto/node.js +149 -0
- package/dist/crypto/web.d.ts +38 -0
- package/dist/crypto/web.js +129 -0
- package/dist/utils/async-lock.d.ts +42 -0
- package/dist/utils/async-lock.js +75 -0
- package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
- package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
- package/dist/webrtc/adapter.d.ts +22 -0
- package/dist/webrtc/adapter.js +5 -0
- package/dist/webrtc/browser.d.ts +12 -0
- package/dist/webrtc/browser.js +15 -0
- package/dist/webrtc/node.d.ts +32 -0
- package/dist/webrtc/node.js +32 -0
- package/package.json +20 -9
- package/dist/api.d.ts +0 -146
- package/dist/api.js +0 -279
- package/dist/connection-config.d.ts +0 -21
- package/dist/crypto-adapter.d.ts +0 -37
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -10
- package/dist/node-crypto-adapter.d.ts +0 -35
- package/dist/node-crypto-adapter.js +0 -78
- package/dist/offerer-connection.d.ts +0 -54
- package/dist/offerer-connection.js +0 -177
- package/dist/rondevu-signaler.d.ts +0 -112
- package/dist/rondevu-signaler.js +0 -401
- package/dist/rondevu.d.ts +0 -407
- package/dist/rondevu.js +0 -847
- package/dist/rpc-batcher.d.ts +0 -61
- package/dist/rpc-batcher.js +0 -111
- package/dist/web-crypto-adapter.d.ts +0 -16
- package/dist/web-crypto-adapter.js +0 -52
- /package/dist/{connection-events.js → connections/events.js} +0 -0
- /package/dist/{types.d.ts → core/types.d.ts} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
- /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
- /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Answerer-side WebRTC connection with answer creation and offer processing
|
|
3
3
|
*/
|
|
4
|
-
import { RondevuConnection } from './
|
|
5
|
-
import { ConnectionState } from './
|
|
4
|
+
import { RondevuConnection } from './base.js';
|
|
5
|
+
import { ConnectionState } from './events.js';
|
|
6
6
|
/**
|
|
7
7
|
* Answerer connection - processes offers and creates answers
|
|
8
8
|
*/
|
|
9
9
|
export class AnswererConnection extends RondevuConnection {
|
|
10
10
|
constructor(options) {
|
|
11
|
-
super(options.rtcConfig, options.config);
|
|
11
|
+
super(options.rtcConfig, options.config, options.webrtcAdapter);
|
|
12
12
|
this.api = options.api;
|
|
13
|
-
this.
|
|
13
|
+
this.ownerUsername = options.ownerUsername;
|
|
14
|
+
this.tags = options.tags;
|
|
14
15
|
this.offerId = options.offerId;
|
|
15
16
|
this.offerSdp = options.offerSdp;
|
|
16
17
|
}
|
|
@@ -25,7 +26,7 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
25
26
|
throw new Error('Peer connection not created');
|
|
26
27
|
// Setup ondatachannel handler BEFORE setting remote description
|
|
27
28
|
// This is critical to avoid race conditions
|
|
28
|
-
this.pc.ondatachannel =
|
|
29
|
+
this.pc.ondatachannel = event => {
|
|
29
30
|
this.debug('Received data channel');
|
|
30
31
|
this.dc = event.channel;
|
|
31
32
|
this.setupDataChannelHandlers(this.dc);
|
|
@@ -43,7 +44,9 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
43
44
|
await this.pc.setLocalDescription(answer);
|
|
44
45
|
this.debug('Answer created, sending to server');
|
|
45
46
|
// Send answer to server
|
|
46
|
-
await this.api.answerOffer(this.
|
|
47
|
+
await this.api.answerOffer(this.offerId, answer.sdp);
|
|
48
|
+
// Note: ICE candidate polling is handled by PollingManager
|
|
49
|
+
// Candidates are received via handleRemoteIceCandidates()
|
|
47
50
|
this.debug('Answer sent successfully');
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
@@ -54,56 +57,41 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
54
57
|
// For answerer, we add ICE candidates to the offer
|
|
55
58
|
// The server will make them available for the offerer to poll
|
|
56
59
|
this.api
|
|
57
|
-
.addOfferIceCandidates(this.
|
|
60
|
+
.addOfferIceCandidates(this.offerId, [
|
|
58
61
|
{
|
|
59
62
|
candidate: candidate.candidate,
|
|
60
63
|
sdpMLineIndex: candidate.sdpMLineIndex,
|
|
61
64
|
sdpMid: candidate.sdpMid,
|
|
62
65
|
},
|
|
63
66
|
])
|
|
64
|
-
.catch(
|
|
67
|
+
.catch(error => {
|
|
65
68
|
this.debug('Failed to send ICE candidate:', error);
|
|
66
69
|
});
|
|
67
70
|
}
|
|
68
71
|
/**
|
|
69
|
-
*
|
|
72
|
+
* Get the API instance
|
|
70
73
|
*/
|
|
71
|
-
|
|
72
|
-
this.api
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
.catch((error) => {
|
|
87
|
-
this.debug('Failed to add ICE candidate:', error);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
// Update last poll time
|
|
91
|
-
if (iceCandidate.createdAt > this.lastIcePollTime) {
|
|
92
|
-
this.lastIcePollTime = iceCandidate.createdAt;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
.catch((error) => {
|
|
98
|
-
this.debug('Failed to poll ICE candidates:', error);
|
|
99
|
-
});
|
|
74
|
+
getApi() {
|
|
75
|
+
return this.api;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the owner username
|
|
79
|
+
*/
|
|
80
|
+
getOwnerUsername() {
|
|
81
|
+
return this.ownerUsername;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Answerers accept ICE candidates from offerers only
|
|
85
|
+
*/
|
|
86
|
+
getIceCandidateRole() {
|
|
87
|
+
return 'offerer';
|
|
100
88
|
}
|
|
101
89
|
/**
|
|
102
|
-
* Attempt to reconnect
|
|
90
|
+
* Attempt to reconnect to the same user
|
|
103
91
|
*/
|
|
104
92
|
attemptReconnect() {
|
|
105
|
-
this.debug(
|
|
106
|
-
// For answerer, we need to fetch a new offer
|
|
93
|
+
this.debug(`Attempting to reconnect to ${this.ownerUsername}`);
|
|
94
|
+
// For answerer, we need to fetch a new offer from the same user
|
|
107
95
|
// Clean up old connection
|
|
108
96
|
if (this.pc) {
|
|
109
97
|
this.pc.close();
|
|
@@ -113,24 +101,31 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
113
101
|
this.dc.close();
|
|
114
102
|
this.dc = null;
|
|
115
103
|
}
|
|
116
|
-
//
|
|
104
|
+
// Discover new offer using tags (use paginated mode to get array)
|
|
117
105
|
this.api
|
|
118
|
-
.
|
|
119
|
-
.then(
|
|
120
|
-
|
|
106
|
+
.discover({ tags: this.tags, limit: 100 })
|
|
107
|
+
.then(result => {
|
|
108
|
+
const response = result;
|
|
109
|
+
if (!response || !response.offers || response.offers.length === 0) {
|
|
121
110
|
throw new Error('No offers available for reconnection');
|
|
122
111
|
}
|
|
123
|
-
//
|
|
124
|
-
const
|
|
112
|
+
// Filter for offers from the same user
|
|
113
|
+
const userOffers = response.offers.filter(o => o.username === this.ownerUsername);
|
|
114
|
+
if (userOffers.length === 0) {
|
|
115
|
+
throw new Error(`No offers available from ${this.ownerUsername}`);
|
|
116
|
+
}
|
|
117
|
+
// Pick a random offer from the same user
|
|
118
|
+
const offer = userOffers[Math.floor(Math.random() * userOffers.length)];
|
|
125
119
|
this.offerId = offer.offerId;
|
|
126
120
|
this.offerSdp = offer.sdp;
|
|
121
|
+
this.debug(`Found new offer ${offer.offerId} from ${this.ownerUsername}`);
|
|
127
122
|
// Reinitialize with new offer
|
|
128
123
|
return this.initialize();
|
|
129
124
|
})
|
|
130
125
|
.then(() => {
|
|
131
126
|
this.emit('reconnect:success');
|
|
132
127
|
})
|
|
133
|
-
.catch(
|
|
128
|
+
.catch(error => {
|
|
134
129
|
this.debug('Reconnection failed:', error);
|
|
135
130
|
this.emit('reconnect:failed', error);
|
|
136
131
|
this.scheduleReconnect();
|
|
@@ -142,4 +137,31 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
142
137
|
getOfferId() {
|
|
143
138
|
return this.offerId;
|
|
144
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Handle remote ICE candidates received from polling
|
|
142
|
+
* Called by Rondevu when poll:ice event is received
|
|
143
|
+
*/
|
|
144
|
+
handleRemoteIceCandidates(candidates) {
|
|
145
|
+
if (!this.pc) {
|
|
146
|
+
this.debug('Cannot add ICE candidates: peer connection not initialized');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const iceCandidate of candidates) {
|
|
150
|
+
// Answerer only accepts offerer's candidates
|
|
151
|
+
if (iceCandidate.role !== 'offerer') {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (iceCandidate.candidate) {
|
|
155
|
+
const rtcCandidate = this.webrtcAdapter.createIceCandidate(iceCandidate.candidate);
|
|
156
|
+
this.pc
|
|
157
|
+
.addIceCandidate(rtcCandidate)
|
|
158
|
+
.then(() => {
|
|
159
|
+
this.emit('ice:candidate:remote', rtcCandidate);
|
|
160
|
+
})
|
|
161
|
+
.catch(error => {
|
|
162
|
+
this.debug('Failed to add ICE candidate:', error);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
145
167
|
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Base connection class with state machine, reconnection, and message buffering
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
-
import { ConnectionConfig } from './
|
|
6
|
-
import { ConnectionState, ConnectionEventMap } from './
|
|
7
|
-
import { ExponentialBackoff } from '
|
|
8
|
-
import { MessageBuffer } from '
|
|
5
|
+
import { ConnectionConfig } from './config.js';
|
|
6
|
+
import { ConnectionState, ConnectionEventMap } from './events.js';
|
|
7
|
+
import { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
8
|
+
import { MessageBuffer } from '../utils/message-buffer.js';
|
|
9
|
+
import { WebRTCAdapter } from '../webrtc/adapter.js';
|
|
9
10
|
/**
|
|
10
11
|
* Abstract base class for WebRTC connections with durability features
|
|
11
12
|
*/
|
|
@@ -15,6 +16,7 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
15
16
|
protected dc: RTCDataChannel | null;
|
|
16
17
|
protected state: ConnectionState;
|
|
17
18
|
protected config: ConnectionConfig;
|
|
19
|
+
protected webrtcAdapter: WebRTCAdapter;
|
|
18
20
|
protected messageBuffer: MessageBuffer | null;
|
|
19
21
|
protected backoff: ExponentialBackoff | null;
|
|
20
22
|
protected reconnectTimeout: ReturnType<typeof setTimeout> | null;
|
|
@@ -25,7 +27,7 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
25
27
|
protected lastIcePollTime: number;
|
|
26
28
|
protected answerProcessed: boolean;
|
|
27
29
|
protected answerSdpFingerprint: string | null;
|
|
28
|
-
constructor(rtcConfig?: RTCConfiguration | undefined, userConfig?: Partial<ConnectionConfig
|
|
30
|
+
constructor(rtcConfig?: RTCConfiguration | undefined, userConfig?: Partial<ConnectionConfig>, webrtcAdapter?: WebRTCAdapter);
|
|
29
31
|
/**
|
|
30
32
|
* Transition to a new state and emit events
|
|
31
33
|
*/
|
|
@@ -82,6 +84,28 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
82
84
|
* Stop ICE candidate polling
|
|
83
85
|
*/
|
|
84
86
|
protected stopIcePolling(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Get the API instance - subclasses must provide
|
|
89
|
+
*/
|
|
90
|
+
protected abstract getApi(): any;
|
|
91
|
+
/**
|
|
92
|
+
* Get the owner username - subclasses must provide
|
|
93
|
+
*/
|
|
94
|
+
protected abstract getOwnerUsername(): string;
|
|
95
|
+
/**
|
|
96
|
+
* Get the offer ID - subclasses must provide
|
|
97
|
+
*/
|
|
98
|
+
protected abstract getOfferId(): string;
|
|
99
|
+
/**
|
|
100
|
+
* Get the ICE candidate role this connection should accept.
|
|
101
|
+
* Returns null for no filtering (offerer), or specific role (answerer accepts 'offerer').
|
|
102
|
+
*/
|
|
103
|
+
protected abstract getIceCandidateRole(): 'offerer' | null;
|
|
104
|
+
/**
|
|
105
|
+
* Poll for remote ICE candidates (consolidated implementation)
|
|
106
|
+
* Subclasses implement getIceCandidateRole() to specify filtering
|
|
107
|
+
*/
|
|
108
|
+
protected pollIceCandidates(): void;
|
|
85
109
|
/**
|
|
86
110
|
* Start connection timeout
|
|
87
111
|
*/
|
|
@@ -141,8 +165,7 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
141
165
|
/**
|
|
142
166
|
* Debug logging helper
|
|
143
167
|
*/
|
|
144
|
-
protected debug(...args:
|
|
168
|
+
protected debug(...args: unknown[]): void;
|
|
145
169
|
protected abstract onLocalIceCandidate(candidate: RTCIceCandidate): void;
|
|
146
|
-
protected abstract pollIceCandidates(): void;
|
|
147
170
|
protected abstract attemptReconnect(): void;
|
|
148
171
|
}
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
* Base connection class with state machine, reconnection, and message buffering
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
-
import { mergeConnectionConfig } from './
|
|
6
|
-
import { ConnectionState, } from './
|
|
7
|
-
import { ExponentialBackoff } from '
|
|
8
|
-
import { MessageBuffer } from '
|
|
5
|
+
import { mergeConnectionConfig } from './config.js';
|
|
6
|
+
import { ConnectionState, } from './events.js';
|
|
7
|
+
import { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
8
|
+
import { MessageBuffer } from '../utils/message-buffer.js';
|
|
9
|
+
import { BrowserWebRTCAdapter } from '../webrtc/browser.js';
|
|
9
10
|
/**
|
|
10
11
|
* Abstract base class for WebRTC connections with durability features
|
|
11
12
|
*/
|
|
12
13
|
export class RondevuConnection extends EventEmitter {
|
|
13
|
-
constructor(rtcConfig, userConfig) {
|
|
14
|
+
constructor(rtcConfig, userConfig, webrtcAdapter) {
|
|
14
15
|
super();
|
|
15
16
|
this.rtcConfig = rtcConfig;
|
|
16
17
|
this.pc = null;
|
|
@@ -32,6 +33,7 @@ export class RondevuConnection extends EventEmitter {
|
|
|
32
33
|
this.answerProcessed = false;
|
|
33
34
|
this.answerSdpFingerprint = null;
|
|
34
35
|
this.config = mergeConnectionConfig(userConfig);
|
|
36
|
+
this.webrtcAdapter = webrtcAdapter || new BrowserWebRTCAdapter();
|
|
35
37
|
// Initialize message buffer if enabled
|
|
36
38
|
if (this.config.bufferEnabled) {
|
|
37
39
|
this.messageBuffer = new MessageBuffer({
|
|
@@ -81,9 +83,9 @@ export class RondevuConnection extends EventEmitter {
|
|
|
81
83
|
* Create and configure RTCPeerConnection
|
|
82
84
|
*/
|
|
83
85
|
createPeerConnection() {
|
|
84
|
-
this.pc =
|
|
86
|
+
this.pc = this.webrtcAdapter.createPeerConnection(this.rtcConfig);
|
|
85
87
|
// Setup event handlers BEFORE any signaling
|
|
86
|
-
this.pc.onicecandidate =
|
|
88
|
+
this.pc.onicecandidate = event => this.handleIceCandidate(event);
|
|
87
89
|
this.pc.oniceconnectionstatechange = () => this.handleIceConnectionStateChange();
|
|
88
90
|
this.pc.onconnectionstatechange = () => this.handleConnectionStateChange();
|
|
89
91
|
this.pc.onicegatheringstatechange = () => this.handleIceGatheringStateChange();
|
|
@@ -95,8 +97,8 @@ export class RondevuConnection extends EventEmitter {
|
|
|
95
97
|
setupDataChannelHandlers(dc) {
|
|
96
98
|
dc.onopen = () => this.handleDataChannelOpen();
|
|
97
99
|
dc.onclose = () => this.handleDataChannelClose();
|
|
98
|
-
dc.onerror =
|
|
99
|
-
dc.onmessage =
|
|
100
|
+
dc.onerror = error => this.handleDataChannelError(error);
|
|
101
|
+
dc.onmessage = event => this.handleMessage(event);
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
102
104
|
* Handle local ICE candidate generation
|
|
@@ -121,7 +123,8 @@ export class RondevuConnection extends EventEmitter {
|
|
|
121
123
|
if (this.state === ConnectionState.SIGNALING) {
|
|
122
124
|
this.transitionTo(ConnectionState.CHECKING, 'ICE checking started');
|
|
123
125
|
}
|
|
124
|
-
|
|
126
|
+
// Note: ICE candidate polling is handled by PollingManager
|
|
127
|
+
// Candidates are received via handleRemoteIceCandidates()
|
|
125
128
|
break;
|
|
126
129
|
case 'connected':
|
|
127
130
|
case 'completed':
|
|
@@ -191,7 +194,9 @@ export class RondevuConnection extends EventEmitter {
|
|
|
191
194
|
this.debug('Data channel opened');
|
|
192
195
|
this.emit('datachannel:open');
|
|
193
196
|
// Only transition to CONNECTED if ICE is also connected
|
|
194
|
-
if (this.pc &&
|
|
197
|
+
if (this.pc &&
|
|
198
|
+
(this.pc.iceConnectionState === 'connected' ||
|
|
199
|
+
this.pc.iceConnectionState === 'completed')) {
|
|
195
200
|
this.transitionTo(ConnectionState.CONNECTED, 'Data channel opened and ICE connected');
|
|
196
201
|
this.onConnected();
|
|
197
202
|
}
|
|
@@ -256,9 +261,14 @@ export class RondevuConnection extends EventEmitter {
|
|
|
256
261
|
return;
|
|
257
262
|
this.debug('Starting ICE polling');
|
|
258
263
|
this.emit('ice:polling:started');
|
|
259
|
-
|
|
264
|
+
// Use 0 instead of Date.now() to get ALL existing candidates on first poll
|
|
265
|
+
// Remote candidates may have been created before this peer's ICE checking started
|
|
266
|
+
this.lastIcePollTime = 0;
|
|
267
|
+
const pollingStartTime = Date.now();
|
|
268
|
+
// Immediately poll for existing candidates
|
|
269
|
+
this.pollIceCandidates();
|
|
260
270
|
this.icePollingInterval = setInterval(() => {
|
|
261
|
-
const elapsed = Date.now() -
|
|
271
|
+
const elapsed = Date.now() - pollingStartTime;
|
|
262
272
|
if (elapsed > this.config.icePollingTimeout) {
|
|
263
273
|
this.debug('ICE polling timeout');
|
|
264
274
|
this.stopIcePolling();
|
|
@@ -278,6 +288,46 @@ export class RondevuConnection extends EventEmitter {
|
|
|
278
288
|
this.icePollingInterval = null;
|
|
279
289
|
this.emit('ice:polling:stopped');
|
|
280
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Poll for remote ICE candidates (consolidated implementation)
|
|
293
|
+
* Subclasses implement getIceCandidateRole() to specify filtering
|
|
294
|
+
*/
|
|
295
|
+
pollIceCandidates() {
|
|
296
|
+
const acceptRole = this.getIceCandidateRole();
|
|
297
|
+
const api = this.getApi();
|
|
298
|
+
const offerId = this.getOfferId();
|
|
299
|
+
api.getOfferIceCandidates(offerId, this.lastIcePollTime)
|
|
300
|
+
.then((result) => {
|
|
301
|
+
if (result.candidates.length > 0) {
|
|
302
|
+
this.debug(`Received ${result.candidates.length} remote ICE candidates`);
|
|
303
|
+
for (const iceCandidate of result.candidates) {
|
|
304
|
+
// Filter by role if specified (answerer only filters for 'offerer')
|
|
305
|
+
if (acceptRole !== null && iceCandidate.role !== acceptRole) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (iceCandidate.candidate && this.pc) {
|
|
309
|
+
const candidate = iceCandidate.candidate;
|
|
310
|
+
const rtcCandidate = this.webrtcAdapter.createIceCandidate(candidate);
|
|
311
|
+
this.pc
|
|
312
|
+
.addIceCandidate(rtcCandidate)
|
|
313
|
+
.then(() => {
|
|
314
|
+
this.emit('ice:candidate:remote', rtcCandidate);
|
|
315
|
+
})
|
|
316
|
+
.catch(error => {
|
|
317
|
+
this.debug('Failed to add ICE candidate:', error);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
// Update last poll time
|
|
321
|
+
if (iceCandidate.createdAt > this.lastIcePollTime) {
|
|
322
|
+
this.lastIcePollTime = iceCandidate.createdAt;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
.catch((error) => {
|
|
328
|
+
this.debug('Failed to poll ICE candidates:', error);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
281
331
|
/**
|
|
282
332
|
* Start connection timeout
|
|
283
333
|
*/
|
|
@@ -329,7 +379,8 @@ export class RondevuConnection extends EventEmitter {
|
|
|
329
379
|
if (!this.config.reconnectEnabled || !this.backoff)
|
|
330
380
|
return;
|
|
331
381
|
// Check if we've exceeded max attempts
|
|
332
|
-
if (this.config.maxReconnectAttempts > 0 &&
|
|
382
|
+
if (this.config.maxReconnectAttempts > 0 &&
|
|
383
|
+
this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
333
384
|
this.debug('Max reconnection attempts reached');
|
|
334
385
|
this.emit('reconnect:exhausted', this.reconnectAttempts);
|
|
335
386
|
return;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection configuration interfaces and defaults
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Simplified connection options for public API.
|
|
6
|
+
* Advanced options use sensible defaults.
|
|
7
|
+
*/
|
|
8
|
+
export interface ConnectionOptions {
|
|
9
|
+
/** Maximum time to wait for connection (ms). Default: 30000 */
|
|
10
|
+
timeout?: number;
|
|
11
|
+
/** Enable automatic reconnection on failures. Default: true */
|
|
12
|
+
reconnect?: boolean;
|
|
13
|
+
/** Maximum reconnection attempts (0 = infinite). Default: 5 */
|
|
14
|
+
maxReconnects?: number;
|
|
15
|
+
/** Buffer messages during disconnections. Default: true */
|
|
16
|
+
bufferMessages?: boolean;
|
|
17
|
+
/** Enable debug logging. Default: false */
|
|
18
|
+
debug?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Full internal configuration with all options.
|
|
22
|
+
* Use ConnectionOptions for public API.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export interface ConnectionConfig {
|
|
26
|
+
connectionTimeout: number;
|
|
27
|
+
iceGatheringTimeout: number;
|
|
28
|
+
reconnectEnabled: boolean;
|
|
29
|
+
maxReconnectAttempts: number;
|
|
30
|
+
reconnectBackoffBase: number;
|
|
31
|
+
reconnectBackoffMax: number;
|
|
32
|
+
reconnectJitter: number;
|
|
33
|
+
bufferEnabled: boolean;
|
|
34
|
+
maxBufferSize: number;
|
|
35
|
+
maxBufferAge: number;
|
|
36
|
+
preserveBufferOnClose: boolean;
|
|
37
|
+
icePollingInterval: number;
|
|
38
|
+
icePollingTimeout: number;
|
|
39
|
+
debug: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare const DEFAULT_CONNECTION_CONFIG: ConnectionConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Merge user config with defaults.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare function mergeConnectionConfig(userConfig?: Partial<ConnectionConfig>): ConnectionConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Convert simplified ConnectionOptions to full ConnectionConfig.
|
|
49
|
+
* Maps user-friendly names to internal config.
|
|
50
|
+
*/
|
|
51
|
+
export declare function toConnectionConfig(options?: ConnectionOptions): ConnectionConfig;
|
|
@@ -22,9 +22,29 @@ export const DEFAULT_CONNECTION_CONFIG = {
|
|
|
22
22
|
// Debug
|
|
23
23
|
debug: false,
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Merge user config with defaults.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
25
29
|
export function mergeConnectionConfig(userConfig) {
|
|
26
30
|
return {
|
|
27
31
|
...DEFAULT_CONNECTION_CONFIG,
|
|
28
32
|
...userConfig,
|
|
29
33
|
};
|
|
30
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Convert simplified ConnectionOptions to full ConnectionConfig.
|
|
37
|
+
* Maps user-friendly names to internal config.
|
|
38
|
+
*/
|
|
39
|
+
export function toConnectionConfig(options) {
|
|
40
|
+
if (!options)
|
|
41
|
+
return DEFAULT_CONNECTION_CONFIG;
|
|
42
|
+
return {
|
|
43
|
+
...DEFAULT_CONNECTION_CONFIG,
|
|
44
|
+
...(options.timeout !== undefined && { connectionTimeout: options.timeout }),
|
|
45
|
+
...(options.reconnect !== undefined && { reconnectEnabled: options.reconnect }),
|
|
46
|
+
...(options.maxReconnects !== undefined && { maxReconnectAttempts: options.maxReconnects }),
|
|
47
|
+
...(options.bufferMessages !== undefined && { bufferEnabled: options.bufferMessages }),
|
|
48
|
+
...(options.debug !== undefined && { debug: options.debug }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -35,17 +35,17 @@ export interface StateChangeInfo {
|
|
|
35
35
|
*/
|
|
36
36
|
export interface ConnectionEventMap {
|
|
37
37
|
'state:changed': [StateChangeInfo];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
connecting: [];
|
|
39
|
+
connected: [];
|
|
40
|
+
disconnected: [reason?: string];
|
|
41
|
+
failed: [error: Error];
|
|
42
|
+
closed: [reason?: string];
|
|
43
43
|
'reconnect:scheduled': [ReconnectInfo];
|
|
44
44
|
'reconnect:attempting': [attempt: number];
|
|
45
45
|
'reconnect:success': [];
|
|
46
46
|
'reconnect:failed': [error: Error];
|
|
47
47
|
'reconnect:exhausted': [attempts: number];
|
|
48
|
-
|
|
48
|
+
message: [data: string | ArrayBuffer | Blob];
|
|
49
49
|
'message:sent': [data: string | ArrayBuffer | Blob, buffered: boolean];
|
|
50
50
|
'message:buffered': [data: string | ArrayBuffer | Blob];
|
|
51
51
|
'message:replayed': [message: BufferedMessage];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offerer-side WebRTC connection with offer creation and answer processing
|
|
3
|
+
*/
|
|
4
|
+
import { RondevuConnection } from './base.js';
|
|
5
|
+
import { RondevuAPI, IceCandidate } from '../api/client.js';
|
|
6
|
+
import { ConnectionConfig } from './config.js';
|
|
7
|
+
import { WebRTCAdapter } from '../webrtc/adapter.js';
|
|
8
|
+
export interface OffererOptions {
|
|
9
|
+
api: RondevuAPI;
|
|
10
|
+
ownerUsername: string;
|
|
11
|
+
offerId: string;
|
|
12
|
+
pc: RTCPeerConnection;
|
|
13
|
+
dc?: RTCDataChannel;
|
|
14
|
+
webrtcAdapter?: WebRTCAdapter;
|
|
15
|
+
config?: Partial<ConnectionConfig>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Offerer connection - manages already-created offers and waits for answers
|
|
19
|
+
*/
|
|
20
|
+
export declare class OffererConnection extends RondevuConnection {
|
|
21
|
+
private api;
|
|
22
|
+
private ownerUsername;
|
|
23
|
+
private offerId;
|
|
24
|
+
private _peerUsername;
|
|
25
|
+
private rotationLock;
|
|
26
|
+
private rotating;
|
|
27
|
+
private rotationAttempts;
|
|
28
|
+
private static readonly MAX_ROTATION_ATTEMPTS;
|
|
29
|
+
private pendingIceCandidates;
|
|
30
|
+
constructor(options: OffererOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the connection - setup handlers for already-created offer
|
|
33
|
+
*/
|
|
34
|
+
initialize(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Process an answer from the answerer
|
|
37
|
+
*/
|
|
38
|
+
processAnswer(sdp: string, answererId: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Rebind this connection to a new offer (when previous offer failed)
|
|
41
|
+
* Keeps the same connection object alive but with new underlying WebRTC
|
|
42
|
+
*/
|
|
43
|
+
rebindToOffer(newOfferId: string, newPc: RTCPeerConnection, newDc?: RTCDataChannel): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Check if connection is currently rotating
|
|
46
|
+
*/
|
|
47
|
+
isRotating(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Override onConnected to reset rotation attempts
|
|
50
|
+
*/
|
|
51
|
+
protected onConnected(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Generate a hash fingerprint of SDP for deduplication
|
|
54
|
+
*/
|
|
55
|
+
private hashSdp;
|
|
56
|
+
/**
|
|
57
|
+
* Handle local ICE candidate generation
|
|
58
|
+
*/
|
|
59
|
+
protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
|
|
60
|
+
/**
|
|
61
|
+
* Get the API instance
|
|
62
|
+
*/
|
|
63
|
+
protected getApi(): any;
|
|
64
|
+
/**
|
|
65
|
+
* Get the owner username
|
|
66
|
+
*/
|
|
67
|
+
protected getOwnerUsername(): string;
|
|
68
|
+
/**
|
|
69
|
+
* Offerers accept all ICE candidates (no filtering)
|
|
70
|
+
*/
|
|
71
|
+
protected getIceCandidateRole(): 'offerer' | null;
|
|
72
|
+
/**
|
|
73
|
+
* Attempt to reconnect (required by abstract base class)
|
|
74
|
+
*
|
|
75
|
+
* For OffererConnection, traditional reconnection is NOT used.
|
|
76
|
+
* Instead, the OfferPool handles failures via offer rotation:
|
|
77
|
+
*
|
|
78
|
+
* 1. When this connection fails, the 'failed' event is emitted
|
|
79
|
+
* 2. OfferPool detects the failure and calls createNewOfferForRotation()
|
|
80
|
+
* 3. The new offer is published to the server
|
|
81
|
+
* 4. This connection is rebound via rebindToOffer()
|
|
82
|
+
*
|
|
83
|
+
* This approach ensures the answerer always gets a fresh offer
|
|
84
|
+
* rather than trying to reconnect to a stale one.
|
|
85
|
+
*
|
|
86
|
+
* @see OfferPool.createNewOfferForRotation() - creates replacement offer
|
|
87
|
+
* @see OffererConnection.rebindToOffer() - rebinds connection to new offer
|
|
88
|
+
*/
|
|
89
|
+
protected attemptReconnect(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Get the offer ID
|
|
92
|
+
*/
|
|
93
|
+
getOfferId(): string;
|
|
94
|
+
/**
|
|
95
|
+
* Get the peer username (who answered this offer)
|
|
96
|
+
* Returns null if no answer has been processed yet
|
|
97
|
+
*/
|
|
98
|
+
get peerUsername(): string | null;
|
|
99
|
+
/**
|
|
100
|
+
* Handle remote ICE candidates received from polling
|
|
101
|
+
* Called by OfferPool when poll:ice event is received
|
|
102
|
+
*/
|
|
103
|
+
handleRemoteIceCandidates(candidates: IceCandidate[]): void;
|
|
104
|
+
/**
|
|
105
|
+
* Apply ICE candidates to the peer connection
|
|
106
|
+
*/
|
|
107
|
+
private applyIceCandidates;
|
|
108
|
+
}
|