@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.
Files changed (70) hide show
  1. package/README.md +92 -117
  2. package/dist/api/batcher.d.ts +83 -0
  3. package/dist/api/batcher.js +155 -0
  4. package/dist/api/client.d.ts +198 -0
  5. package/dist/api/client.js +400 -0
  6. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
  7. package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
  8. package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
  9. package/dist/{connection.js → connections/base.js} +65 -14
  10. package/dist/connections/config.d.ts +51 -0
  11. package/dist/{connection-config.js → connections/config.js} +20 -0
  12. package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
  13. package/dist/connections/offerer.d.ts +108 -0
  14. package/dist/connections/offerer.js +306 -0
  15. package/dist/core/ice-config.d.ts +35 -0
  16. package/dist/core/ice-config.js +111 -0
  17. package/dist/core/index.d.ts +22 -0
  18. package/dist/core/index.js +22 -0
  19. package/dist/core/offer-pool.d.ts +113 -0
  20. package/dist/core/offer-pool.js +281 -0
  21. package/dist/core/peer.d.ts +155 -0
  22. package/dist/core/peer.js +252 -0
  23. package/dist/core/polling-manager.d.ts +71 -0
  24. package/dist/core/polling-manager.js +122 -0
  25. package/dist/core/rondevu-errors.d.ts +59 -0
  26. package/dist/core/rondevu-errors.js +75 -0
  27. package/dist/core/rondevu-types.d.ts +125 -0
  28. package/dist/core/rondevu-types.js +6 -0
  29. package/dist/core/rondevu.d.ts +296 -0
  30. package/dist/core/rondevu.js +472 -0
  31. package/dist/crypto/adapter.d.ts +53 -0
  32. package/dist/crypto/node.d.ts +57 -0
  33. package/dist/crypto/node.js +149 -0
  34. package/dist/crypto/web.d.ts +38 -0
  35. package/dist/crypto/web.js +129 -0
  36. package/dist/utils/async-lock.d.ts +42 -0
  37. package/dist/utils/async-lock.js +75 -0
  38. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  39. package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
  40. package/dist/webrtc/adapter.d.ts +22 -0
  41. package/dist/webrtc/adapter.js +5 -0
  42. package/dist/webrtc/browser.d.ts +12 -0
  43. package/dist/webrtc/browser.js +15 -0
  44. package/dist/webrtc/node.d.ts +32 -0
  45. package/dist/webrtc/node.js +32 -0
  46. package/package.json +20 -9
  47. package/dist/api.d.ts +0 -146
  48. package/dist/api.js +0 -279
  49. package/dist/connection-config.d.ts +0 -21
  50. package/dist/crypto-adapter.d.ts +0 -37
  51. package/dist/index.d.ts +0 -13
  52. package/dist/index.js +0 -10
  53. package/dist/node-crypto-adapter.d.ts +0 -35
  54. package/dist/node-crypto-adapter.js +0 -78
  55. package/dist/offerer-connection.d.ts +0 -54
  56. package/dist/offerer-connection.js +0 -177
  57. package/dist/rondevu-signaler.d.ts +0 -112
  58. package/dist/rondevu-signaler.js +0 -401
  59. package/dist/rondevu.d.ts +0 -407
  60. package/dist/rondevu.js +0 -847
  61. package/dist/rpc-batcher.d.ts +0 -61
  62. package/dist/rpc-batcher.js +0 -111
  63. package/dist/web-crypto-adapter.d.ts +0 -16
  64. package/dist/web-crypto-adapter.js +0 -52
  65. /package/dist/{connection-events.js → connections/events.js} +0 -0
  66. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  67. /package/dist/{types.js → core/types.js} +0 -0
  68. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  69. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  70. /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 {};