@xtr-dev/rondevu-client 0.8.2 → 0.9.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.
@@ -0,0 +1,264 @@
1
+ /**
2
+ * DurableService - Service with automatic TTL refresh
3
+ *
4
+ * Manages service publishing with automatic reconnection for incoming
5
+ * connections and TTL auto-refresh to prevent expiration.
6
+ */
7
+ import { EventEmitter } from '../event-emitter.js';
8
+ import { ServicePool } from '../service-pool.js';
9
+ import { DurableChannel } from './channel.js';
10
+ /**
11
+ * Default configuration for durable services
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ isPublic: false,
15
+ ttlRefreshMargin: 0.2,
16
+ poolSize: 1,
17
+ pollingInterval: 2000,
18
+ maxReconnectAttempts: 10,
19
+ reconnectBackoffBase: 1000,
20
+ reconnectBackoffMax: 30000,
21
+ reconnectJitter: 0.2,
22
+ connectionTimeout: 30000,
23
+ maxQueueSize: 1000,
24
+ maxMessageAge: 60000,
25
+ rtcConfig: {
26
+ iceServers: [
27
+ { urls: 'stun:stun.l.google.com:19302' },
28
+ { urls: 'stun:stun1.l.google.com:19302' }
29
+ ]
30
+ }
31
+ };
32
+ /**
33
+ * Durable service that automatically refreshes TTL and handles reconnections
34
+ *
35
+ * The DurableService manages service publishing and provides:
36
+ * - Automatic TTL refresh before expiration
37
+ * - Durable connections for incoming peers
38
+ * - Connection pooling for multiple simultaneous connections
39
+ * - High-level connection lifecycle events
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const service = new DurableService(
44
+ * offersApi,
45
+ * (channel, connectionId) => {
46
+ * channel.on('message', (data) => {
47
+ * console.log(`Message from ${connectionId}:`, data);
48
+ * channel.send(`Echo: ${data}`);
49
+ * });
50
+ * },
51
+ * {
52
+ * username: 'alice',
53
+ * privateKey: keypair.privateKey,
54
+ * serviceFqn: 'chat@1.0.0',
55
+ * poolSize: 10
56
+ * }
57
+ * );
58
+ *
59
+ * service.on('published', (serviceId, uuid) => {
60
+ * console.log(`Service published: ${uuid}`);
61
+ * });
62
+ *
63
+ * service.on('connection', (connectionId) => {
64
+ * console.log(`New connection: ${connectionId}`);
65
+ * });
66
+ *
67
+ * await service.start();
68
+ * ```
69
+ */
70
+ export class DurableService extends EventEmitter {
71
+ constructor(offersApi, baseUrl, credentials, handler, config) {
72
+ super();
73
+ this.offersApi = offersApi;
74
+ this.baseUrl = baseUrl;
75
+ this.credentials = credentials;
76
+ this.handler = handler;
77
+ this.activeChannels = new Map();
78
+ this.config = { ...DEFAULT_CONFIG, ...config };
79
+ }
80
+ /**
81
+ * Start the service
82
+ *
83
+ * Publishes the service and begins accepting connections.
84
+ *
85
+ * @returns Service information
86
+ */
87
+ async start() {
88
+ if (this.servicePool) {
89
+ throw new Error('Service already started');
90
+ }
91
+ // Create and start service pool
92
+ this.servicePool = new ServicePool(this.baseUrl, this.credentials, {
93
+ username: this.config.username,
94
+ privateKey: this.config.privateKey,
95
+ serviceFqn: this.config.serviceFqn,
96
+ rtcConfig: this.config.rtcConfig,
97
+ isPublic: this.config.isPublic,
98
+ metadata: this.config.metadata,
99
+ ttl: this.config.ttl,
100
+ poolSize: this.config.poolSize,
101
+ pollingInterval: this.config.pollingInterval,
102
+ handler: (channel, peer, connectionId) => {
103
+ this.handleNewConnection(channel, connectionId);
104
+ },
105
+ onPoolStatus: (status) => {
106
+ // Could emit pool status event if needed
107
+ },
108
+ onError: (error, context) => {
109
+ this.emit('error', error, context);
110
+ }
111
+ });
112
+ const handle = await this.servicePool.start();
113
+ // Store service info
114
+ this.serviceId = handle.serviceId;
115
+ this.uuid = handle.uuid;
116
+ this.expiresAt = Date.now() + (this.config.ttl || 300000); // Default 5 minutes
117
+ this.emit('published', this.serviceId, this.uuid);
118
+ // Schedule TTL refresh
119
+ this.scheduleRefresh();
120
+ return {
121
+ serviceId: this.serviceId,
122
+ uuid: this.uuid,
123
+ expiresAt: this.expiresAt
124
+ };
125
+ }
126
+ /**
127
+ * Stop the service
128
+ *
129
+ * Unpublishes the service and closes all active connections.
130
+ */
131
+ async stop() {
132
+ // Cancel TTL refresh
133
+ if (this.ttlRefreshTimer) {
134
+ clearTimeout(this.ttlRefreshTimer);
135
+ this.ttlRefreshTimer = undefined;
136
+ }
137
+ // Close all active channels
138
+ for (const channel of this.activeChannels.values()) {
139
+ channel.close();
140
+ }
141
+ this.activeChannels.clear();
142
+ // Stop service pool
143
+ if (this.servicePool) {
144
+ await this.servicePool.stop();
145
+ this.servicePool = undefined;
146
+ }
147
+ this.emit('closed');
148
+ }
149
+ /**
150
+ * Get list of active connection IDs
151
+ */
152
+ getActiveConnections() {
153
+ return Array.from(this.activeChannels.keys());
154
+ }
155
+ /**
156
+ * Get service information
157
+ */
158
+ getServiceInfo() {
159
+ if (!this.serviceId || !this.uuid || !this.expiresAt) {
160
+ return null;
161
+ }
162
+ return {
163
+ serviceId: this.serviceId,
164
+ uuid: this.uuid,
165
+ expiresAt: this.expiresAt
166
+ };
167
+ }
168
+ /**
169
+ * Schedule TTL refresh
170
+ */
171
+ scheduleRefresh() {
172
+ if (!this.expiresAt || !this.config.ttl) {
173
+ return;
174
+ }
175
+ // Cancel existing timer
176
+ if (this.ttlRefreshTimer) {
177
+ clearTimeout(this.ttlRefreshTimer);
178
+ }
179
+ // Calculate refresh time (default: refresh at 80% of TTL)
180
+ const timeUntilExpiry = this.expiresAt - Date.now();
181
+ const refreshMargin = timeUntilExpiry * this.config.ttlRefreshMargin;
182
+ const refreshTime = Math.max(0, timeUntilExpiry - refreshMargin);
183
+ // Schedule refresh
184
+ this.ttlRefreshTimer = setTimeout(() => {
185
+ this.refreshServiceTTL().catch(error => {
186
+ this.emit('error', error, 'ttl-refresh');
187
+ // Retry after short delay
188
+ setTimeout(() => this.scheduleRefresh(), 5000);
189
+ });
190
+ }, refreshTime);
191
+ }
192
+ /**
193
+ * Refresh service TTL
194
+ */
195
+ async refreshServiceTTL() {
196
+ if (!this.serviceId || !this.uuid) {
197
+ return;
198
+ }
199
+ // Delete old service
200
+ await this.servicePool?.stop();
201
+ // Recreate service pool (this republishes the service)
202
+ this.servicePool = new ServicePool(this.baseUrl, this.credentials, {
203
+ username: this.config.username,
204
+ privateKey: this.config.privateKey,
205
+ serviceFqn: this.config.serviceFqn,
206
+ rtcConfig: this.config.rtcConfig,
207
+ isPublic: this.config.isPublic,
208
+ metadata: this.config.metadata,
209
+ ttl: this.config.ttl,
210
+ poolSize: this.config.poolSize,
211
+ pollingInterval: this.config.pollingInterval,
212
+ handler: (channel, peer, connectionId) => {
213
+ this.handleNewConnection(channel, connectionId);
214
+ },
215
+ onPoolStatus: (status) => {
216
+ // Could emit pool status event if needed
217
+ },
218
+ onError: (error, context) => {
219
+ this.emit('error', error, context);
220
+ }
221
+ });
222
+ const handle = await this.servicePool.start();
223
+ // Update service info
224
+ this.serviceId = handle.serviceId;
225
+ this.uuid = handle.uuid;
226
+ this.expiresAt = Date.now() + (this.config.ttl || 300000);
227
+ this.emit('ttl-refreshed', this.expiresAt);
228
+ // Schedule next refresh
229
+ this.scheduleRefresh();
230
+ }
231
+ /**
232
+ * Handle new incoming connection
233
+ */
234
+ handleNewConnection(channel, connectionId) {
235
+ // Create durable channel
236
+ const durableChannel = new DurableChannel(channel.label, {
237
+ maxQueueSize: this.config.maxQueueSize,
238
+ maxMessageAge: this.config.maxMessageAge
239
+ });
240
+ // Attach to underlying channel
241
+ durableChannel.attachToChannel(channel);
242
+ // Track channel
243
+ this.activeChannels.set(connectionId, durableChannel);
244
+ // Setup cleanup on close
245
+ durableChannel.on('close', () => {
246
+ this.activeChannels.delete(connectionId);
247
+ this.emit('disconnection', connectionId);
248
+ });
249
+ // Emit connection event
250
+ this.emit('connection', connectionId);
251
+ // Invoke user handler
252
+ try {
253
+ const result = this.handler(durableChannel, connectionId);
254
+ if (result && typeof result.then === 'function') {
255
+ result.catch(error => {
256
+ this.emit('error', error, 'handler');
257
+ });
258
+ }
259
+ }
260
+ catch (error) {
261
+ this.emit('error', error, 'handler');
262
+ }
263
+ }
264
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Type definitions for durable WebRTC connections
3
+ *
4
+ * This module defines all interfaces, enums, and types used by the durable
5
+ * connection system for automatic reconnection and message queuing.
6
+ */
7
+ /**
8
+ * Connection state enum
9
+ */
10
+ export declare enum DurableConnectionState {
11
+ CONNECTING = "connecting",
12
+ CONNECTED = "connected",
13
+ RECONNECTING = "reconnecting",
14
+ DISCONNECTED = "disconnected",
15
+ FAILED = "failed",
16
+ CLOSED = "closed"
17
+ }
18
+ /**
19
+ * Channel state enum
20
+ */
21
+ export declare enum DurableChannelState {
22
+ CONNECTING = "connecting",
23
+ OPEN = "open",
24
+ CLOSING = "closing",
25
+ CLOSED = "closed"
26
+ }
27
+ /**
28
+ * Configuration for durable connections
29
+ */
30
+ export interface DurableConnectionConfig {
31
+ /** Maximum number of reconnection attempts (default: 10) */
32
+ maxReconnectAttempts?: number;
33
+ /** Base delay for exponential backoff in milliseconds (default: 1000) */
34
+ reconnectBackoffBase?: number;
35
+ /** Maximum delay between reconnection attempts in milliseconds (default: 30000) */
36
+ reconnectBackoffMax?: number;
37
+ /** Jitter factor for randomizing reconnection delays (default: 0.2 = ±20%) */
38
+ reconnectJitter?: number;
39
+ /** Timeout for initial connection attempt in milliseconds (default: 30000) */
40
+ connectionTimeout?: number;
41
+ /** Maximum number of messages to queue during disconnection (default: 1000) */
42
+ maxQueueSize?: number;
43
+ /** Maximum age of queued messages in milliseconds (default: 60000) */
44
+ maxMessageAge?: number;
45
+ /** WebRTC configuration */
46
+ rtcConfig?: RTCConfiguration;
47
+ }
48
+ /**
49
+ * Configuration for durable channels
50
+ */
51
+ export interface DurableChannelConfig {
52
+ /** Maximum number of messages to queue (default: 1000) */
53
+ maxQueueSize?: number;
54
+ /** Maximum age of queued messages in milliseconds (default: 60000) */
55
+ maxMessageAge?: number;
56
+ /** Whether messages should be delivered in order (default: true) */
57
+ ordered?: boolean;
58
+ /** Maximum retransmits for unordered channels (default: undefined) */
59
+ maxRetransmits?: number;
60
+ }
61
+ /**
62
+ * Configuration for durable services
63
+ */
64
+ export interface DurableServiceConfig extends DurableConnectionConfig {
65
+ /** Username that owns the service */
66
+ username: string;
67
+ /** Private key for signing service operations */
68
+ privateKey: string;
69
+ /** Fully qualified service name (e.g., com.example.chat@1.0.0) */
70
+ serviceFqn: string;
71
+ /** Whether the service is publicly discoverable (default: false) */
72
+ isPublic?: boolean;
73
+ /** Optional metadata for the service */
74
+ metadata?: Record<string, any>;
75
+ /** Time-to-live for service in milliseconds (default: server default) */
76
+ ttl?: number;
77
+ /** Margin before TTL expiry to trigger refresh (default: 0.2 = refresh at 80%) */
78
+ ttlRefreshMargin?: number;
79
+ /** Number of simultaneous open offers to maintain (default: 1) */
80
+ poolSize?: number;
81
+ /** Polling interval for checking answers in milliseconds (default: 2000) */
82
+ pollingInterval?: number;
83
+ }
84
+ /**
85
+ * Queued message structure
86
+ */
87
+ export interface QueuedMessage {
88
+ /** Message data */
89
+ data: string | Blob | ArrayBuffer | ArrayBufferView;
90
+ /** Timestamp when message was enqueued */
91
+ enqueuedAt: number;
92
+ /** Unique message ID */
93
+ id: string;
94
+ }
95
+ /**
96
+ * Event type map for DurableConnection
97
+ */
98
+ export interface DurableConnectionEvents extends Record<string, (...args: any[]) => void> {
99
+ 'state': (state: DurableConnectionState, previousState: DurableConnectionState) => void;
100
+ 'connected': () => void;
101
+ 'reconnecting': (attempt: number, maxAttempts: number, nextRetryIn: number) => void;
102
+ 'disconnected': () => void;
103
+ 'failed': (error: Error, permanent: boolean) => void;
104
+ 'closed': () => void;
105
+ }
106
+ /**
107
+ * Event type map for DurableChannel
108
+ */
109
+ export interface DurableChannelEvents extends Record<string, (...args: any[]) => void> {
110
+ 'open': () => void;
111
+ 'message': (data: any) => void;
112
+ 'error': (error: Error) => void;
113
+ 'close': () => void;
114
+ 'bufferedAmountLow': () => void;
115
+ 'queueOverflow': (droppedCount: number) => void;
116
+ }
117
+ /**
118
+ * Event type map for DurableService
119
+ */
120
+ export interface DurableServiceEvents extends Record<string, (...args: any[]) => void> {
121
+ 'published': (serviceId: string, uuid: string) => void;
122
+ 'connection': (connectionId: string) => void;
123
+ 'disconnection': (connectionId: string) => void;
124
+ 'ttl-refreshed': (expiresAt: number) => void;
125
+ 'error': (error: Error, context: string) => void;
126
+ 'closed': () => void;
127
+ }
128
+ /**
129
+ * Information about a durable connection
130
+ */
131
+ export interface ConnectionInfo {
132
+ /** Username (for username-based connections) */
133
+ username?: string;
134
+ /** Service FQN (for service-based connections) */
135
+ serviceFqn?: string;
136
+ /** UUID (for UUID-based connections) */
137
+ uuid?: string;
138
+ }
139
+ /**
140
+ * Service information returned when service is published
141
+ */
142
+ export interface ServiceInfo {
143
+ /** Service ID */
144
+ serviceId: string;
145
+ /** Service UUID for discovery */
146
+ uuid: string;
147
+ /** Expiration timestamp */
148
+ expiresAt: number;
149
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Type definitions for durable WebRTC connections
3
+ *
4
+ * This module defines all interfaces, enums, and types used by the durable
5
+ * connection system for automatic reconnection and message queuing.
6
+ */
7
+ /**
8
+ * Connection state enum
9
+ */
10
+ export var DurableConnectionState;
11
+ (function (DurableConnectionState) {
12
+ DurableConnectionState["CONNECTING"] = "connecting";
13
+ DurableConnectionState["CONNECTED"] = "connected";
14
+ DurableConnectionState["RECONNECTING"] = "reconnecting";
15
+ DurableConnectionState["DISCONNECTED"] = "disconnected";
16
+ DurableConnectionState["FAILED"] = "failed";
17
+ DurableConnectionState["CLOSED"] = "closed";
18
+ })(DurableConnectionState || (DurableConnectionState = {}));
19
+ /**
20
+ * Channel state enum
21
+ */
22
+ export var DurableChannelState;
23
+ (function (DurableChannelState) {
24
+ DurableChannelState["CONNECTING"] = "connecting";
25
+ DurableChannelState["OPEN"] = "open";
26
+ DurableChannelState["CLOSING"] = "closing";
27
+ DurableChannelState["CLOSED"] = "closed";
28
+ })(DurableChannelState || (DurableChannelState = {}));
package/dist/index.d.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  /**
2
2
  * @xtr-dev/rondevu-client
3
- * WebRTC peer signaling and discovery client with topic-based discovery
3
+ * WebRTC peer signaling and discovery client with durable connections
4
4
  */
5
5
  export { Rondevu } from './rondevu.js';
6
6
  export type { RondevuOptions } from './rondevu.js';
7
7
  export { RondevuAuth } from './auth.js';
8
8
  export type { Credentials, FetchFunction } from './auth.js';
9
- export { RondevuOffers } from './offers.js';
10
- export type { CreateOfferRequest, Offer, IceCandidate, TopicInfo } from './offers.js';
11
- export { default as RondevuPeer } from './peer/index.js';
12
- export type { PeerOptions, PeerEvents, PeerTimeouts } from './peer/index.js';
13
9
  export { RondevuUsername } from './usernames.js';
14
10
  export type { UsernameClaimResult, UsernameCheckResult } from './usernames.js';
15
- export { RondevuServices } from './services.js';
16
- export type { ServicePublishResult, PublishServiceOptions, ServiceHandle } from './services.js';
17
- export { RondevuDiscovery } from './discovery.js';
18
- export type { ServiceInfo, ServiceListResult, ServiceQueryResult, ServiceDetails, ConnectResult } from './discovery.js';
19
- export type { PoolStatus, PooledServiceHandle } from './service-pool.js';
11
+ export { DurableConnection } from './durable/connection.js';
12
+ export { DurableChannel } from './durable/channel.js';
13
+ export { DurableService } from './durable/service.js';
14
+ export type { DurableConnectionState, DurableChannelState, DurableConnectionConfig, DurableChannelConfig, DurableServiceConfig, QueuedMessage, DurableConnectionEvents, DurableChannelEvents, DurableServiceEvents, ConnectionInfo, ServiceInfo } from './durable/types.js';
package/dist/index.js CHANGED
@@ -1,18 +1,14 @@
1
1
  /**
2
2
  * @xtr-dev/rondevu-client
3
- * WebRTC peer signaling and discovery client with topic-based discovery
3
+ * WebRTC peer signaling and discovery client with durable connections
4
4
  */
5
5
  // Export main client class
6
6
  export { Rondevu } from './rondevu.js';
7
7
  // Export authentication
8
8
  export { RondevuAuth } from './auth.js';
9
- // Export offers API
10
- export { RondevuOffers } from './offers.js';
11
- // Export peer manager
12
- export { default as RondevuPeer } from './peer/index.js';
13
9
  // Export username API
14
10
  export { RondevuUsername } from './usernames.js';
15
- // Export services API
16
- export { RondevuServices } from './services.js';
17
- // Export discovery API
18
- export { RondevuDiscovery } from './discovery.js';
11
+ // Export durable connection APIs
12
+ export { DurableConnection } from './durable/connection.js';
13
+ export { DurableChannel } from './durable/channel.js';
14
+ export { DurableService } from './durable/service.js';
@@ -6,6 +6,8 @@ export interface AnsweredOffer {
6
6
  offerId: string;
7
7
  answererId: string;
8
8
  sdp: string;
9
+ peerConnection: RTCPeerConnection;
10
+ dataChannel?: RTCDataChannel;
9
11
  answeredAt: number;
10
12
  }
11
13
  /**
@@ -19,7 +21,11 @@ export interface OfferPoolOptions {
19
21
  /** Callback invoked when an offer is answered */
20
22
  onAnswered: (answer: AnsweredOffer) => Promise<void>;
21
23
  /** Callback to create new offers when refilling the pool */
22
- onRefill: (count: number) => Promise<Offer[]>;
24
+ onRefill: (count: number) => Promise<{
25
+ offers: Offer[];
26
+ peerConnections: RTCPeerConnection[];
27
+ dataChannels: RTCDataChannel[];
28
+ }>;
23
29
  /** Error handler for pool operations */
24
30
  onError: (error: Error, context: string) => void;
25
31
  }
@@ -34,15 +40,17 @@ export declare class OfferPool {
34
40
  private offersApi;
35
41
  private options;
36
42
  private offers;
43
+ private peerConnections;
44
+ private dataChannels;
37
45
  private polling;
38
46
  private pollingTimer?;
39
47
  private lastPollTime;
40
48
  private readonly pollingInterval;
41
49
  constructor(offersApi: RondevuOffers, options: OfferPoolOptions);
42
50
  /**
43
- * Add offers to the pool
51
+ * Add offers to the pool with their peer connections and data channels
44
52
  */
45
- addOffers(offers: Offer[]): Promise<void>;
53
+ addOffers(offers: Offer[], peerConnections?: RTCPeerConnection[], dataChannels?: RTCDataChannel[]): Promise<void>;
46
54
  /**
47
55
  * Start polling for answers
48
56
  */
@@ -63,6 +71,10 @@ export declare class OfferPool {
63
71
  * Get all active offer IDs
64
72
  */
65
73
  getActiveOfferIds(): string[];
74
+ /**
75
+ * Get all active peer connections
76
+ */
77
+ getActivePeerConnections(): RTCPeerConnection[];
66
78
  /**
67
79
  * Get the last poll timestamp
68
80
  */
@@ -10,16 +10,25 @@ export class OfferPool {
10
10
  this.offersApi = offersApi;
11
11
  this.options = options;
12
12
  this.offers = new Map();
13
+ this.peerConnections = new Map();
14
+ this.dataChannels = new Map();
13
15
  this.polling = false;
14
16
  this.lastPollTime = 0;
15
17
  this.pollingInterval = options.pollingInterval || 2000;
16
18
  }
17
19
  /**
18
- * Add offers to the pool
20
+ * Add offers to the pool with their peer connections and data channels
19
21
  */
20
- async addOffers(offers) {
21
- for (const offer of offers) {
22
+ async addOffers(offers, peerConnections, dataChannels) {
23
+ for (let i = 0; i < offers.length; i++) {
24
+ const offer = offers[i];
22
25
  this.offers.set(offer.id, offer);
26
+ if (peerConnections && peerConnections[i]) {
27
+ this.peerConnections.set(offer.id, peerConnections[i]);
28
+ }
29
+ if (dataChannels && dataChannels[i]) {
30
+ this.dataChannels.set(offer.id, dataChannels[i]);
31
+ }
23
32
  }
24
33
  }
25
34
  /**
@@ -64,22 +73,33 @@ export class OfferPool {
64
73
  const myAnswers = answers.filter(a => this.offers.has(a.offerId));
65
74
  // Process each answer
66
75
  for (const answer of myAnswers) {
67
- // Notify ServicePool
76
+ // Get the original offer, peer connection, and data channel
77
+ const offer = this.offers.get(answer.offerId);
78
+ const pc = this.peerConnections.get(answer.offerId);
79
+ const channel = this.dataChannels.get(answer.offerId);
80
+ if (!offer || !pc) {
81
+ continue; // Offer or peer connection already consumed, skip
82
+ }
83
+ // Remove from pool BEFORE processing to prevent duplicate processing
84
+ this.offers.delete(answer.offerId);
85
+ this.peerConnections.delete(answer.offerId);
86
+ this.dataChannels.delete(answer.offerId);
87
+ // Notify ServicePool with answer, original peer connection, and data channel
68
88
  await this.options.onAnswered({
69
89
  offerId: answer.offerId,
70
90
  answererId: answer.answererId,
71
91
  sdp: answer.sdp,
92
+ peerConnection: pc,
93
+ dataChannel: channel,
72
94
  answeredAt: answer.answeredAt
73
95
  });
74
- // Remove consumed offer from pool
75
- this.offers.delete(answer.offerId);
76
96
  }
77
97
  // Immediate refill if below pool size
78
98
  if (this.offers.size < this.options.poolSize) {
79
99
  const needed = this.options.poolSize - this.offers.size;
80
100
  try {
81
- const newOffers = await this.options.onRefill(needed);
82
- await this.addOffers(newOffers);
101
+ const result = await this.options.onRefill(needed);
102
+ await this.addOffers(result.offers, result.peerConnections, result.dataChannels);
83
103
  }
84
104
  catch (refillError) {
85
105
  this.options.onError(refillError, 'refill');
@@ -104,6 +124,12 @@ export class OfferPool {
104
124
  getActiveOfferIds() {
105
125
  return Array.from(this.offers.keys());
106
126
  }
127
+ /**
128
+ * Get all active peer connections
129
+ */
130
+ getActivePeerConnections() {
131
+ return Array.from(this.peerConnections.values());
132
+ }
107
133
  /**
108
134
  * Get the last poll timestamp
109
135
  */
@@ -21,14 +21,22 @@ export class ExchangingIceState extends PeerState {
21
21
  this.pollingInterval = setInterval(async () => {
22
22
  try {
23
23
  const candidates = await this.peer.offersApi.getIceCandidates(this.offerId, this.lastIceTimestamp);
24
+ if (candidates.length > 0) {
25
+ console.log(`📥 Received ${candidates.length} remote ICE candidate(s)`);
26
+ }
24
27
  for (const cand of candidates) {
25
28
  if (cand.candidate && cand.candidate.candidate && cand.candidate.candidate !== '') {
29
+ const type = cand.candidate.candidate.includes('typ host') ? 'host' :
30
+ cand.candidate.candidate.includes('typ srflx') ? 'srflx' :
31
+ cand.candidate.candidate.includes('typ relay') ? 'relay' : 'unknown';
32
+ console.log(`🧊 Adding remote ${type} ICE candidate:`, cand.candidate.candidate);
26
33
  try {
27
34
  await this.peer.pc.addIceCandidate(new this.peer.RTCIceCandidate(cand.candidate));
35
+ console.log(`✅ Added remote ${type} ICE candidate`);
28
36
  this.lastIceTimestamp = cand.createdAt;
29
37
  }
30
38
  catch (err) {
31
- console.warn('Failed to add ICE candidate:', err);
39
+ console.warn(`⚠️ Failed to add remote ${type} ICE candidate:`, err);
32
40
  this.lastIceTimestamp = cand.createdAt;
33
41
  }
34
42
  }
@@ -38,7 +46,7 @@ export class ExchangingIceState extends PeerState {
38
46
  }
39
47
  }
40
48
  catch (err) {
41
- console.error('Error polling for ICE candidates:', err);
49
+ console.error('Error polling for ICE candidates:', err);
42
50
  if (err instanceof Error && err.message.includes('not found')) {
43
51
  this.cleanup();
44
52
  const { FailedState } = await import('./failed-state.js');
@@ -32,7 +32,7 @@ export default class RondevuPeer extends EventEmitter<PeerEvents> {
32
32
  * RTCPeerConnection state
33
33
  */
34
34
  get connectionState(): RTCPeerConnectionState;
35
- constructor(offersApi: RondevuOffers, rtcConfig?: RTCConfiguration, rtcPeerConnection?: typeof RTCPeerConnection, rtcSessionDescription?: typeof RTCSessionDescription, rtcIceCandidate?: typeof RTCIceCandidate);
35
+ constructor(offersApi: RondevuOffers, rtcConfig?: RTCConfiguration, existingPeerConnection?: RTCPeerConnection, rtcPeerConnection?: typeof RTCPeerConnection, rtcSessionDescription?: typeof RTCSessionDescription, rtcIceCandidate?: typeof RTCIceCandidate);
36
36
  /**
37
37
  * Set up peer connection event handlers
38
38
  */