@xtr-dev/rondevu-client 0.18.7 → 0.18.8

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/dist/rondevu.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { RondevuAPI } from './api.js';
2
2
  import { EventEmitter } from 'eventemitter3';
3
+ import { OffererConnection } from './offerer-connection.js';
4
+ import { AnswererConnection } from './answerer-connection.js';
3
5
  // ICE server presets
4
6
  export const ICE_SERVER_PRESETS = {
5
7
  'ipv4-turn': [
@@ -86,14 +88,13 @@ export class ConnectionError extends RondevuError {
86
88
  }
87
89
  }
88
90
  /**
89
- * Rondevu - Complete WebRTC signaling client
91
+ * Rondevu - Complete WebRTC signaling client with durable connections
90
92
  *
91
- * Provides a unified API for:
92
- * - Implicit username claiming (auto-claimed on first authenticated request)
93
- * - Service publishing with automatic signature generation
94
- * - Service discovery (direct, random, paginated)
95
- * - WebRTC signaling (offer/answer exchange, ICE relay)
96
- * - Keypair management
93
+ * v1.0.0 introduces breaking changes:
94
+ * - connectToService() now returns AnswererConnection instead of ConnectionContext
95
+ * - Automatic reconnection and message buffering built-in
96
+ * - Connection objects expose .send() method instead of raw DataChannel
97
+ * - Rich event system for connection lifecycle (connected, disconnected, reconnecting, etc.)
97
98
  *
98
99
  * @example
99
100
  * ```typescript
@@ -104,39 +105,39 @@ export class ConnectionError extends RondevuError {
104
105
  * iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
105
106
  * })
106
107
  *
107
- * // Or use custom ICE servers
108
- * const rondevu2 = await Rondevu.connect({
109
- * apiUrl: 'https://signal.example.com',
110
- * username: 'bob',
111
- * iceServers: [
112
- * { urls: 'stun:stun.l.google.com:19302' },
113
- * { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' }
114
- * ]
115
- * })
116
- *
117
108
  * // Publish a service with automatic offer management
118
109
  * await rondevu.publishService({
119
110
  * service: 'chat:2.0.0',
120
- * maxOffers: 5, // Maintain up to 5 concurrent offers
121
- * offerFactory: async (pc) => {
122
- * // pc is created by Rondevu with ICE handlers already attached
123
- * const dc = pc.createDataChannel('chat')
124
- * const offer = await pc.createOffer()
125
- * await pc.setLocalDescription(offer)
126
- * return { dc, offer }
127
- * }
111
+ * maxOffers: 5 // Maintain up to 5 concurrent offers
128
112
  * })
129
113
  *
130
114
  * // Start accepting connections (auto-fills offers and polls)
131
115
  * await rondevu.startFilling()
132
116
  *
133
- * // Access active connections
134
- * for (const offer of rondevu.getActiveOffers()) {
135
- * offer.dc?.addEventListener('message', (e) => console.log(e.data))
136
- * }
117
+ * // Listen for connections (v1.0.0 API)
118
+ * rondevu.on('connection:opened', (offerId, connection) => {
119
+ * connection.on('connected', () => console.log('Connected!'))
120
+ * connection.on('message', (data) => console.log('Received:', data))
121
+ * connection.send('Hello!')
122
+ * })
137
123
  *
138
- * // Stop when done
139
- * rondevu.stopFilling()
124
+ * // Connect to a service (v1.0.0 - returns AnswererConnection)
125
+ * const connection = await rondevu.connectToService({
126
+ * serviceFqn: 'chat:2.0.0@bob'
127
+ * })
128
+ *
129
+ * connection.on('connected', () => {
130
+ * console.log('Connected!')
131
+ * connection.send('Hello!')
132
+ * })
133
+ *
134
+ * connection.on('message', (data) => {
135
+ * console.log('Received:', data)
136
+ * })
137
+ *
138
+ * connection.on('reconnecting', (attempt) => {
139
+ * console.log(`Reconnecting, attempt ${attempt}`)
140
+ * })
140
141
  * ```
141
142
  */
142
143
  export class Rondevu extends EventEmitter {
@@ -148,12 +149,12 @@ export class Rondevu extends EventEmitter {
148
149
  this.maxOffers = 0;
149
150
  this.offerFactory = null;
150
151
  this.ttl = Rondevu.DEFAULT_TTL_MS;
151
- this.activeOffers = new Map();
152
+ this.activeConnections = new Map();
152
153
  // Polling
153
154
  this.filling = false;
155
+ this.fillingSemaphore = false; // Semaphore to prevent concurrent fillOffers calls
154
156
  this.pollingInterval = null;
155
157
  this.lastPollTimestamp = 0;
156
- this.isPolling = false; // Guard against concurrent poll execution
157
158
  this.apiUrl = apiUrl;
158
159
  this.username = username;
159
160
  this.keypair = keypair;
@@ -284,49 +285,27 @@ export class Rondevu extends EventEmitter {
284
285
  * ```typescript
285
286
  * await rondevu.publishService({
286
287
  * service: 'chat:2.0.0',
287
- * maxOffers: 5
288
+ * maxOffers: 5,
289
+ * connectionConfig: {
290
+ * reconnectEnabled: true,
291
+ * bufferEnabled: true
292
+ * }
288
293
  * })
289
294
  * await rondevu.startFilling()
290
295
  * ```
291
296
  */
292
297
  async publishService(options) {
293
- const { service, maxOffers, offerFactory, ttl } = options;
298
+ const { service, maxOffers, offerFactory, ttl, connectionConfig } = options;
294
299
  this.currentService = service;
295
300
  this.maxOffers = maxOffers;
296
301
  this.offerFactory = offerFactory || this.defaultOfferFactory.bind(this);
297
302
  this.ttl = ttl || Rondevu.DEFAULT_TTL_MS;
303
+ this.connectionConfig = connectionConfig;
298
304
  this.debug(`Publishing service: ${service} with maxOffers: ${maxOffers}`);
299
305
  this.usernameClaimed = true;
300
306
  }
301
307
  /**
302
- * Set up ICE candidate handler to send candidates to the server
303
- *
304
- * Note: This is used by connectToService() where the offerId is already known.
305
- * For createOffer(), we use inline ICE handling with early candidate queuing
306
- * since the offerId isn't available until after the factory completes.
307
- */
308
- setupIceCandidateHandler(pc, serviceFqn, offerId) {
309
- pc.onicecandidate = async (event) => {
310
- if (event.candidate) {
311
- try {
312
- // Handle both browser and Node.js (wrtc) environments
313
- // Browser: candidate.toJSON() exists
314
- // Node.js wrtc: candidate is already a plain object
315
- const candidateData = typeof event.candidate.toJSON === 'function'
316
- ? event.candidate.toJSON()
317
- : event.candidate;
318
- // Emit local ICE candidate event
319
- this.emit('ice:candidate:local', offerId, candidateData);
320
- await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]);
321
- }
322
- catch (err) {
323
- console.error('[Rondevu] Failed to send ICE candidate:', err);
324
- }
325
- }
326
- };
327
- }
328
- /**
329
- * Create a single offer and publish it to the server
308
+ * Create a single offer and publish it to the server using OffererConnection
330
309
  */
331
310
  async createOffer() {
332
311
  if (!this.currentService || !this.offerFactory) {
@@ -338,42 +317,9 @@ export class Rondevu extends EventEmitter {
338
317
  // Auto-append username to service
339
318
  const serviceFqn = `${this.currentService}@${this.username}`;
340
319
  this.debug('Creating new offer...');
341
- // 1. Create the RTCPeerConnection - Rondevu controls this to set up handlers early
320
+ // 1. Create RTCPeerConnection using factory (for now, keep compatibility)
342
321
  const pc = new RTCPeerConnection(rtcConfig);
343
- // 2. Set up ICE candidate handler with queuing BEFORE the factory runs
344
- // This ensures we capture all candidates, even those generated immediately
345
- // when setLocalDescription() is called in the factory
346
- const earlyIceCandidates = [];
347
- let offerId;
348
- pc.onicecandidate = async (event) => {
349
- if (event.candidate) {
350
- // Handle both browser and Node.js (wrtc) environments
351
- const candidateData = typeof event.candidate.toJSON === 'function'
352
- ? event.candidate.toJSON()
353
- : event.candidate;
354
- // Emit local ICE candidate event
355
- if (offerId) {
356
- this.emit('ice:candidate:local', offerId, candidateData);
357
- }
358
- if (offerId) {
359
- // We have the offerId, send directly
360
- try {
361
- await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]);
362
- }
363
- catch (err) {
364
- console.error('[Rondevu] Failed to send ICE candidate:', err);
365
- }
366
- }
367
- else {
368
- // Queue for later - we don't have the offerId yet
369
- this.debug('Queuing early ICE candidate');
370
- earlyIceCandidates.push(candidateData);
371
- }
372
- }
373
- };
374
- // 3. Call the factory with the pc - factory creates data channel and offer
375
- // When factory calls setLocalDescription(), ICE gathering starts and
376
- // candidates are captured by the handler we set up above
322
+ // 2. Call the factory to create offer
377
323
  let dc;
378
324
  let offer;
379
325
  try {
@@ -382,11 +328,10 @@ export class Rondevu extends EventEmitter {
382
328
  offer = factoryResult.offer;
383
329
  }
384
330
  catch (err) {
385
- // Clean up the connection if factory fails
386
331
  pc.close();
387
332
  throw err;
388
333
  }
389
- // 4. Publish to server to get offerId
334
+ // 3. Publish to server to get offerId
390
335
  const result = await this.api.publishService({
391
336
  serviceFqn,
392
337
  offers: [{ sdp: offer.sdp }],
@@ -394,62 +339,68 @@ export class Rondevu extends EventEmitter {
394
339
  signature: '',
395
340
  message: '',
396
341
  });
397
- offerId = result.offers[0].offerId;
398
- // 5. Store active offer
399
- this.activeOffers.set(offerId, {
400
- offerId,
342
+ const offerId = result.offers[0].offerId;
343
+ // 4. Create OffererConnection instance
344
+ const connection = new OffererConnection({
345
+ api: this.api,
401
346
  serviceFqn,
402
- pc,
403
- dc,
404
- answered: false,
405
- createdAt: Date.now()
347
+ offerId,
348
+ rtcConfig,
349
+ config: {
350
+ ...this.connectionConfig,
351
+ debug: this.debugEnabled,
352
+ },
353
+ });
354
+ // Setup connection event handlers
355
+ connection.on('connected', () => {
356
+ this.debug(`Connection established for offer ${offerId}`);
357
+ this.emit('connection:opened', offerId, connection);
358
+ });
359
+ connection.on('failed', (error) => {
360
+ this.debug(`Connection failed for offer ${offerId}:`, error);
361
+ this.activeConnections.delete(offerId);
362
+ this.fillOffers(); // Replace failed offer
406
363
  });
364
+ connection.on('closed', () => {
365
+ this.debug(`Connection closed for offer ${offerId}`);
366
+ this.activeConnections.delete(offerId);
367
+ this.fillOffers(); // Replace closed offer
368
+ });
369
+ // Store active connection
370
+ this.activeConnections.set(offerId, connection);
371
+ // Initialize the connection
372
+ await connection.initialize();
407
373
  this.debug(`Offer created: ${offerId}`);
408
374
  this.emit('offer:created', offerId, serviceFqn);
409
- // Set up data channel open handler (offerer side)
410
- if (dc) {
411
- dc.onopen = () => {
412
- this.debug(`Data channel opened for offer ${offerId}`);
413
- this.emit('connection:opened', offerId, dc);
414
- };
415
- }
416
- // 6. Send any queued early ICE candidates
417
- if (earlyIceCandidates.length > 0) {
418
- this.debug(`Sending ${earlyIceCandidates.length} early ICE candidates`);
419
- try {
420
- await this.api.addOfferIceCandidates(serviceFqn, offerId, earlyIceCandidates);
421
- }
422
- catch (err) {
423
- console.error('[Rondevu] Failed to send early ICE candidates:', err);
424
- }
425
- }
426
- // 7. Monitor connection state
427
- pc.onconnectionstatechange = () => {
428
- this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`);
429
- if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
430
- this.emit('connection:closed', offerId);
431
- this.activeOffers.delete(offerId);
432
- this.fillOffers(); // Try to replace failed offer
433
- }
434
- };
435
375
  }
436
376
  /**
437
- * Fill offers to reach maxOffers count
377
+ * Fill offers to reach maxOffers count with semaphore protection
438
378
  */
439
379
  async fillOffers() {
440
380
  if (!this.filling || !this.currentService)
441
381
  return;
442
- const currentCount = this.activeOffers.size;
443
- const needed = this.maxOffers - currentCount;
444
- this.debug(`Filling offers: current=${currentCount}, needed=${needed}`);
445
- for (let i = 0; i < needed; i++) {
446
- try {
447
- await this.createOffer();
448
- }
449
- catch (err) {
450
- console.error('[Rondevu] Failed to create offer:', err);
382
+ // Semaphore to prevent concurrent fills
383
+ if (this.fillingSemaphore) {
384
+ this.debug('fillOffers already in progress, skipping');
385
+ return;
386
+ }
387
+ this.fillingSemaphore = true;
388
+ try {
389
+ const currentCount = this.activeConnections.size;
390
+ const needed = this.maxOffers - currentCount;
391
+ this.debug(`Filling offers: current=${currentCount}, needed=${needed}`);
392
+ for (let i = 0; i < needed; i++) {
393
+ try {
394
+ await this.createOffer();
395
+ }
396
+ catch (err) {
397
+ console.error('[Rondevu] Failed to create offer:', err);
398
+ }
451
399
  }
452
400
  }
401
+ finally {
402
+ this.fillingSemaphore = false;
403
+ }
453
404
  }
454
405
  /**
455
406
  * Poll for answers and ICE candidates (internal use for automatic offer management)
@@ -457,41 +408,20 @@ export class Rondevu extends EventEmitter {
457
408
  async pollInternal() {
458
409
  if (!this.filling)
459
410
  return;
460
- // Prevent concurrent poll execution to avoid duplicate answer processing
461
- if (this.isPolling) {
462
- this.debug('Poll already in progress, skipping');
463
- return;
464
- }
465
- this.isPolling = true;
466
411
  try {
467
412
  const result = await this.api.poll(this.lastPollTimestamp);
468
- // Process answers
413
+ // Process answers - delegate to OffererConnections
469
414
  for (const answer of result.answers) {
470
- const activeOffer = this.activeOffers.get(answer.offerId);
471
- if (activeOffer && !activeOffer.answered) {
472
- this.debug(`Received answer for offer ${answer.offerId}`);
473
- await activeOffer.pc.setRemoteDescription({
474
- type: 'answer',
475
- sdp: answer.sdp
476
- });
477
- activeOffer.answered = true;
478
- this.lastPollTimestamp = answer.answeredAt;
479
- this.emit('offer:answered', answer.offerId, answer.answererId);
480
- // Create replacement offer
481
- this.fillOffers();
482
- }
483
- }
484
- // Process ICE candidates
485
- for (const [offerId, candidates] of Object.entries(result.iceCandidates)) {
486
- const activeOffer = this.activeOffers.get(offerId);
487
- if (activeOffer) {
488
- const answererCandidates = candidates.filter(c => c.role === 'answerer');
489
- for (const item of answererCandidates) {
490
- if (item.candidate) {
491
- this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
492
- await activeOffer.pc.addIceCandidate(new RTCIceCandidate(item.candidate));
493
- this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt);
494
- }
415
+ const connection = this.activeConnections.get(answer.offerId);
416
+ if (connection) {
417
+ try {
418
+ await connection.processAnswer(answer.sdp, answer.answererId);
419
+ this.lastPollTimestamp = Math.max(this.lastPollTimestamp, answer.answeredAt);
420
+ // Create replacement offer
421
+ this.fillOffers();
422
+ }
423
+ catch (err) {
424
+ this.debug(`Failed to process answer for offer ${answer.offerId}:`, err);
495
425
  }
496
426
  }
497
427
  }
@@ -499,9 +429,6 @@ export class Rondevu extends EventEmitter {
499
429
  catch (err) {
500
430
  console.error('[Rondevu] Polling error:', err);
501
431
  }
502
- finally {
503
- this.isPolling = false;
504
- }
505
432
  }
506
433
  /**
507
434
  * Start filling offers and polling for answers/ICE
@@ -531,35 +458,34 @@ export class Rondevu extends EventEmitter {
531
458
  stopFilling() {
532
459
  this.debug('Stopping offer filling and polling');
533
460
  this.filling = false;
534
- this.isPolling = false; // Reset polling guard
461
+ this.fillingSemaphore = false;
535
462
  // Stop polling
536
463
  if (this.pollingInterval) {
537
464
  clearInterval(this.pollingInterval);
538
465
  this.pollingInterval = null;
539
466
  }
540
467
  // Close all active connections
541
- for (const [offerId, offer] of this.activeOffers.entries()) {
542
- this.debug(`Closing offer ${offerId}`);
543
- offer.dc?.close();
544
- offer.pc.close();
468
+ for (const [offerId, connection] of this.activeConnections.entries()) {
469
+ this.debug(`Closing connection ${offerId}`);
470
+ connection.close();
545
471
  }
546
- this.activeOffers.clear();
472
+ this.activeConnections.clear();
547
473
  }
548
474
  /**
549
475
  * Get the count of active offers
550
476
  * @returns Number of active offers
551
477
  */
552
478
  getOfferCount() {
553
- return this.activeOffers.size;
479
+ return this.activeConnections.size;
554
480
  }
555
481
  /**
556
482
  * Check if an offer is currently connected
557
483
  * @param offerId - The offer ID to check
558
- * @returns True if the offer exists and has been answered
484
+ * @returns True if the offer exists and is connected
559
485
  */
560
486
  isConnected(offerId) {
561
- const offer = this.activeOffers.get(offerId);
562
- return offer ? offer.answered : false;
487
+ const connection = this.activeConnections.get(offerId);
488
+ return connection ? connection.getState() === 'connected' : false;
563
489
  }
564
490
  /**
565
491
  * Disconnect all active offers
@@ -567,12 +493,11 @@ export class Rondevu extends EventEmitter {
567
493
  */
568
494
  async disconnectAll() {
569
495
  this.debug('Disconnecting all offers');
570
- for (const [offerId, offer] of this.activeOffers.entries()) {
571
- this.debug(`Closing offer ${offerId}`);
572
- offer.dc?.close();
573
- offer.pc.close();
496
+ for (const [offerId, connection] of this.activeConnections.entries()) {
497
+ this.debug(`Closing connection ${offerId}`);
498
+ connection.close();
574
499
  }
575
- this.activeOffers.clear();
500
+ this.activeConnections.clear();
576
501
  }
577
502
  /**
578
503
  * Get the current service status
@@ -581,7 +506,7 @@ export class Rondevu extends EventEmitter {
581
506
  getServiceStatus() {
582
507
  return {
583
508
  active: this.currentService !== null,
584
- offerCount: this.activeOffers.size,
509
+ offerCount: this.activeConnections.size,
585
510
  maxOffers: this.maxOffers,
586
511
  filling: this.filling
587
512
  };
@@ -609,54 +534,43 @@ export class Rondevu extends EventEmitter {
609
534
  }
610
535
  }
611
536
  /**
612
- * Start polling for remote ICE candidates
613
- * Returns the polling interval ID
614
- */
615
- startIcePolling(pc, serviceFqn, offerId) {
616
- let lastIceTimestamp = 0;
617
- return setInterval(async () => {
618
- try {
619
- const result = await this.api.getOfferIceCandidates(serviceFqn, offerId, lastIceTimestamp);
620
- for (const item of result.candidates) {
621
- if (item.candidate) {
622
- this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
623
- await pc.addIceCandidate(new RTCIceCandidate(item.candidate));
624
- lastIceTimestamp = item.createdAt;
625
- }
626
- }
627
- }
628
- catch (err) {
629
- console.error('[Rondevu] Failed to poll ICE candidates:', err);
630
- }
631
- }, Rondevu.POLLING_INTERVAL_MS);
632
- }
633
- /**
634
- * Automatically connect to a service (answerer side)
635
- * Handles the entire connection flow: discovery, WebRTC setup, answer exchange, ICE candidates
537
+ * Connect to a service (answerer side) - v1.0.0 API
538
+ * Returns an AnswererConnection with automatic reconnection and buffering
539
+ *
540
+ * BREAKING CHANGE: This now returns AnswererConnection instead of ConnectionContext
636
541
  *
637
542
  * @example
638
543
  * ```typescript
639
544
  * // Connect to specific user
640
545
  * const connection = await rondevu.connectToService({
641
546
  * serviceFqn: 'chat:2.0.0@alice',
642
- * onConnection: ({ dc, peerUsername }) => {
643
- * console.log('Connected to', peerUsername)
644
- * dc.addEventListener('message', (e) => console.log(e.data))
645
- * dc.addEventListener('open', () => dc.send('Hello!'))
547
+ * connectionConfig: {
548
+ * reconnectEnabled: true,
549
+ * bufferEnabled: true
646
550
  * }
647
551
  * })
648
552
  *
553
+ * connection.on('connected', () => {
554
+ * console.log('Connected!')
555
+ * connection.send('Hello!')
556
+ * })
557
+ *
558
+ * connection.on('message', (data) => {
559
+ * console.log('Received:', data)
560
+ * })
561
+ *
562
+ * connection.on('reconnecting', (attempt) => {
563
+ * console.log(`Reconnecting, attempt ${attempt}`)
564
+ * })
565
+ *
649
566
  * // Discover random service
650
567
  * const connection = await rondevu.connectToService({
651
- * service: 'chat:2.0.0',
652
- * onConnection: ({ dc, peerUsername }) => {
653
- * console.log('Connected to', peerUsername)
654
- * }
568
+ * service: 'chat:2.0.0'
655
569
  * })
656
570
  * ```
657
571
  */
658
572
  async connectToService(options) {
659
- const { onConnection, rtcConfig } = options;
573
+ const { rtcConfig, connectionConfig } = options;
660
574
  // Validate inputs
661
575
  if (options.serviceFqn !== undefined && typeof options.serviceFqn === 'string' && !options.serviceFqn.trim()) {
662
576
  throw new Error('serviceFqn cannot be empty');
@@ -670,73 +584,28 @@ export class Rondevu extends EventEmitter {
670
584
  // Determine the full service FQN
671
585
  const fqn = await this.resolveServiceFqn(options);
672
586
  this.debug(`Connecting to service: ${fqn}`);
673
- // 1. Get service offer
587
+ // Get service offer
674
588
  const serviceData = await this.api.getService(fqn);
675
589
  this.debug(`Found service from @${serviceData.username}`);
676
- // 2. Create RTCPeerConnection
590
+ // Create RTCConfiguration
677
591
  const rtcConfiguration = rtcConfig || {
678
592
  iceServers: this.iceServers
679
593
  };
680
- const pc = new RTCPeerConnection(rtcConfiguration);
681
- // 3. Set up data channel handler (answerer receives it from offerer)
682
- let dc = null;
683
- const dataChannelPromise = new Promise((resolve) => {
684
- pc.ondatachannel = (event) => {
685
- this.debug('Data channel received from offerer');
686
- dc = event.channel;
687
- this.emit('connection:opened', serviceData.offerId, dc);
688
- resolve(dc);
689
- };
690
- });
691
- // 4. Set up ICE candidate exchange
692
- this.setupIceCandidateHandler(pc, serviceData.serviceFqn, serviceData.offerId);
693
- // 5. Poll for remote ICE candidates
694
- const icePollInterval = this.startIcePolling(pc, serviceData.serviceFqn, serviceData.offerId);
695
- // 6. Set remote description
696
- await pc.setRemoteDescription({
697
- type: 'offer',
698
- sdp: serviceData.sdp
699
- });
700
- // 7. Create and send answer
701
- const answer = await pc.createAnswer();
702
- await pc.setLocalDescription(answer);
703
- await this.api.answerOffer(serviceData.serviceFqn, serviceData.offerId, answer.sdp);
704
- // 8. Wait for data channel to be established
705
- dc = await dataChannelPromise;
706
- // Create connection context
707
- const context = {
708
- pc,
709
- dc,
594
+ // Create AnswererConnection
595
+ const connection = new AnswererConnection({
596
+ api: this.api,
710
597
  serviceFqn: serviceData.serviceFqn,
711
598
  offerId: serviceData.offerId,
712
- peerUsername: serviceData.username
713
- };
714
- // 9. Set up connection state monitoring
715
- pc.onconnectionstatechange = () => {
716
- this.debug(`Connection state: ${pc.connectionState}`);
717
- if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
718
- clearInterval(icePollInterval);
719
- }
720
- };
721
- // 10. Wait for data channel to open and call onConnection
722
- if (dc.readyState === 'open') {
723
- this.debug('Data channel already open');
724
- if (onConnection) {
725
- await onConnection(context);
726
- }
727
- }
728
- else {
729
- await new Promise((resolve) => {
730
- dc.addEventListener('open', async () => {
731
- this.debug('Data channel opened');
732
- if (onConnection) {
733
- await onConnection(context);
734
- }
735
- resolve();
736
- });
737
- });
738
- }
739
- return context;
599
+ offerSdp: serviceData.sdp,
600
+ rtcConfig: rtcConfiguration,
601
+ config: {
602
+ ...connectionConfig,
603
+ debug: this.debugEnabled,
604
+ },
605
+ });
606
+ // Initialize the connection
607
+ await connection.initialize();
608
+ return connection;
740
609
  }
741
610
  // ============================================
742
611
  // Service Discovery
@@ -744,8 +613,6 @@ export class Rondevu extends EventEmitter {
744
613
  /**
745
614
  * Find a service - unified discovery method
746
615
  *
747
- * Replaces getService(), discoverService(), and discoverServices() with a single method.
748
- *
749
616
  * @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
750
617
  * @param options - Discovery options
751
618
  *
@@ -834,6 +701,34 @@ export class Rondevu extends EventEmitter {
834
701
  getPublicKey() {
835
702
  return this.keypair.publicKey;
836
703
  }
704
+ /**
705
+ * Get active connections (for offerer side)
706
+ */
707
+ getActiveConnections() {
708
+ return this.activeConnections;
709
+ }
710
+ /**
711
+ * Get all active offers (legacy compatibility)
712
+ * @deprecated Use getActiveConnections() instead
713
+ */
714
+ getActiveOffers() {
715
+ const offers = [];
716
+ for (const [offerId, connection] of this.activeConnections.entries()) {
717
+ const pc = connection.getPeerConnection();
718
+ const dc = connection.getDataChannel();
719
+ if (pc) {
720
+ offers.push({
721
+ offerId,
722
+ serviceFqn: this.currentService ? `${this.currentService}@${this.username}` : '',
723
+ pc,
724
+ dc: dc || undefined,
725
+ answered: connection.getState() === 'connected',
726
+ createdAt: Date.now(),
727
+ });
728
+ }
729
+ }
730
+ return offers;
731
+ }
837
732
  /**
838
733
  * Access to underlying API for advanced operations
839
734
  * @deprecated Use direct methods on Rondevu instance instead