@xtr-dev/rondevu-client 0.18.9 → 0.20.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 (40) hide show
  1. package/README.md +324 -47
  2. package/dist/{api.d.ts → api/client.d.ts} +17 -8
  3. package/dist/{api.js → api/client.js} +114 -81
  4. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +13 -5
  5. package/dist/{answerer-connection.js → connections/answerer.js} +17 -32
  6. package/dist/{connection.d.ts → connections/base.d.ts} +26 -5
  7. package/dist/{connection.js → connections/base.js} +45 -4
  8. package/dist/{offerer-connection.d.ts → connections/offerer.d.ts} +30 -5
  9. package/dist/{offerer-connection.js → connections/offerer.js} +93 -32
  10. package/dist/core/index.d.ts +22 -0
  11. package/dist/core/index.js +17 -0
  12. package/dist/core/offer-pool.d.ts +94 -0
  13. package/dist/core/offer-pool.js +267 -0
  14. package/dist/{rondevu.d.ts → core/rondevu.d.ts} +7 -28
  15. package/dist/{rondevu.js → core/rondevu.js} +32 -175
  16. package/dist/{node-crypto-adapter.d.ts → crypto/node.d.ts} +1 -1
  17. package/dist/{web-crypto-adapter.d.ts → crypto/web.d.ts} +1 -1
  18. package/dist/utils/async-lock.d.ts +42 -0
  19. package/dist/utils/async-lock.js +75 -0
  20. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  21. package/package.json +3 -3
  22. package/dist/index.d.ts +0 -22
  23. package/dist/index.js +0 -17
  24. package/dist/rondevu-signaler.d.ts +0 -112
  25. package/dist/rondevu-signaler.js +0 -401
  26. /package/dist/{rpc-batcher.d.ts → api/batcher.d.ts} +0 -0
  27. /package/dist/{rpc-batcher.js → api/batcher.js} +0 -0
  28. /package/dist/{connection-config.d.ts → connections/config.d.ts} +0 -0
  29. /package/dist/{connection-config.js → connections/config.js} +0 -0
  30. /package/dist/{connection-events.d.ts → connections/events.d.ts} +0 -0
  31. /package/dist/{connection-events.js → connections/events.js} +0 -0
  32. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  33. /package/dist/{types.js → core/types.js} +0 -0
  34. /package/dist/{crypto-adapter.d.ts → crypto/adapter.d.ts} +0 -0
  35. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  36. /package/dist/{node-crypto-adapter.js → crypto/node.js} +0 -0
  37. /package/dist/{web-crypto-adapter.js → crypto/web.js} +0 -0
  38. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  39. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
  40. /package/dist/{message-buffer.js → utils/message-buffer.js} +0 -0
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Rondevu API Client - RPC interface
3
3
  */
4
- import { WebCryptoAdapter } from './web-crypto-adapter.js';
5
- import { RpcBatcher } from './rpc-batcher.js';
4
+ import { WebCryptoAdapter } from '../crypto/web.js';
5
+ import { RpcBatcher } from './batcher.js';
6
6
  /**
7
7
  * RondevuAPI - RPC-based API client for Rondevu signaling server
8
8
  */
@@ -20,34 +20,90 @@ export class RondevuAPI {
20
20
  }
21
21
  }
22
22
  /**
23
- * Generate authentication parameters for RPC calls
23
+ * Create canonical JSON string with sorted keys for deterministic signing
24
24
  */
25
- async generateAuth(method, params = '') {
25
+ canonicalJSON(obj) {
26
+ if (obj === null || obj === undefined) {
27
+ return JSON.stringify(obj);
28
+ }
29
+ if (typeof obj !== 'object') {
30
+ return JSON.stringify(obj);
31
+ }
32
+ if (Array.isArray(obj)) {
33
+ return '[' + obj.map(item => this.canonicalJSON(item)).join(',') + ']';
34
+ }
35
+ const sortedKeys = Object.keys(obj).sort();
36
+ const pairs = sortedKeys.map(key => {
37
+ return JSON.stringify(key) + ':' + this.canonicalJSON(obj[key]);
38
+ });
39
+ return '{' + pairs.join(',') + '}';
40
+ }
41
+ /**
42
+ * Generate authentication headers for RPC request
43
+ * Signs the payload (method + params + timestamp + username)
44
+ */
45
+ async generateAuthHeaders(request, includePublicKey = false) {
26
46
  const timestamp = Date.now();
27
- const message = params
28
- ? `${method}:${this.username}:${params}:${timestamp}`
29
- : `${method}:${this.username}:${timestamp}`;
30
- const signature = await this.crypto.signMessage(message, this.keypair.privateKey);
31
- return { message, signature };
47
+ // Create payload with timestamp and username for signing: { method, params, timestamp, username }
48
+ const payload = { ...request, timestamp, username: this.username };
49
+ // Create canonical JSON representation for signing
50
+ const canonical = this.canonicalJSON(payload);
51
+ // Sign the canonical representation
52
+ const signature = await this.crypto.signMessage(canonical, this.keypair.privateKey);
53
+ const headers = {
54
+ 'X-Signature': signature,
55
+ 'X-Timestamp': timestamp.toString(),
56
+ 'X-Username': this.username,
57
+ };
58
+ if (includePublicKey) {
59
+ headers['X-Public-Key'] = this.keypair.publicKey;
60
+ }
61
+ return headers;
62
+ }
63
+ /**
64
+ * Generate authentication fields embedded in request body (for batch requests)
65
+ * Signs the payload (method + params + timestamp + username)
66
+ */
67
+ async generateAuthForRequest(request, includePublicKey = false) {
68
+ const timestamp = Date.now();
69
+ // Create payload with timestamp and username for signing: { method, params, timestamp, username }
70
+ const payload = { ...request, timestamp, username: this.username };
71
+ // Create canonical JSON representation for signing
72
+ const canonical = this.canonicalJSON(payload);
73
+ // Sign the canonical representation
74
+ const signature = await this.crypto.signMessage(canonical, this.keypair.privateKey);
75
+ const authRequest = {
76
+ ...request,
77
+ signature,
78
+ timestamp,
79
+ username: this.username,
80
+ };
81
+ if (includePublicKey) {
82
+ authRequest.publicKey = this.keypair.publicKey;
83
+ }
84
+ return authRequest;
32
85
  }
33
86
  /**
34
87
  * Execute RPC call with optional batching
35
88
  */
36
- async rpc(request) {
89
+ async rpc(request, authHeaders) {
37
90
  // Use batcher if enabled
38
91
  if (this.batcher) {
39
92
  return await this.batcher.add(request);
40
93
  }
41
94
  // Direct call without batching
42
- return await this.rpcDirect(request);
95
+ return await this.rpcDirect(request, authHeaders);
43
96
  }
44
97
  /**
45
98
  * Execute single RPC call directly (bypasses batcher)
46
99
  */
47
- async rpcDirect(request) {
100
+ async rpcDirect(request, authHeaders) {
48
101
  const response = await fetch(`${this.baseUrl}/rpc`, {
49
102
  method: 'POST',
50
- headers: { 'Content-Type': 'application/json' },
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ ...authHeaders,
106
+ },
51
107
  body: JSON.stringify(request),
52
108
  });
53
109
  if (!response.ok) {
@@ -61,30 +117,25 @@ export class RondevuAPI {
61
117
  }
62
118
  /**
63
119
  * Execute batch RPC calls directly (bypasses batcher)
120
+ * Each request in the batch has its own embedded authentication (signature, timestamp, username, publicKey)
64
121
  */
65
122
  async rpcBatchDirect(requests) {
123
+ // Add auth to each request in the batch
124
+ const requestsWithAuth = await Promise.all(requests.map(req => this.generateAuthForRequest(req, true)));
66
125
  const response = await fetch(`${this.baseUrl}/rpc`, {
67
126
  method: 'POST',
68
127
  headers: { 'Content-Type': 'application/json' },
69
- body: JSON.stringify(requests),
128
+ body: JSON.stringify(requestsWithAuth),
70
129
  });
71
130
  if (!response.ok) {
72
131
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
73
132
  }
74
133
  const results = await response.json();
75
- // Validate response is an array
76
134
  if (!Array.isArray(results)) {
77
- console.error('Invalid RPC batch response:', results);
78
135
  throw new Error('Server returned invalid batch response (not an array)');
79
136
  }
80
- // Check response length matches request length
81
- if (results.length !== requests.length) {
82
- console.error(`Response length mismatch: expected ${requests.length}, got ${results.length}`);
83
- }
137
+ // Map results, throwing error for any failed request
84
138
  return results.map((result, i) => {
85
- if (!result || typeof result !== 'object') {
86
- throw new Error(`Invalid response at index ${i}`);
87
- }
88
139
  if (!result.success) {
89
140
  throw new Error(result.error || `RPC call ${i} failed`);
90
141
  }
@@ -125,26 +176,24 @@ export class RondevuAPI {
125
176
  * Check if a username is available
126
177
  */
127
178
  async isUsernameAvailable(username) {
128
- const auth = await this.generateAuth('getUser', username);
129
- const result = await this.rpc({
179
+ const request = {
130
180
  method: 'getUser',
131
- message: auth.message,
132
- signature: auth.signature,
133
181
  params: { username },
134
- });
182
+ };
183
+ const authHeaders = await this.generateAuthHeaders(request, false);
184
+ const result = await this.rpc(request, authHeaders);
135
185
  return result.available;
136
186
  }
137
187
  /**
138
188
  * Check if current username is claimed
139
189
  */
140
190
  async isUsernameClaimed() {
141
- const auth = await this.generateAuth('getUser', this.username);
142
- const result = await this.rpc({
191
+ const request = {
143
192
  method: 'getUser',
144
- message: auth.message,
145
- signature: auth.signature,
146
193
  params: { username: this.username },
147
- });
194
+ };
195
+ const authHeaders = await this.generateAuthHeaders(request, false);
196
+ const result = await this.rpc(request, authHeaders);
148
197
  return !result.available;
149
198
  }
150
199
  // ============================================
@@ -154,47 +203,41 @@ export class RondevuAPI {
154
203
  * Publish a service
155
204
  */
156
205
  async publishService(service) {
157
- const auth = await this.generateAuth('publishService', service.serviceFqn);
158
- return await this.rpc({
206
+ const request = {
159
207
  method: 'publishService',
160
- message: auth.message,
161
- signature: auth.signature,
162
- publicKey: this.keypair.publicKey,
163
208
  params: {
164
209
  serviceFqn: service.serviceFqn,
165
210
  offers: service.offers,
166
211
  ttl: service.ttl,
167
212
  },
168
- });
213
+ };
214
+ const authHeaders = await this.generateAuthHeaders(request, true);
215
+ return await this.rpc(request, authHeaders);
169
216
  }
170
217
  /**
171
218
  * Get service by FQN (direct lookup, random, or paginated)
172
219
  */
173
220
  async getService(serviceFqn, options) {
174
- const auth = await this.generateAuth('getService', serviceFqn);
175
- return await this.rpc({
221
+ const request = {
176
222
  method: 'getService',
177
- message: auth.message,
178
- signature: auth.signature,
179
- publicKey: this.keypair.publicKey,
180
223
  params: {
181
224
  serviceFqn,
182
225
  ...options,
183
226
  },
184
- });
227
+ };
228
+ const authHeaders = await this.generateAuthHeaders(request, true);
229
+ return await this.rpc(request, authHeaders);
185
230
  }
186
231
  /**
187
232
  * Delete a service
188
233
  */
189
234
  async deleteService(serviceFqn) {
190
- const auth = await this.generateAuth('deleteService', serviceFqn);
191
- await this.rpc({
235
+ const request = {
192
236
  method: 'deleteService',
193
- message: auth.message,
194
- signature: auth.signature,
195
- publicKey: this.keypair.publicKey,
196
237
  params: { serviceFqn },
197
- });
238
+ };
239
+ const authHeaders = await this.generateAuthHeaders(request, true);
240
+ await this.rpc(request, authHeaders);
198
241
  }
199
242
  // ============================================
200
243
  // WebRTC Signaling
@@ -203,28 +246,24 @@ export class RondevuAPI {
203
246
  * Answer an offer
204
247
  */
205
248
  async answerOffer(serviceFqn, offerId, sdp) {
206
- const auth = await this.generateAuth('answerOffer', offerId);
207
- await this.rpc({
249
+ const request = {
208
250
  method: 'answerOffer',
209
- message: auth.message,
210
- signature: auth.signature,
211
- publicKey: this.keypair.publicKey,
212
251
  params: { serviceFqn, offerId, sdp },
213
- });
252
+ };
253
+ const authHeaders = await this.generateAuthHeaders(request, true);
254
+ await this.rpc(request, authHeaders);
214
255
  }
215
256
  /**
216
257
  * Get answer for a specific offer (offerer polls this)
217
258
  */
218
259
  async getOfferAnswer(serviceFqn, offerId) {
219
260
  try {
220
- const auth = await this.generateAuth('getOfferAnswer', offerId);
221
- return await this.rpc({
261
+ const request = {
222
262
  method: 'getOfferAnswer',
223
- message: auth.message,
224
- signature: auth.signature,
225
- publicKey: this.keypair.publicKey,
226
263
  params: { serviceFqn, offerId },
227
- });
264
+ };
265
+ const authHeaders = await this.generateAuthHeaders(request, true);
266
+ return await this.rpc(request, authHeaders);
228
267
  }
229
268
  catch (err) {
230
269
  if (err.message.includes('not yet answered')) {
@@ -237,40 +276,34 @@ export class RondevuAPI {
237
276
  * Combined polling for answers and ICE candidates
238
277
  */
239
278
  async poll(since) {
240
- const auth = await this.generateAuth('poll');
241
- return await this.rpc({
279
+ const request = {
242
280
  method: 'poll',
243
- message: auth.message,
244
- signature: auth.signature,
245
- publicKey: this.keypair.publicKey,
246
281
  params: { since },
247
- });
282
+ };
283
+ const authHeaders = await this.generateAuthHeaders(request, true);
284
+ return await this.rpc(request, authHeaders);
248
285
  }
249
286
  /**
250
287
  * Add ICE candidates to a specific offer
251
288
  */
252
289
  async addOfferIceCandidates(serviceFqn, offerId, candidates) {
253
- const auth = await this.generateAuth('addIceCandidates', offerId);
254
- return await this.rpc({
290
+ const request = {
255
291
  method: 'addIceCandidates',
256
- message: auth.message,
257
- signature: auth.signature,
258
- publicKey: this.keypair.publicKey,
259
292
  params: { serviceFqn, offerId, candidates },
260
- });
293
+ };
294
+ const authHeaders = await this.generateAuthHeaders(request, true);
295
+ return await this.rpc(request, authHeaders);
261
296
  }
262
297
  /**
263
298
  * Get ICE candidates for a specific offer
264
299
  */
265
300
  async getOfferIceCandidates(serviceFqn, offerId, since = 0) {
266
- const auth = await this.generateAuth('getIceCandidates', `${offerId}:${since}`);
267
- const result = await this.rpc({
301
+ const request = {
268
302
  method: 'getIceCandidates',
269
- message: auth.message,
270
- signature: auth.signature,
271
- publicKey: this.keypair.publicKey,
272
303
  params: { serviceFqn, offerId, since },
273
- });
304
+ };
305
+ const authHeaders = await this.generateAuthHeaders(request, true);
306
+ const result = await this.rpc(request, authHeaders);
274
307
  return {
275
308
  candidates: result.candidates || [],
276
309
  offerId: result.offerId,
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Answerer-side WebRTC connection with answer creation and offer processing
3
3
  */
4
- import { RondevuConnection } from './connection.js';
5
- import { RondevuAPI } from './api.js';
6
- import { ConnectionConfig } from './connection-config.js';
4
+ import { RondevuConnection } from './base.js';
5
+ import { RondevuAPI } from '../api/client.js';
6
+ import { ConnectionConfig } from './config.js';
7
7
  export interface AnswererOptions {
8
8
  api: RondevuAPI;
9
9
  serviceFqn: string;
@@ -30,9 +30,17 @@ export declare class AnswererConnection extends RondevuConnection {
30
30
  */
31
31
  protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
32
32
  /**
33
- * Poll for remote ICE candidates (from offerer)
33
+ * Get the API instance
34
34
  */
35
- protected pollIceCandidates(): void;
35
+ protected getApi(): any;
36
+ /**
37
+ * Get the service FQN
38
+ */
39
+ protected getServiceFqn(): string;
40
+ /**
41
+ * Answerers accept ICE candidates from offerers only
42
+ */
43
+ protected getIceCandidateRole(): 'offerer' | null;
36
44
  /**
37
45
  * Attempt to reconnect
38
46
  */
@@ -1,8 +1,8 @@
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
  */
@@ -66,37 +66,22 @@ export class AnswererConnection extends RondevuConnection {
66
66
  });
67
67
  }
68
68
  /**
69
- * Poll for remote ICE candidates (from offerer)
69
+ * Get the API instance
70
70
  */
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
- });
71
+ getApi() {
72
+ return this.api;
73
+ }
74
+ /**
75
+ * Get the service FQN
76
+ */
77
+ getServiceFqn() {
78
+ return this.serviceFqn;
79
+ }
80
+ /**
81
+ * Answerers accept ICE candidates from offerers only
82
+ */
83
+ getIceCandidateRole() {
84
+ return 'offerer';
100
85
  }
101
86
  /**
102
87
  * Attempt to reconnect
@@ -2,10 +2,10 @@
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
9
  /**
10
10
  * Abstract base class for WebRTC connections with durability features
11
11
  */
@@ -82,6 +82,28 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
82
82
  * Stop ICE candidate polling
83
83
  */
84
84
  protected stopIcePolling(): void;
85
+ /**
86
+ * Get the API instance - subclasses must provide
87
+ */
88
+ protected abstract getApi(): any;
89
+ /**
90
+ * Get the service FQN - subclasses must provide
91
+ */
92
+ protected abstract getServiceFqn(): string;
93
+ /**
94
+ * Get the offer ID - subclasses must provide
95
+ */
96
+ protected abstract getOfferId(): string;
97
+ /**
98
+ * Get the ICE candidate role this connection should accept.
99
+ * Returns null for no filtering (offerer), or specific role (answerer accepts 'offerer').
100
+ */
101
+ protected abstract getIceCandidateRole(): 'offerer' | null;
102
+ /**
103
+ * Poll for remote ICE candidates (consolidated implementation)
104
+ * Subclasses implement getIceCandidateRole() to specify filtering
105
+ */
106
+ protected pollIceCandidates(): void;
85
107
  /**
86
108
  * Start connection timeout
87
109
  */
@@ -143,6 +165,5 @@ export declare abstract class RondevuConnection extends EventEmitter<ConnectionE
143
165
  */
144
166
  protected debug(...args: any[]): void;
145
167
  protected abstract onLocalIceCandidate(candidate: RTCIceCandidate): void;
146
- protected abstract pollIceCandidates(): void;
147
168
  protected abstract attemptReconnect(): void;
148
169
  }
@@ -2,10 +2,10 @@
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
9
  /**
10
10
  * Abstract base class for WebRTC connections with durability features
11
11
  */
@@ -278,6 +278,47 @@ export class RondevuConnection extends EventEmitter {
278
278
  this.icePollingInterval = null;
279
279
  this.emit('ice:polling:stopped');
280
280
  }
281
+ /**
282
+ * Poll for remote ICE candidates (consolidated implementation)
283
+ * Subclasses implement getIceCandidateRole() to specify filtering
284
+ */
285
+ pollIceCandidates() {
286
+ const acceptRole = this.getIceCandidateRole();
287
+ const api = this.getApi();
288
+ const serviceFqn = this.getServiceFqn();
289
+ const offerId = this.getOfferId();
290
+ api
291
+ .getOfferIceCandidates(serviceFqn, offerId, this.lastIcePollTime)
292
+ .then((result) => {
293
+ if (result.candidates.length > 0) {
294
+ this.debug(`Received ${result.candidates.length} remote ICE candidates`);
295
+ for (const iceCandidate of result.candidates) {
296
+ // Filter by role if specified (answerer only filters for 'offerer')
297
+ if (acceptRole !== null && iceCandidate.role !== acceptRole) {
298
+ continue;
299
+ }
300
+ if (iceCandidate.candidate && this.pc) {
301
+ const candidate = iceCandidate.candidate;
302
+ this.pc
303
+ .addIceCandidate(new RTCIceCandidate(candidate))
304
+ .then(() => {
305
+ this.emit('ice:candidate:remote', new RTCIceCandidate(candidate));
306
+ })
307
+ .catch((error) => {
308
+ this.debug('Failed to add ICE candidate:', error);
309
+ });
310
+ }
311
+ // Update last poll time
312
+ if (iceCandidate.createdAt > this.lastIcePollTime) {
313
+ this.lastIcePollTime = iceCandidate.createdAt;
314
+ }
315
+ }
316
+ }
317
+ })
318
+ .catch((error) => {
319
+ this.debug('Failed to poll ICE candidates:', error);
320
+ });
321
+ }
281
322
  /**
282
323
  * Start connection timeout
283
324
  */
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Offerer-side WebRTC connection with offer creation and answer processing
3
3
  */
4
- import { RondevuConnection } from './connection.js';
5
- import { RondevuAPI } from './api.js';
6
- import { ConnectionConfig } from './connection-config.js';
4
+ import { RondevuConnection } from './base.js';
5
+ import { RondevuAPI } from '../api/client.js';
6
+ import { ConnectionConfig } from './config.js';
7
7
  export interface OffererOptions {
8
8
  api: RondevuAPI;
9
9
  serviceFqn: string;
@@ -19,6 +19,10 @@ export declare class OffererConnection extends RondevuConnection {
19
19
  private api;
20
20
  private serviceFqn;
21
21
  private offerId;
22
+ private rotationLock;
23
+ private rotating;
24
+ private rotationAttempts;
25
+ private static readonly MAX_ROTATION_ATTEMPTS;
22
26
  constructor(options: OffererOptions);
23
27
  /**
24
28
  * Initialize the connection - setup handlers for already-created offer
@@ -28,6 +32,19 @@ export declare class OffererConnection extends RondevuConnection {
28
32
  * Process an answer from the answerer
29
33
  */
30
34
  processAnswer(sdp: string, answererId: string): Promise<void>;
35
+ /**
36
+ * Rebind this connection to a new offer (when previous offer failed)
37
+ * Keeps the same connection object alive but with new underlying WebRTC
38
+ */
39
+ rebindToOffer(newOfferId: string, newPc: RTCPeerConnection, newDc?: RTCDataChannel): Promise<void>;
40
+ /**
41
+ * Check if connection is currently rotating
42
+ */
43
+ isRotating(): boolean;
44
+ /**
45
+ * Override onConnected to reset rotation attempts
46
+ */
47
+ protected onConnected(): void;
31
48
  /**
32
49
  * Generate a hash fingerprint of SDP for deduplication
33
50
  */
@@ -37,9 +54,17 @@ export declare class OffererConnection extends RondevuConnection {
37
54
  */
38
55
  protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
39
56
  /**
40
- * Poll for remote ICE candidates
57
+ * Get the API instance
58
+ */
59
+ protected getApi(): any;
60
+ /**
61
+ * Get the service FQN
62
+ */
63
+ protected getServiceFqn(): string;
64
+ /**
65
+ * Offerers accept all ICE candidates (no filtering)
41
66
  */
42
- protected pollIceCandidates(): void;
67
+ protected getIceCandidateRole(): 'offerer' | null;
43
68
  /**
44
69
  * Attempt to reconnect
45
70
  *