@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
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offerer-side WebRTC connection with offer creation and answer processing
|
|
3
|
+
*/
|
|
4
|
+
import { RondevuConnection } from './base.js';
|
|
5
|
+
import { ConnectionState } from './events.js';
|
|
6
|
+
import { AsyncLock } from '../utils/async-lock.js';
|
|
7
|
+
/**
|
|
8
|
+
* Offerer connection - manages already-created offers and waits for answers
|
|
9
|
+
*/
|
|
10
|
+
export class OffererConnection extends RondevuConnection {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
// Force reconnectEnabled: false for offerer connections (offers are ephemeral)
|
|
13
|
+
super(undefined, { ...options.config, reconnectEnabled: false }, options.webrtcAdapter);
|
|
14
|
+
this._peerUsername = null;
|
|
15
|
+
// Rotation tracking
|
|
16
|
+
this.rotationLock = new AsyncLock();
|
|
17
|
+
this.rotating = false;
|
|
18
|
+
this.rotationAttempts = 0;
|
|
19
|
+
// ICE candidate buffering (for candidates received before answer is processed)
|
|
20
|
+
this.pendingIceCandidates = [];
|
|
21
|
+
this.api = options.api;
|
|
22
|
+
this.ownerUsername = options.ownerUsername;
|
|
23
|
+
this.offerId = options.offerId;
|
|
24
|
+
// Use the already-created peer connection and data channel
|
|
25
|
+
this.pc = options.pc;
|
|
26
|
+
this.dc = options.dc || null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the connection - setup handlers for already-created offer
|
|
30
|
+
*/
|
|
31
|
+
async initialize() {
|
|
32
|
+
this.debug('Initializing offerer connection');
|
|
33
|
+
if (!this.pc)
|
|
34
|
+
throw new Error('Peer connection not provided');
|
|
35
|
+
// Setup peer connection event handlers
|
|
36
|
+
this.pc.onicecandidate = event => this.handleIceCandidate(event);
|
|
37
|
+
this.pc.oniceconnectionstatechange = () => this.handleIceConnectionStateChange();
|
|
38
|
+
this.pc.onconnectionstatechange = () => this.handleConnectionStateChange();
|
|
39
|
+
this.pc.onicegatheringstatechange = () => this.handleIceGatheringStateChange();
|
|
40
|
+
// Setup data channel handlers if we have one
|
|
41
|
+
if (this.dc) {
|
|
42
|
+
this.setupDataChannelHandlers(this.dc);
|
|
43
|
+
}
|
|
44
|
+
// Start connection timeout
|
|
45
|
+
this.startConnectionTimeout();
|
|
46
|
+
// Transition to signaling state (offer already created and published)
|
|
47
|
+
this.transitionTo(ConnectionState.SIGNALING, 'Offer published, waiting for answer');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Process an answer from the answerer
|
|
51
|
+
*/
|
|
52
|
+
async processAnswer(sdp, answererId) {
|
|
53
|
+
if (!this.pc) {
|
|
54
|
+
this.debug('Cannot process answer: peer connection not initialized');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Generate SDP fingerprint for deduplication
|
|
58
|
+
const fingerprint = await this.hashSdp(sdp);
|
|
59
|
+
// Check for duplicate answer
|
|
60
|
+
if (this.answerProcessed) {
|
|
61
|
+
if (this.answerSdpFingerprint === fingerprint) {
|
|
62
|
+
this.debug('Duplicate answer detected (same fingerprint), skipping');
|
|
63
|
+
this.emit('answer:duplicate', this.offerId);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
throw new Error('Received different answer after already processing one (protocol violation)');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Validate state - allow SIGNALING, CHECKING, and FAILED (for late-arriving answers before rotation)
|
|
71
|
+
if (this.state !== ConnectionState.SIGNALING &&
|
|
72
|
+
this.state !== ConnectionState.CHECKING &&
|
|
73
|
+
this.state !== ConnectionState.FAILED) {
|
|
74
|
+
this.debug(`Cannot process answer in state ${this.state}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Mark as processed BEFORE setRemoteDescription to prevent race conditions
|
|
78
|
+
this.answerProcessed = true;
|
|
79
|
+
this.answerSdpFingerprint = fingerprint;
|
|
80
|
+
try {
|
|
81
|
+
await this.pc.setRemoteDescription({
|
|
82
|
+
type: 'answer',
|
|
83
|
+
sdp,
|
|
84
|
+
});
|
|
85
|
+
// Store the peer username
|
|
86
|
+
this._peerUsername = answererId;
|
|
87
|
+
this.debug(`Answer processed successfully from ${answererId}`);
|
|
88
|
+
this.emit('answer:processed', this.offerId, answererId);
|
|
89
|
+
// Apply any buffered ICE candidates that arrived before the answer
|
|
90
|
+
if (this.pendingIceCandidates.length > 0) {
|
|
91
|
+
this.debug(`Applying ${this.pendingIceCandidates.length} buffered ICE candidates`);
|
|
92
|
+
const buffered = this.pendingIceCandidates;
|
|
93
|
+
this.pendingIceCandidates = [];
|
|
94
|
+
this.applyIceCandidates(buffered);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// Reset flags on error so we can try again
|
|
99
|
+
this.answerProcessed = false;
|
|
100
|
+
this.answerSdpFingerprint = null;
|
|
101
|
+
this.debug('Failed to set remote description:', error);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Rebind this connection to a new offer (when previous offer failed)
|
|
107
|
+
* Keeps the same connection object alive but with new underlying WebRTC
|
|
108
|
+
*/
|
|
109
|
+
async rebindToOffer(newOfferId, newPc, newDc) {
|
|
110
|
+
return this.rotationLock.run(async () => {
|
|
111
|
+
if (this.rotating) {
|
|
112
|
+
throw new Error('Rotation already in progress');
|
|
113
|
+
}
|
|
114
|
+
this.rotating = true;
|
|
115
|
+
try {
|
|
116
|
+
this.rotationAttempts++;
|
|
117
|
+
if (this.rotationAttempts > OffererConnection.MAX_ROTATION_ATTEMPTS) {
|
|
118
|
+
throw new Error('Max rotation attempts exceeded');
|
|
119
|
+
}
|
|
120
|
+
this.debug(`Rebinding connection from ${this.offerId} to ${newOfferId}`);
|
|
121
|
+
// 1. Clean up old peer connection
|
|
122
|
+
if (this.pc) {
|
|
123
|
+
this.pc.close();
|
|
124
|
+
}
|
|
125
|
+
if (this.dc && this.dc !== newDc) {
|
|
126
|
+
this.dc.close();
|
|
127
|
+
}
|
|
128
|
+
// 2. Update to new offer
|
|
129
|
+
this.offerId = newOfferId;
|
|
130
|
+
this.pc = newPc;
|
|
131
|
+
this.dc = newDc || null;
|
|
132
|
+
// 3. Reset answer processing flags, peer username, and pending candidates
|
|
133
|
+
this.answerProcessed = false;
|
|
134
|
+
this.answerSdpFingerprint = null;
|
|
135
|
+
this._peerUsername = null;
|
|
136
|
+
this.pendingIceCandidates = [];
|
|
137
|
+
// 4. Setup event handlers for new peer connection
|
|
138
|
+
this.pc.onicecandidate = event => this.handleIceCandidate(event);
|
|
139
|
+
this.pc.oniceconnectionstatechange = () => this.handleIceConnectionStateChange();
|
|
140
|
+
this.pc.onconnectionstatechange = () => this.handleConnectionStateChange();
|
|
141
|
+
this.pc.onicegatheringstatechange = () => this.handleIceGatheringStateChange();
|
|
142
|
+
// 5. Setup data channel handlers if we have one
|
|
143
|
+
if (this.dc) {
|
|
144
|
+
this.setupDataChannelHandlers(this.dc);
|
|
145
|
+
}
|
|
146
|
+
// 6. Restart connection timeout
|
|
147
|
+
this.startConnectionTimeout();
|
|
148
|
+
// 7. Transition to SIGNALING state (waiting for answer)
|
|
149
|
+
this.transitionTo(ConnectionState.SIGNALING, 'Offer rotated, waiting for answer');
|
|
150
|
+
// Note: Message buffer is NOT cleared - it persists!
|
|
151
|
+
this.debug(`Rebind complete. Buffer has ${this.messageBuffer?.size() ?? 0} messages`);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
this.rotating = false;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Check if connection is currently rotating
|
|
160
|
+
*/
|
|
161
|
+
isRotating() {
|
|
162
|
+
return this.rotating;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Override onConnected to reset rotation attempts
|
|
166
|
+
*/
|
|
167
|
+
onConnected() {
|
|
168
|
+
super.onConnected();
|
|
169
|
+
this.rotationAttempts = 0;
|
|
170
|
+
this.debug('Connection established, rotation attempts reset');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Generate a hash fingerprint of SDP for deduplication
|
|
174
|
+
*/
|
|
175
|
+
async hashSdp(sdp) {
|
|
176
|
+
// Simple hash using built-in crypto if available
|
|
177
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
178
|
+
const encoder = new TextEncoder();
|
|
179
|
+
const data = encoder.encode(sdp);
|
|
180
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
181
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
182
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Fallback: use simple string hash
|
|
186
|
+
let hash = 0;
|
|
187
|
+
for (let i = 0; i < sdp.length; i++) {
|
|
188
|
+
const char = sdp.charCodeAt(i);
|
|
189
|
+
hash = (hash << 5) - hash + char;
|
|
190
|
+
hash = hash & hash;
|
|
191
|
+
}
|
|
192
|
+
return hash.toString(16);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Handle local ICE candidate generation
|
|
197
|
+
*/
|
|
198
|
+
onLocalIceCandidate(candidate) {
|
|
199
|
+
this.debug('Generated local ICE candidate');
|
|
200
|
+
// Send ICE candidate to server
|
|
201
|
+
this.api
|
|
202
|
+
.addOfferIceCandidates(this.offerId, [
|
|
203
|
+
{
|
|
204
|
+
candidate: candidate.candidate,
|
|
205
|
+
sdpMLineIndex: candidate.sdpMLineIndex,
|
|
206
|
+
sdpMid: candidate.sdpMid,
|
|
207
|
+
},
|
|
208
|
+
])
|
|
209
|
+
.catch(error => {
|
|
210
|
+
this.debug('Failed to send ICE candidate:', error);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get the API instance
|
|
215
|
+
*/
|
|
216
|
+
getApi() {
|
|
217
|
+
return this.api;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get the owner username
|
|
221
|
+
*/
|
|
222
|
+
getOwnerUsername() {
|
|
223
|
+
return this.ownerUsername;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Offerers accept all ICE candidates (no filtering)
|
|
227
|
+
*/
|
|
228
|
+
getIceCandidateRole() {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Attempt to reconnect (required by abstract base class)
|
|
233
|
+
*
|
|
234
|
+
* For OffererConnection, traditional reconnection is NOT used.
|
|
235
|
+
* Instead, the OfferPool handles failures via offer rotation:
|
|
236
|
+
*
|
|
237
|
+
* 1. When this connection fails, the 'failed' event is emitted
|
|
238
|
+
* 2. OfferPool detects the failure and calls createNewOfferForRotation()
|
|
239
|
+
* 3. The new offer is published to the server
|
|
240
|
+
* 4. This connection is rebound via rebindToOffer()
|
|
241
|
+
*
|
|
242
|
+
* This approach ensures the answerer always gets a fresh offer
|
|
243
|
+
* rather than trying to reconnect to a stale one.
|
|
244
|
+
*
|
|
245
|
+
* @see OfferPool.createNewOfferForRotation() - creates replacement offer
|
|
246
|
+
* @see OffererConnection.rebindToOffer() - rebinds connection to new offer
|
|
247
|
+
*/
|
|
248
|
+
attemptReconnect() {
|
|
249
|
+
this.debug('Reconnection delegated to OfferPool rotation mechanism');
|
|
250
|
+
this.emit('reconnect:failed', new Error('Offerer uses rotation, not reconnection'));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Get the offer ID
|
|
254
|
+
*/
|
|
255
|
+
getOfferId() {
|
|
256
|
+
return this.offerId;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Get the peer username (who answered this offer)
|
|
260
|
+
* Returns null if no answer has been processed yet
|
|
261
|
+
*/
|
|
262
|
+
get peerUsername() {
|
|
263
|
+
return this._peerUsername;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Handle remote ICE candidates received from polling
|
|
267
|
+
* Called by OfferPool when poll:ice event is received
|
|
268
|
+
*/
|
|
269
|
+
handleRemoteIceCandidates(candidates) {
|
|
270
|
+
if (!this.pc) {
|
|
271
|
+
this.debug('Cannot add ICE candidates: peer connection not initialized');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// If answer hasn't been processed yet, buffer the candidates
|
|
275
|
+
if (!this.answerProcessed) {
|
|
276
|
+
this.debug(`Buffering ${candidates.length} ICE candidates (waiting for answer)`);
|
|
277
|
+
this.pendingIceCandidates.push(...candidates);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Answer is processed, apply candidates immediately
|
|
281
|
+
this.applyIceCandidates(candidates);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Apply ICE candidates to the peer connection
|
|
285
|
+
*/
|
|
286
|
+
applyIceCandidates(candidates) {
|
|
287
|
+
if (!this.pc)
|
|
288
|
+
return;
|
|
289
|
+
for (const iceCandidate of candidates) {
|
|
290
|
+
// Offerer accepts answerer's candidates (no role filtering needed here
|
|
291
|
+
// since OfferPool already filters by offerId)
|
|
292
|
+
if (iceCandidate.candidate) {
|
|
293
|
+
const rtcCandidate = this.webrtcAdapter.createIceCandidate(iceCandidate.candidate);
|
|
294
|
+
this.pc
|
|
295
|
+
.addIceCandidate(rtcCandidate)
|
|
296
|
+
.then(() => {
|
|
297
|
+
this.emit('ice:candidate:remote', rtcCandidate);
|
|
298
|
+
})
|
|
299
|
+
.catch(error => {
|
|
300
|
+
this.debug('Failed to add ICE candidate:', error);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
OffererConnection.MAX_ROTATION_ATTEMPTS = 5;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICE Configuration Types and Presets
|
|
3
|
+
*
|
|
4
|
+
* Provides typed ICE server presets for common WebRTC configurations.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Available ICE server preset names
|
|
8
|
+
*/
|
|
9
|
+
export type IceServerPreset = 'rondevu' | 'rondevu-relay' | 'rondevu-ipv4' | 'rondevu-ipv4-relay' | 'google-stun' | 'public-stun';
|
|
10
|
+
/**
|
|
11
|
+
* ICE preset configuration containing servers and optional transport policy.
|
|
12
|
+
* The iceTransportPolicy belongs on RTCConfiguration, not RTCIceServer.
|
|
13
|
+
*/
|
|
14
|
+
export interface IcePresetConfig {
|
|
15
|
+
iceServers: RTCIceServer[];
|
|
16
|
+
iceTransportPolicy?: RTCIceTransportPolicy;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Pre-configured ICE server presets.
|
|
20
|
+
*
|
|
21
|
+
* - `rondevu`: Official Rondevu TURN/STUN servers (recommended)
|
|
22
|
+
* - `rondevu-relay`: Same as rondevu but forces relay mode (hides client IPs)
|
|
23
|
+
* - `rondevu-ipv4`: Direct IPv4 address (for networks with DNS issues)
|
|
24
|
+
* - `rondevu-ipv4-relay`: IPv4 with forced relay mode
|
|
25
|
+
* - `google-stun`: Google's free STUN servers (no relay, direct connections only)
|
|
26
|
+
* - `public-stun`: Multiple public STUN servers for redundancy
|
|
27
|
+
*/
|
|
28
|
+
export declare const ICE_SERVER_PRESETS: Record<IceServerPreset, IcePresetConfig>;
|
|
29
|
+
/**
|
|
30
|
+
* Get the full RTCConfiguration for a preset or custom ICE servers.
|
|
31
|
+
*
|
|
32
|
+
* @param iceServers - Either a preset name or custom ICE servers array
|
|
33
|
+
* @returns Partial RTCConfiguration with iceServers and optional iceTransportPolicy
|
|
34
|
+
*/
|
|
35
|
+
export declare function getIceConfiguration(iceServers?: IceServerPreset | RTCIceServer[]): Pick<RTCConfiguration, 'iceServers' | 'iceTransportPolicy'>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICE Configuration Types and Presets
|
|
3
|
+
*
|
|
4
|
+
* Provides typed ICE server presets for common WebRTC configurations.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Pre-configured ICE server presets.
|
|
8
|
+
*
|
|
9
|
+
* - `rondevu`: Official Rondevu TURN/STUN servers (recommended)
|
|
10
|
+
* - `rondevu-relay`: Same as rondevu but forces relay mode (hides client IPs)
|
|
11
|
+
* - `rondevu-ipv4`: Direct IPv4 address (for networks with DNS issues)
|
|
12
|
+
* - `rondevu-ipv4-relay`: IPv4 with forced relay mode
|
|
13
|
+
* - `google-stun`: Google's free STUN servers (no relay, direct connections only)
|
|
14
|
+
* - `public-stun`: Multiple public STUN servers for redundancy
|
|
15
|
+
*/
|
|
16
|
+
export const ICE_SERVER_PRESETS = {
|
|
17
|
+
rondevu: {
|
|
18
|
+
iceServers: [
|
|
19
|
+
{ urls: 'stun:relay.ronde.vu:3478' },
|
|
20
|
+
{
|
|
21
|
+
urls: [
|
|
22
|
+
'turns:relay.ronde.vu:5349?transport=tcp',
|
|
23
|
+
'turn:relay.ronde.vu:3478?transport=tcp',
|
|
24
|
+
'turn:relay.ronde.vu:3478?transport=udp',
|
|
25
|
+
],
|
|
26
|
+
username: 'rondevu',
|
|
27
|
+
credential: 'rondevu-public-turn',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
'rondevu-relay': {
|
|
32
|
+
iceServers: [
|
|
33
|
+
{ urls: 'stun:relay.ronde.vu:3478' },
|
|
34
|
+
{
|
|
35
|
+
urls: [
|
|
36
|
+
'turns:relay.ronde.vu:5349?transport=tcp',
|
|
37
|
+
'turn:relay.ronde.vu:3478?transport=tcp',
|
|
38
|
+
'turn:relay.ronde.vu:3478?transport=udp',
|
|
39
|
+
],
|
|
40
|
+
username: 'rondevu',
|
|
41
|
+
credential: 'rondevu-public-turn',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
iceTransportPolicy: 'relay', // Force relay mode - hides client IPs
|
|
45
|
+
},
|
|
46
|
+
'rondevu-ipv4': {
|
|
47
|
+
iceServers: [
|
|
48
|
+
{ urls: 'stun:57.129.61.67:3478' },
|
|
49
|
+
{
|
|
50
|
+
urls: [
|
|
51
|
+
'turn:57.129.61.67:3478?transport=tcp',
|
|
52
|
+
'turn:57.129.61.67:3478?transport=udp',
|
|
53
|
+
],
|
|
54
|
+
username: 'rondevu',
|
|
55
|
+
credential: 'rondevu-public-turn',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
'rondevu-ipv4-relay': {
|
|
60
|
+
iceServers: [
|
|
61
|
+
{ urls: 'stun:57.129.61.67:3478' },
|
|
62
|
+
{
|
|
63
|
+
urls: [
|
|
64
|
+
'turn:57.129.61.67:3478?transport=tcp',
|
|
65
|
+
'turn:57.129.61.67:3478?transport=udp',
|
|
66
|
+
],
|
|
67
|
+
username: 'rondevu',
|
|
68
|
+
credential: 'rondevu-public-turn',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
iceTransportPolicy: 'relay',
|
|
72
|
+
},
|
|
73
|
+
'google-stun': {
|
|
74
|
+
iceServers: [
|
|
75
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
76
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
'public-stun': {
|
|
80
|
+
iceServers: [
|
|
81
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
82
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
83
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
84
|
+
{ urls: 'stun:stun.relay.metered.ca:80' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Get the full RTCConfiguration for a preset or custom ICE servers.
|
|
90
|
+
*
|
|
91
|
+
* @param iceServers - Either a preset name or custom ICE servers array
|
|
92
|
+
* @returns Partial RTCConfiguration with iceServers and optional iceTransportPolicy
|
|
93
|
+
*/
|
|
94
|
+
export function getIceConfiguration(iceServers) {
|
|
95
|
+
if (typeof iceServers === 'string') {
|
|
96
|
+
const preset = ICE_SERVER_PRESETS[iceServers];
|
|
97
|
+
return {
|
|
98
|
+
iceServers: preset.iceServers,
|
|
99
|
+
iceTransportPolicy: preset.iceTransportPolicy,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Default to rondevu preset if no ICE servers specified
|
|
103
|
+
if (!iceServers) {
|
|
104
|
+
const preset = ICE_SERVER_PRESETS.rondevu;
|
|
105
|
+
return {
|
|
106
|
+
iceServers: preset.iceServers,
|
|
107
|
+
iceTransportPolicy: preset.iceTransportPolicy,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { iceServers };
|
|
111
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xtr-dev/rondevu-client
|
|
3
|
+
* WebRTC peer signaling client
|
|
4
|
+
*
|
|
5
|
+
* Simple API:
|
|
6
|
+
* const rondevu = await Rondevu.connect()
|
|
7
|
+
*
|
|
8
|
+
* // Host: publish offers (auto-starts)
|
|
9
|
+
* const offer = await rondevu.offer({ tags: ['chat'], maxOffers: 5 })
|
|
10
|
+
* rondevu.on('connection:opened', (id, conn) => { ... })
|
|
11
|
+
* // Later: offer.cancel()
|
|
12
|
+
*
|
|
13
|
+
* // Guest: connect to a peer
|
|
14
|
+
* const peer = await rondevu.peer({ tags: ['chat'] })
|
|
15
|
+
* peer.on('open', () => peer.send('Hello!'))
|
|
16
|
+
*/
|
|
17
|
+
export { Rondevu } from './rondevu.js';
|
|
18
|
+
export { Peer } from './peer.js';
|
|
19
|
+
export { ICE_SERVER_PRESETS } from './ice-config.js';
|
|
20
|
+
export type { RondevuOptions, OfferOptions, OfferHandle, DiscoverOptions, DiscoverResult, } from './rondevu.js';
|
|
21
|
+
export type { PeerState, PeerOptions } from './peer.js';
|
|
22
|
+
export type { IceServerPreset } from './ice-config.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xtr-dev/rondevu-client
|
|
3
|
+
* WebRTC peer signaling client
|
|
4
|
+
*
|
|
5
|
+
* Simple API:
|
|
6
|
+
* const rondevu = await Rondevu.connect()
|
|
7
|
+
*
|
|
8
|
+
* // Host: publish offers (auto-starts)
|
|
9
|
+
* const offer = await rondevu.offer({ tags: ['chat'], maxOffers: 5 })
|
|
10
|
+
* rondevu.on('connection:opened', (id, conn) => { ... })
|
|
11
|
+
* // Later: offer.cancel()
|
|
12
|
+
*
|
|
13
|
+
* // Guest: connect to a peer
|
|
14
|
+
* const peer = await rondevu.peer({ tags: ['chat'] })
|
|
15
|
+
* peer.on('open', () => peer.send('Hello!'))
|
|
16
|
+
*/
|
|
17
|
+
// Main entry point
|
|
18
|
+
export { Rondevu } from './rondevu.js';
|
|
19
|
+
// Simplified peer connection
|
|
20
|
+
export { Peer } from './peer.js';
|
|
21
|
+
// ICE server configuration presets
|
|
22
|
+
export { ICE_SERVER_PRESETS } from './ice-config.js';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { RondevuAPI } from '../api/client.js';
|
|
3
|
+
import { OffererConnection } from '../connections/offerer.js';
|
|
4
|
+
import { ConnectionConfig } from '../connections/config.js';
|
|
5
|
+
import { WebRTCAdapter } from '../webrtc/adapter.js';
|
|
6
|
+
import type { PollAnswerEvent, PollIceEvent } from './polling-manager.js';
|
|
7
|
+
export type OfferFactory = (pc: RTCPeerConnection) => Promise<{
|
|
8
|
+
dc?: RTCDataChannel;
|
|
9
|
+
offer: RTCSessionDescriptionInit;
|
|
10
|
+
}>;
|
|
11
|
+
export interface OfferPoolOptions {
|
|
12
|
+
api: RondevuAPI;
|
|
13
|
+
tags: string[];
|
|
14
|
+
ownerUsername: string;
|
|
15
|
+
maxOffers: number;
|
|
16
|
+
offerFactory: OfferFactory;
|
|
17
|
+
ttl: number;
|
|
18
|
+
iceServers: RTCIceServer[];
|
|
19
|
+
iceTransportPolicy?: RTCIceTransportPolicy;
|
|
20
|
+
webrtcAdapter: WebRTCAdapter;
|
|
21
|
+
connectionConfig?: Partial<ConnectionConfig>;
|
|
22
|
+
debugEnabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
interface OfferPoolEvents {
|
|
25
|
+
'connection:opened': (offerId: string, connection: OffererConnection) => void;
|
|
26
|
+
'offer:created': (offerId: string, tags: string[]) => void;
|
|
27
|
+
'offer:failed': (offerId: string, error: Error) => void;
|
|
28
|
+
'connection:rotated': (oldOfferId: string, newOfferId: string, connection: OffererConnection) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* OfferPool manages a pool of WebRTC offers for published tags.
|
|
32
|
+
* Maintains a target number of active offers and automatically replaces
|
|
33
|
+
* offers that fail or get answered.
|
|
34
|
+
*/
|
|
35
|
+
export declare class OfferPool extends EventEmitter<OfferPoolEvents> {
|
|
36
|
+
private readonly api;
|
|
37
|
+
private readonly tags;
|
|
38
|
+
private readonly ownerUsername;
|
|
39
|
+
private readonly maxOffers;
|
|
40
|
+
private readonly offerFactory;
|
|
41
|
+
private readonly ttl;
|
|
42
|
+
private readonly iceServers;
|
|
43
|
+
private readonly iceTransportPolicy?;
|
|
44
|
+
private readonly webrtcAdapter;
|
|
45
|
+
private readonly connectionConfig?;
|
|
46
|
+
private readonly debugEnabled;
|
|
47
|
+
private readonly activeConnections;
|
|
48
|
+
private readonly fillLock;
|
|
49
|
+
private running;
|
|
50
|
+
constructor(options: OfferPoolOptions);
|
|
51
|
+
/**
|
|
52
|
+
* Start filling offers
|
|
53
|
+
* Polling is managed externally by Rondevu's PollingManager
|
|
54
|
+
*/
|
|
55
|
+
start(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Stop filling offers
|
|
58
|
+
* Closes all active connections
|
|
59
|
+
*/
|
|
60
|
+
stop(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Get count of active offers
|
|
63
|
+
*/
|
|
64
|
+
getOfferCount(): number;
|
|
65
|
+
/**
|
|
66
|
+
* Get all active connections
|
|
67
|
+
*/
|
|
68
|
+
getActiveConnections(): Map<string, OffererConnection>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if a specific offer is connected
|
|
71
|
+
*/
|
|
72
|
+
isConnected(offerId: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Disconnect all active offers
|
|
75
|
+
*/
|
|
76
|
+
disconnectAll(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Fill offers to reach maxOffers count
|
|
79
|
+
* Uses AsyncLock to prevent concurrent fills
|
|
80
|
+
*/
|
|
81
|
+
private fillOffers;
|
|
82
|
+
/**
|
|
83
|
+
* Create and publish an offer to the server.
|
|
84
|
+
* Shared logic used by both createOffer() and createNewOfferForRotation().
|
|
85
|
+
*
|
|
86
|
+
* @returns The offer ID, RTCPeerConnection, and optional data channel
|
|
87
|
+
*/
|
|
88
|
+
private createOfferAndPublish;
|
|
89
|
+
/**
|
|
90
|
+
* Create a new offer for rotation (reuses existing creation logic)
|
|
91
|
+
* Similar to createOffer() but only creates the offer, doesn't create connection
|
|
92
|
+
*/
|
|
93
|
+
private createNewOfferForRotation;
|
|
94
|
+
/**
|
|
95
|
+
* Create a single offer and publish it to the server
|
|
96
|
+
*/
|
|
97
|
+
private createOffer;
|
|
98
|
+
/**
|
|
99
|
+
* Handle poll:answer event from PollingManager
|
|
100
|
+
* Called by Rondevu when a poll:answer event is received
|
|
101
|
+
*/
|
|
102
|
+
handlePollAnswer(data: PollAnswerEvent): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Handle poll:ice event from PollingManager
|
|
105
|
+
* Called by Rondevu when a poll:ice event is received
|
|
106
|
+
*/
|
|
107
|
+
handlePollIce(data: PollIceEvent): void;
|
|
108
|
+
/**
|
|
109
|
+
* Debug logging (only if debug enabled)
|
|
110
|
+
*/
|
|
111
|
+
private debug;
|
|
112
|
+
}
|
|
113
|
+
export {};
|