@xtr-dev/rondevu-client 0.20.1 → 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 +83 -385
- package/dist/api/batcher.d.ts +60 -38
- package/dist/api/batcher.js +121 -77
- package/dist/api/client.d.ts +104 -61
- package/dist/api/client.js +273 -185
- package/dist/connections/answerer.d.ts +15 -6
- package/dist/connections/answerer.js +56 -19
- package/dist/connections/base.d.ts +6 -4
- package/dist/connections/base.js +26 -16
- package/dist/connections/config.d.ts +30 -0
- package/dist/connections/config.js +20 -0
- package/dist/connections/events.d.ts +6 -6
- package/dist/connections/offerer.d.ts +37 -8
- package/dist/connections/offerer.js +92 -24
- package/dist/core/ice-config.d.ts +35 -0
- package/dist/core/ice-config.js +111 -0
- package/dist/core/index.d.ts +18 -18
- package/dist/core/index.js +18 -13
- package/dist/core/offer-pool.d.ts +30 -11
- package/dist/core/offer-pool.js +90 -76
- 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 +106 -209
- package/dist/core/rondevu.js +221 -349
- package/dist/crypto/adapter.d.ts +25 -9
- package/dist/crypto/node.d.ts +27 -5
- package/dist/crypto/node.js +96 -25
- package/dist/crypto/web.d.ts +26 -4
- package/dist/crypto/web.js +102 -25
- package/dist/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 +17 -6
|
@@ -8,9 +8,10 @@ import { ConnectionState } from './events.js';
|
|
|
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,14 +57,14 @@ 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
|
}
|
|
@@ -72,10 +75,10 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
72
75
|
return this.api;
|
|
73
76
|
}
|
|
74
77
|
/**
|
|
75
|
-
* Get the
|
|
78
|
+
* Get the owner username
|
|
76
79
|
*/
|
|
77
|
-
|
|
78
|
-
return this.
|
|
80
|
+
getOwnerUsername() {
|
|
81
|
+
return this.ownerUsername;
|
|
79
82
|
}
|
|
80
83
|
/**
|
|
81
84
|
* Answerers accept ICE candidates from offerers only
|
|
@@ -84,11 +87,11 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
84
87
|
return 'offerer';
|
|
85
88
|
}
|
|
86
89
|
/**
|
|
87
|
-
* Attempt to reconnect
|
|
90
|
+
* Attempt to reconnect to the same user
|
|
88
91
|
*/
|
|
89
92
|
attemptReconnect() {
|
|
90
|
-
this.debug(
|
|
91
|
-
// 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
|
|
92
95
|
// Clean up old connection
|
|
93
96
|
if (this.pc) {
|
|
94
97
|
this.pc.close();
|
|
@@ -98,24 +101,31 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
98
101
|
this.dc.close();
|
|
99
102
|
this.dc = null;
|
|
100
103
|
}
|
|
101
|
-
//
|
|
104
|
+
// Discover new offer using tags (use paginated mode to get array)
|
|
102
105
|
this.api
|
|
103
|
-
.
|
|
104
|
-
.then(
|
|
105
|
-
|
|
106
|
+
.discover({ tags: this.tags, limit: 100 })
|
|
107
|
+
.then(result => {
|
|
108
|
+
const response = result;
|
|
109
|
+
if (!response || !response.offers || response.offers.length === 0) {
|
|
106
110
|
throw new Error('No offers available for reconnection');
|
|
107
111
|
}
|
|
108
|
-
//
|
|
109
|
-
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)];
|
|
110
119
|
this.offerId = offer.offerId;
|
|
111
120
|
this.offerSdp = offer.sdp;
|
|
121
|
+
this.debug(`Found new offer ${offer.offerId} from ${this.ownerUsername}`);
|
|
112
122
|
// Reinitialize with new offer
|
|
113
123
|
return this.initialize();
|
|
114
124
|
})
|
|
115
125
|
.then(() => {
|
|
116
126
|
this.emit('reconnect:success');
|
|
117
127
|
})
|
|
118
|
-
.catch(
|
|
128
|
+
.catch(error => {
|
|
119
129
|
this.debug('Reconnection failed:', error);
|
|
120
130
|
this.emit('reconnect:failed', error);
|
|
121
131
|
this.scheduleReconnect();
|
|
@@ -127,4 +137,31 @@ export class AnswererConnection extends RondevuConnection {
|
|
|
127
137
|
getOfferId() {
|
|
128
138
|
return this.offerId;
|
|
129
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
|
+
}
|
|
130
167
|
}
|
|
@@ -6,6 +6,7 @@ import { ConnectionConfig } from './config.js';
|
|
|
6
6
|
import { ConnectionState, ConnectionEventMap } from './events.js';
|
|
7
7
|
import { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
8
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
|
*/
|
|
@@ -87,9 +89,9 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
87
89
|
*/
|
|
88
90
|
protected abstract getApi(): any;
|
|
89
91
|
/**
|
|
90
|
-
* Get the
|
|
92
|
+
* Get the owner username - subclasses must provide
|
|
91
93
|
*/
|
|
92
|
-
protected abstract
|
|
94
|
+
protected abstract getOwnerUsername(): string;
|
|
93
95
|
/**
|
|
94
96
|
* Get the offer ID - subclasses must provide
|
|
95
97
|
*/
|
|
@@ -163,7 +165,7 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
|
|
|
163
165
|
/**
|
|
164
166
|
* Debug logging helper
|
|
165
167
|
*/
|
|
166
|
-
protected debug(...args:
|
|
168
|
+
protected debug(...args: unknown[]): void;
|
|
167
169
|
protected abstract onLocalIceCandidate(candidate: RTCIceCandidate): void;
|
|
168
170
|
protected abstract attemptReconnect(): void;
|
|
169
171
|
}
|
package/dist/connections/base.js
CHANGED
|
@@ -6,11 +6,12 @@ import { mergeConnectionConfig } from './config.js';
|
|
|
6
6
|
import { ConnectionState, } from './events.js';
|
|
7
7
|
import { ExponentialBackoff } from '../utils/exponential-backoff.js';
|
|
8
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();
|
|
@@ -285,10 +295,8 @@ export class RondevuConnection extends EventEmitter {
|
|
|
285
295
|
pollIceCandidates() {
|
|
286
296
|
const acceptRole = this.getIceCandidateRole();
|
|
287
297
|
const api = this.getApi();
|
|
288
|
-
const serviceFqn = this.getServiceFqn();
|
|
289
298
|
const offerId = this.getOfferId();
|
|
290
|
-
api
|
|
291
|
-
.getOfferIceCandidates(serviceFqn, offerId, this.lastIcePollTime)
|
|
299
|
+
api.getOfferIceCandidates(offerId, this.lastIcePollTime)
|
|
292
300
|
.then((result) => {
|
|
293
301
|
if (result.candidates.length > 0) {
|
|
294
302
|
this.debug(`Received ${result.candidates.length} remote ICE candidates`);
|
|
@@ -299,12 +307,13 @@ export class RondevuConnection extends EventEmitter {
|
|
|
299
307
|
}
|
|
300
308
|
if (iceCandidate.candidate && this.pc) {
|
|
301
309
|
const candidate = iceCandidate.candidate;
|
|
310
|
+
const rtcCandidate = this.webrtcAdapter.createIceCandidate(candidate);
|
|
302
311
|
this.pc
|
|
303
|
-
.addIceCandidate(
|
|
312
|
+
.addIceCandidate(rtcCandidate)
|
|
304
313
|
.then(() => {
|
|
305
|
-
this.emit('ice:candidate:remote',
|
|
314
|
+
this.emit('ice:candidate:remote', rtcCandidate);
|
|
306
315
|
})
|
|
307
|
-
.catch(
|
|
316
|
+
.catch(error => {
|
|
308
317
|
this.debug('Failed to add ICE candidate:', error);
|
|
309
318
|
});
|
|
310
319
|
}
|
|
@@ -370,7 +379,8 @@ export class RondevuConnection extends EventEmitter {
|
|
|
370
379
|
if (!this.config.reconnectEnabled || !this.backoff)
|
|
371
380
|
return;
|
|
372
381
|
// Check if we've exceeded max attempts
|
|
373
|
-
if (this.config.maxReconnectAttempts > 0 &&
|
|
382
|
+
if (this.config.maxReconnectAttempts > 0 &&
|
|
383
|
+
this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
374
384
|
this.debug('Max reconnection attempts reached');
|
|
375
385
|
this.emit('reconnect:exhausted', this.reconnectAttempts);
|
|
376
386
|
return;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connection configuration interfaces and defaults
|
|
3
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
|
+
*/
|
|
4
25
|
export interface ConnectionConfig {
|
|
5
26
|
connectionTimeout: number;
|
|
6
27
|
iceGatheringTimeout: number;
|
|
@@ -18,4 +39,13 @@ export interface ConnectionConfig {
|
|
|
18
39
|
debug: boolean;
|
|
19
40
|
}
|
|
20
41
|
export declare const DEFAULT_CONNECTION_CONFIG: ConnectionConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Merge user config with defaults.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
21
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];
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* Offerer-side WebRTC connection with offer creation and answer processing
|
|
3
3
|
*/
|
|
4
4
|
import { RondevuConnection } from './base.js';
|
|
5
|
-
import { RondevuAPI } from '../api/client.js';
|
|
5
|
+
import { RondevuAPI, IceCandidate } from '../api/client.js';
|
|
6
6
|
import { ConnectionConfig } from './config.js';
|
|
7
|
+
import { WebRTCAdapter } from '../webrtc/adapter.js';
|
|
7
8
|
export interface OffererOptions {
|
|
8
9
|
api: RondevuAPI;
|
|
9
|
-
|
|
10
|
+
ownerUsername: string;
|
|
10
11
|
offerId: string;
|
|
11
12
|
pc: RTCPeerConnection;
|
|
12
13
|
dc?: RTCDataChannel;
|
|
14
|
+
webrtcAdapter?: WebRTCAdapter;
|
|
13
15
|
config?: Partial<ConnectionConfig>;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
@@ -17,12 +19,14 @@ export interface OffererOptions {
|
|
|
17
19
|
*/
|
|
18
20
|
export declare class OffererConnection extends RondevuConnection {
|
|
19
21
|
private api;
|
|
20
|
-
private
|
|
22
|
+
private ownerUsername;
|
|
21
23
|
private offerId;
|
|
24
|
+
private _peerUsername;
|
|
22
25
|
private rotationLock;
|
|
23
26
|
private rotating;
|
|
24
27
|
private rotationAttempts;
|
|
25
28
|
private static readonly MAX_ROTATION_ATTEMPTS;
|
|
29
|
+
private pendingIceCandidates;
|
|
26
30
|
constructor(options: OffererOptions);
|
|
27
31
|
/**
|
|
28
32
|
* Initialize the connection - setup handlers for already-created offer
|
|
@@ -58,22 +62,47 @@ export declare class OffererConnection extends RondevuConnection {
|
|
|
58
62
|
*/
|
|
59
63
|
protected getApi(): any;
|
|
60
64
|
/**
|
|
61
|
-
* Get the
|
|
65
|
+
* Get the owner username
|
|
62
66
|
*/
|
|
63
|
-
protected
|
|
67
|
+
protected getOwnerUsername(): string;
|
|
64
68
|
/**
|
|
65
69
|
* Offerers accept all ICE candidates (no filtering)
|
|
66
70
|
*/
|
|
67
71
|
protected getIceCandidateRole(): 'offerer' | null;
|
|
68
72
|
/**
|
|
69
|
-
* Attempt to reconnect
|
|
73
|
+
* Attempt to reconnect (required by abstract base class)
|
|
70
74
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
|
73
88
|
*/
|
|
74
89
|
protected attemptReconnect(): void;
|
|
75
90
|
/**
|
|
76
91
|
* Get the offer ID
|
|
77
92
|
*/
|
|
78
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;
|
|
79
108
|
}
|