@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
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * Answerer-side WebRTC connection with answer creation and offer processing
3
3
  */
4
- import { RondevuConnection } from './connection.js';
5
- import { ConnectionState } from './connection-events.js';
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.serviceFqn = options.serviceFqn;
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 = (event) => {
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.serviceFqn, this.offerId, answer.sdp);
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.serviceFqn, this.offerId, [
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((error) => {
67
+ .catch(error => {
65
68
  this.debug('Failed to send ICE candidate:', error);
66
69
  });
67
70
  }
68
71
  /**
69
- * Poll for remote ICE candidates (from offerer)
72
+ * Get the API instance
70
73
  */
71
- pollIceCandidates() {
72
- this.api
73
- .getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastIcePollTime)
74
- .then((result) => {
75
- if (result.candidates.length > 0) {
76
- this.debug(`Received ${result.candidates.length} remote ICE candidates`);
77
- for (const iceCandidate of result.candidates) {
78
- // Only process ICE candidates from the offerer
79
- if (iceCandidate.role === 'offerer' && iceCandidate.candidate && this.pc) {
80
- const candidate = iceCandidate.candidate;
81
- this.pc
82
- .addIceCandidate(new RTCIceCandidate(candidate))
83
- .then(() => {
84
- this.emit('ice:candidate:remote', new RTCIceCandidate(candidate));
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('Attempting to reconnect');
106
- // For answerer, we need to fetch a new offer and create a new answer
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
- // Fetch new offer from service
104
+ // Discover new offer using tags (use paginated mode to get array)
117
105
  this.api
118
- .getService(this.serviceFqn)
119
- .then((service) => {
120
- if (!service || !service.offers || service.offers.length === 0) {
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
- // Pick a random offer
124
- const offer = service.offers[Math.floor(Math.random() * service.offers.length)];
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((error) => {
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 './connection-config.js';
6
- import { ConnectionState, ConnectionEventMap } from './connection-events.js';
7
- import { ExponentialBackoff } from './exponential-backoff.js';
8
- import { MessageBuffer } from './message-buffer.js';
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: any[]): void;
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 './connection-config.js';
6
- import { ConnectionState, } from './connection-events.js';
7
- import { ExponentialBackoff } from './exponential-backoff.js';
8
- import { MessageBuffer } from './message-buffer.js';
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 = new RTCPeerConnection(this.rtcConfig);
86
+ this.pc = this.webrtcAdapter.createPeerConnection(this.rtcConfig);
85
87
  // Setup event handlers BEFORE any signaling
86
- this.pc.onicecandidate = (event) => this.handleIceCandidate(event);
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 = (error) => this.handleDataChannelError(error);
99
- dc.onmessage = (event) => this.handleMessage(event);
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
- this.startIcePolling();
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 && (this.pc.iceConnectionState === 'connected' || this.pc.iceConnectionState === 'completed')) {
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
- this.lastIcePollTime = Date.now();
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() - this.lastIcePollTime;
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 && this.reconnectAttempts >= this.config.maxReconnectAttempts) {
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
- 'connecting': [];
39
- 'connected': [];
40
- 'disconnected': [reason?: string];
41
- 'failed': [error: Error];
42
- 'closed': [reason?: string];
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
- 'message': [data: string | ArrayBuffer | Blob];
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
+ }