@xtr-dev/rondevu-client 0.20.1 → 0.21.3

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 (43) hide show
  1. package/README.md +83 -385
  2. package/dist/api/batcher.d.ts +60 -38
  3. package/dist/api/batcher.js +121 -77
  4. package/dist/api/client.d.ts +104 -61
  5. package/dist/api/client.js +273 -185
  6. package/dist/connections/answerer.d.ts +15 -6
  7. package/dist/connections/answerer.js +56 -19
  8. package/dist/connections/base.d.ts +6 -4
  9. package/dist/connections/base.js +26 -16
  10. package/dist/connections/config.d.ts +30 -0
  11. package/dist/connections/config.js +20 -0
  12. package/dist/connections/events.d.ts +6 -6
  13. package/dist/connections/offerer.d.ts +37 -8
  14. package/dist/connections/offerer.js +92 -24
  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 +18 -18
  18. package/dist/core/index.js +18 -13
  19. package/dist/core/offer-pool.d.ts +30 -11
  20. package/dist/core/offer-pool.js +90 -76
  21. package/dist/core/peer.d.ts +158 -0
  22. package/dist/core/peer.js +254 -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 +106 -209
  30. package/dist/core/rondevu.js +222 -349
  31. package/dist/crypto/adapter.d.ts +25 -9
  32. package/dist/crypto/node.d.ts +27 -5
  33. package/dist/crypto/node.js +96 -25
  34. package/dist/crypto/web.d.ts +26 -4
  35. package/dist/crypto/web.js +102 -25
  36. package/dist/utils/message-buffer.js +4 -4
  37. package/dist/webrtc/adapter.d.ts +22 -0
  38. package/dist/webrtc/adapter.js +5 -0
  39. package/dist/webrtc/browser.d.ts +12 -0
  40. package/dist/webrtc/browser.js +15 -0
  41. package/dist/webrtc/node.d.ts +32 -0
  42. package/dist/webrtc/node.js +32 -0
  43. package/package.json +17 -6
@@ -1,129 +1,44 @@
1
1
  import { RondevuAPI } from '../api/client.js';
2
+ import { BrowserWebRTCAdapter } from '../webrtc/browser.js';
2
3
  import { EventEmitter } from 'eventemitter3';
3
- import { AnswererConnection } from '../connections/answerer.js';
4
4
  import { OfferPool } from './offer-pool.js';
5
- // ICE server presets
6
- export const ICE_SERVER_PRESETS = {
7
- 'ipv4-turn': [
8
- { urls: 'stun:57.129.61.67:3478' },
9
- {
10
- urls: [
11
- 'turn:57.129.61.67:3478?transport=tcp',
12
- 'turn:57.129.61.67:3478?transport=udp',
13
- ],
14
- username: 'webrtcuser',
15
- credential: 'supersecretpassword'
16
- }
17
- ],
18
- 'hostname-turns': [
19
- { urls: 'stun:turn.share.fish:3478' },
20
- {
21
- urls: [
22
- 'turns:turn.share.fish:5349?transport=tcp',
23
- 'turns:turn.share.fish:5349?transport=udp',
24
- 'turn:turn.share.fish:3478?transport=tcp',
25
- 'turn:turn.share.fish:3478?transport=udp',
26
- ],
27
- username: 'webrtcuser',
28
- credential: 'supersecretpassword'
29
- }
30
- ],
31
- 'google-stun': [
32
- { urls: 'stun:stun.l.google.com:19302' },
33
- { urls: 'stun:stun1.l.google.com:19302' }
34
- ],
35
- 'relay-only': [
36
- { urls: 'stun:57.129.61.67:3478' },
37
- {
38
- urls: [
39
- 'turn:57.129.61.67:3478?transport=tcp',
40
- 'turn:57.129.61.67:3478?transport=udp',
41
- ],
42
- username: 'webrtcuser',
43
- credential: 'supersecretpassword',
44
- // @ts-expect-error - iceTransportPolicy is valid but not in RTCIceServer type
45
- iceTransportPolicy: 'relay'
46
- }
47
- ]
48
- };
49
- /**
50
- * Base error class for Rondevu errors
51
- */
52
- export class RondevuError extends Error {
53
- constructor(message, context) {
54
- super(message);
55
- this.context = context;
56
- this.name = 'RondevuError';
57
- Object.setPrototypeOf(this, RondevuError.prototype);
58
- }
59
- }
60
- /**
61
- * Network-related errors (API calls, connectivity)
62
- */
63
- export class NetworkError extends RondevuError {
64
- constructor(message, context) {
65
- super(message, context);
66
- this.name = 'NetworkError';
67
- Object.setPrototypeOf(this, NetworkError.prototype);
68
- }
69
- }
70
- /**
71
- * Validation errors (invalid input, malformed data)
72
- */
73
- export class ValidationError extends RondevuError {
74
- constructor(message, context) {
75
- super(message, context);
76
- this.name = 'ValidationError';
77
- Object.setPrototypeOf(this, ValidationError.prototype);
78
- }
79
- }
80
- /**
81
- * WebRTC connection errors (peer connection failures, ICE issues)
82
- */
83
- export class ConnectionError extends RondevuError {
84
- constructor(message, context) {
85
- super(message, context);
86
- this.name = 'ConnectionError';
87
- Object.setPrototypeOf(this, ConnectionError.prototype);
88
- }
89
- }
5
+ import { Peer } from './peer.js';
6
+ import { getIceConfiguration } from './ice-config.js';
7
+ import { PollingManager } from './polling-manager.js';
8
+ // Re-export ICE config for backward compatibility
9
+ export { ICE_SERVER_PRESETS } from './ice-config.js';
90
10
  /**
91
11
  * Rondevu - Complete WebRTC signaling client with durable connections
92
12
  *
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.)
13
+ * Uses a tags-based discovery system where offers have 1+ tags for matching.
98
14
  *
99
15
  * @example
100
16
  * ```typescript
101
17
  * // Create and initialize Rondevu instance with preset ICE servers
102
18
  * const rondevu = await Rondevu.connect({
103
19
  * apiUrl: 'https://signal.example.com',
104
- * username: 'alice',
105
20
  * iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
106
21
  * })
107
22
  *
108
- * // Publish a service with automatic offer management
109
- * await rondevu.publishService({
110
- * service: 'chat:2.0.0',
23
+ * // Create offers with tags for discovery
24
+ * await rondevu.offer({
25
+ * tags: ['chat', 'video'],
111
26
  * maxOffers: 5 // Maintain up to 5 concurrent offers
112
27
  * })
113
28
  *
114
29
  * // Start accepting connections (auto-fills offers and polls)
115
30
  * await rondevu.startFilling()
116
31
  *
117
- * // Listen for connections (v1.0.0 API)
32
+ * // Listen for connections
118
33
  * rondevu.on('connection:opened', (offerId, connection) => {
119
34
  * connection.on('connected', () => console.log('Connected!'))
120
35
  * connection.on('message', (data) => console.log('Received:', data))
121
36
  * connection.send('Hello!')
122
37
  * })
123
38
  *
124
- * // Connect to a service (v1.0.0 - returns AnswererConnection)
125
- * const connection = await rondevu.connectToService({
126
- * serviceFqn: 'chat:2.0.0@bob'
39
+ * // Connect by discovering offers with matching tags
40
+ * const connection = await rondevu.connect({
41
+ * tags: ['chat']
127
42
  * })
128
43
  *
129
44
  * connection.on('connected', () => {
@@ -141,27 +56,35 @@ export class ConnectionError extends RondevuError {
141
56
  * ```
142
57
  */
143
58
  export class Rondevu extends EventEmitter {
144
- constructor(apiUrl, username, keypair, api, iceServers, cryptoAdapter, batchingOptions, debugEnabled = false, rtcPeerConnection, rtcIceCandidate) {
59
+ constructor(apiUrl, credential, api, iceServers, iceTransportPolicy, webrtcAdapter, cryptoAdapter, debugEnabled = false) {
145
60
  super();
146
- this.usernameClaimed = false;
147
- // Service management
148
- this.currentService = null;
61
+ // Publishing state
62
+ this.currentTags = null;
149
63
  this.offerPool = null;
150
64
  this.apiUrl = apiUrl;
151
- this.username = username;
152
- this.keypair = keypair;
65
+ this.credential = credential;
153
66
  this.api = api;
154
67
  this.iceServers = iceServers;
68
+ this.iceTransportPolicy = iceTransportPolicy;
69
+ this.webrtcAdapter = webrtcAdapter;
155
70
  this.cryptoAdapter = cryptoAdapter;
156
- this.batchingOptions = batchingOptions;
157
71
  this.debugEnabled = debugEnabled;
158
- this.rtcPeerConnection = rtcPeerConnection;
159
- this.rtcIceCandidate = rtcIceCandidate;
72
+ // Initialize centralized polling manager
73
+ this.pollingManager = new PollingManager({
74
+ api: this.api,
75
+ debugEnabled: this.debugEnabled,
76
+ });
77
+ // Forward polling events to Rondevu instance
78
+ this.pollingManager.on('poll:answer', data => {
79
+ this.emit('poll:answer', data);
80
+ });
81
+ this.pollingManager.on('poll:ice', data => {
82
+ this.emit('poll:ice', data);
83
+ });
160
84
  this.debug('Instance created:', {
161
- username: this.username,
162
- publicKey: this.keypair.publicKey,
85
+ name: this.credential.name,
163
86
  hasIceServers: iceServers.length > 0,
164
- batchingEnabled: batchingOptions !== false
87
+ iceTransportPolicy: iceTransportPolicy || 'all',
165
88
  });
166
89
  }
167
90
  /**
@@ -177,84 +100,76 @@ export class Rondevu extends EventEmitter {
177
100
  *
178
101
  * @example
179
102
  * ```typescript
103
+ * const rondevu = await Rondevu.connect({}) // Uses default API URL
104
+ * // or
180
105
  * const rondevu = await Rondevu.connect({
181
- * apiUrl: 'https://api.ronde.vu',
182
- * username: 'alice'
106
+ * apiUrl: 'https://custom.api.com'
183
107
  * })
184
108
  * ```
185
109
  */
186
- static async connect(options) {
187
- const username = options.username || Rondevu.generateAnonymousUsername();
188
- // Apply WebRTC polyfills to global scope if provided (Node.js environments)
189
- if (options.rtcPeerConnection) {
190
- globalThis.RTCPeerConnection = options.rtcPeerConnection;
191
- }
192
- if (options.rtcIceCandidate) {
193
- globalThis.RTCIceCandidate = options.rtcIceCandidate;
194
- }
195
- // Handle preset string or custom array
196
- let iceServers;
197
- if (typeof options.iceServers === 'string') {
198
- iceServers = ICE_SERVER_PRESETS[options.iceServers];
199
- }
200
- else {
201
- iceServers = options.iceServers || [
202
- { urls: 'stun:stun.l.google.com:19302' }
203
- ];
204
- }
110
+ static async connect(options = {}) {
111
+ const apiUrl = options.apiUrl || Rondevu.DEFAULT_API_URL;
112
+ // Use provided WebRTC adapter or default to browser adapter
113
+ const webrtcAdapter = options.webrtcAdapter || new BrowserWebRTCAdapter();
114
+ // Handle preset string or custom array, extracting iceTransportPolicy if present
115
+ const iceConfig = getIceConfiguration(options.iceServers);
205
116
  if (options.debug) {
206
117
  console.log('[Rondevu] Connecting:', {
207
- username,
208
- hasKeypair: !!options.keypair,
209
- iceServers: iceServers.length,
210
- batchingEnabled: options.batching !== false
118
+ apiUrl,
119
+ hasCredential: !!options.credential,
120
+ iceServers: iceConfig.iceServers?.length ?? 0,
121
+ iceTransportPolicy: iceConfig.iceTransportPolicy || 'all',
211
122
  });
212
123
  }
213
- // Generate keypair if not provided
214
- let keypair = options.keypair;
215
- if (!keypair) {
124
+ // Generate credential if not provided
125
+ let credential = options.credential;
126
+ if (!credential) {
216
127
  if (options.debug)
217
- console.log('[Rondevu] Generating new keypair...');
218
- keypair = await RondevuAPI.generateKeypair(options.cryptoAdapter);
128
+ console.log('[Rondevu] Generating new credentials...');
129
+ credential = await RondevuAPI.generateCredentials(apiUrl, {
130
+ name: options.username, // Will claim this username if provided
131
+ });
219
132
  if (options.debug)
220
- console.log('[Rondevu] Generated keypair, publicKey:', keypair.publicKey);
133
+ console.log('[Rondevu] Generated credentials, name:', credential.name);
221
134
  }
222
135
  else {
223
136
  if (options.debug)
224
- console.log('[Rondevu] Using existing keypair, publicKey:', keypair.publicKey);
137
+ console.log('[Rondevu] Using existing credential, name:', credential.name);
225
138
  }
226
139
  // Create API instance
227
- const api = new RondevuAPI(options.apiUrl, username, keypair, options.cryptoAdapter, options.batching);
140
+ const api = new RondevuAPI(apiUrl, credential, options.cryptoAdapter);
228
141
  if (options.debug)
229
142
  console.log('[Rondevu] Created API instance');
230
- return new Rondevu(options.apiUrl, username, keypair, api, iceServers, options.cryptoAdapter, options.batching, options.debug || false, options.rtcPeerConnection, options.rtcIceCandidate);
143
+ return new Rondevu(apiUrl, credential, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, options.cryptoAdapter, options.debug || false);
231
144
  }
145
+ // ============================================
146
+ // Credential Access
147
+ // ============================================
232
148
  /**
233
- * Generate an anonymous username with timestamp and random component
149
+ * Get the current credential name
234
150
  */
235
- static generateAnonymousUsername() {
236
- const timestamp = Date.now().toString(36);
237
- const random = Array.from(crypto.getRandomValues(new Uint8Array(3)))
238
- .map(b => b.toString(16).padStart(2, '0')).join('');
239
- return `anon-${timestamp}-${random}`;
151
+ getName() {
152
+ return this.credential.name;
240
153
  }
241
- // ============================================
242
- // Username Management
243
- // ============================================
244
154
  /**
245
- * Check if username has been claimed (checks with server)
155
+ * Get the full credential (name + secret)
156
+ * Use this to persist credentials for future sessions
157
+ *
158
+ * ⚠️ SECURITY WARNING:
159
+ * - The secret grants full access to this identity
160
+ * - Store credentials securely (encrypted storage, never in logs)
161
+ * - Never expose credentials in URLs, console output, or error messages
162
+ * - Treat the secret like a password or API key
246
163
  */
247
- async isUsernameClaimed() {
248
- try {
249
- const claimed = await this.api.isUsernameClaimed();
250
- // Update internal flag to match server state
251
- this.usernameClaimed = claimed;
252
- return claimed;
253
- }
254
- catch (err) {
255
- console.error('Failed to check username claim status:', err);
256
- return false;
257
- }
164
+ getCredential() {
165
+ return { ...this.credential };
166
+ }
167
+ /**
168
+ * Get the WebRTC adapter for creating peer connections
169
+ * Used internally by offer pool and connections
170
+ */
171
+ getWebRTCAdapter() {
172
+ return this.webrtcAdapter;
258
173
  }
259
174
  // ============================================
260
175
  // Service Publishing
@@ -270,37 +185,39 @@ export class Rondevu extends EventEmitter {
270
185
  return { dc, offer };
271
186
  }
272
187
  /**
273
- * Publish a service with automatic offer management
274
- * Call startFilling() to begin accepting connections
188
+ * Create offers with tags for discovery (offerer/host side)
189
+ * Auto-starts filling by default. Use the returned object to cancel.
275
190
  *
276
191
  * @example
277
192
  * ```typescript
278
- * await rondevu.publishService({
279
- * service: 'chat:2.0.0',
280
- * maxOffers: 5,
281
- * connectionConfig: {
282
- * reconnectEnabled: true,
283
- * bufferEnabled: true
284
- * }
193
+ * // Auto-start (default)
194
+ * const offer = await rondevu.offer({
195
+ * tags: ['chat', 'video'],
196
+ * maxOffers: 5
285
197
  * })
198
+ * // Later: offer.cancel() to stop
199
+ *
200
+ * // Manual start
201
+ * await rondevu.offer({ tags: ['chat'], maxOffers: 5, autoStart: false })
286
202
  * await rondevu.startFilling()
287
203
  * ```
288
204
  */
289
- async publishService(options) {
290
- const { service, maxOffers, offerFactory, ttl, connectionConfig } = options;
291
- this.currentService = service;
205
+ async offer(options) {
206
+ const { tags, maxOffers, offerFactory, ttl, connectionConfig, autoStart = true } = options;
207
+ this.currentTags = tags;
292
208
  this.connectionConfig = connectionConfig;
293
- // Auto-append username to service
294
- const serviceFqn = `${service}@${this.username}`;
295
- this.debug(`Publishing service: ${service} with maxOffers: ${maxOffers}`);
296
- // Create OfferPool (but don't start it yet - call startFilling() to begin)
209
+ this.debug(`Creating offers with tags: ${tags.join(', ')} with maxOffers: ${maxOffers}`);
210
+ // Create OfferPool
297
211
  this.offerPool = new OfferPool({
298
212
  api: this.api,
299
- serviceFqn,
213
+ tags,
214
+ ownerUsername: this.credential.name,
300
215
  maxOffers,
301
216
  offerFactory: offerFactory || this.defaultOfferFactory.bind(this),
302
217
  ttl: ttl || Rondevu.DEFAULT_TTL_MS,
303
218
  iceServers: this.iceServers,
219
+ iceTransportPolicy: this.iceTransportPolicy,
220
+ webrtcAdapter: this.webrtcAdapter,
304
221
  connectionConfig,
305
222
  debugEnabled: this.debugEnabled,
306
223
  });
@@ -308,23 +225,39 @@ export class Rondevu extends EventEmitter {
308
225
  this.offerPool.on('connection:opened', (offerId, connection) => {
309
226
  this.emit('connection:opened', offerId, connection);
310
227
  });
311
- this.offerPool.on('offer:created', (offerId, serviceFqn) => {
312
- this.emit('offer:created', offerId, serviceFqn);
228
+ this.offerPool.on('offer:created', (offerId, tags) => {
229
+ this.emit('offer:created', offerId, tags);
313
230
  });
314
231
  this.offerPool.on('connection:rotated', (oldOfferId, newOfferId, connection) => {
315
232
  this.emit('connection:rotated', oldOfferId, newOfferId, connection);
316
233
  });
317
- this.usernameClaimed = true;
234
+ // Subscribe to polling events and forward to OfferPool
235
+ this.on('poll:answer', data => {
236
+ this.offerPool?.handlePollAnswer(data);
237
+ });
238
+ this.on('poll:ice', data => {
239
+ this.offerPool?.handlePollIce(data);
240
+ });
241
+ // Auto-start if enabled (default)
242
+ if (autoStart) {
243
+ await this.startFilling();
244
+ }
245
+ // Return handle for cancellation
246
+ return {
247
+ cancel: () => this.stopFilling(),
248
+ };
318
249
  }
319
250
  /**
320
251
  * Start filling offers and polling for answers/ICE
321
- * Call this after publishService() to begin accepting connections
252
+ * Call this after offer() to begin accepting connections
322
253
  */
323
254
  async startFilling() {
324
255
  if (!this.offerPool) {
325
- throw new Error('No service published. Call publishService() first.');
256
+ throw new Error('No offers created. Call offer() first.');
326
257
  }
327
258
  this.debug('Starting offer filling and polling');
259
+ // Start the centralized polling manager
260
+ this.pollingManager.start();
328
261
  await this.offerPool.start();
329
262
  }
330
263
  /**
@@ -333,8 +266,31 @@ export class Rondevu extends EventEmitter {
333
266
  */
334
267
  stopFilling() {
335
268
  this.debug('Stopping offer filling and polling');
269
+ // Stop the centralized polling manager
270
+ this.pollingManager.stop();
336
271
  this.offerPool?.stop();
337
272
  }
273
+ /**
274
+ * Start the centralized polling manager
275
+ * Use this when you need polling without offers (e.g., answerer connections)
276
+ */
277
+ startPolling() {
278
+ this.debug('Starting polling manager');
279
+ this.pollingManager.start();
280
+ }
281
+ /**
282
+ * Stop the centralized polling manager
283
+ */
284
+ stopPolling() {
285
+ this.debug('Stopping polling manager');
286
+ this.pollingManager.stop();
287
+ }
288
+ /**
289
+ * Check if polling is active
290
+ */
291
+ isPolling() {
292
+ return this.pollingManager.isRunning();
293
+ }
338
294
  /**
339
295
  * Get the count of active offers
340
296
  * @returns Number of active offers
@@ -359,148 +315,112 @@ export class Rondevu extends EventEmitter {
359
315
  this.offerPool?.disconnectAll();
360
316
  }
361
317
  /**
362
- * Get the current service status
363
- * @returns Object with service state information
318
+ * Get the current publishing status
319
+ * @returns Object with publishing state information
364
320
  */
365
- getServiceStatus() {
321
+ getPublishStatus() {
366
322
  return {
367
- active: this.currentService !== null,
368
- offerCount: this.offerPool?.getOfferCount() ?? 0
323
+ active: this.currentTags !== null,
324
+ offerCount: this.offerPool?.getOfferCount() ?? 0,
325
+ tags: this.currentTags,
369
326
  };
370
327
  }
371
328
  /**
372
- * Resolve the full service FQN from various input options
373
- * Supports direct FQN, service+username, or service discovery
374
- */
375
- async resolveServiceFqn(options) {
376
- const { serviceFqn, service, username } = options;
377
- if (serviceFqn) {
378
- return serviceFqn;
379
- }
380
- else if (service && username) {
381
- return `${service}@${username}`;
382
- }
383
- else if (service) {
384
- // Discovery mode - get random service
385
- this.debug(`Discovering service: ${service}`);
386
- const discovered = await this.findService(service);
387
- return discovered.serviceFqn;
388
- }
389
- else {
390
- throw new Error('Either serviceFqn or service must be provided');
391
- }
392
- }
393
- /**
394
- * Connect to a service (answerer side) - v1.0.0 API
395
- * Returns an AnswererConnection with automatic reconnection and buffering
396
- *
397
- * BREAKING CHANGE: This now returns AnswererConnection instead of ConnectionContext
329
+ * Create a peer connection with simplified DX
330
+ * Returns a Peer object with clean state management and events
398
331
  *
399
332
  * @example
400
333
  * ```typescript
334
+ * // Connect to any peer matching tags
335
+ * const peer = await rondevu.peer({ tags: ['chat'] })
336
+ *
401
337
  * // Connect to specific user
402
- * const connection = await rondevu.connectToService({
403
- * serviceFqn: 'chat:2.0.0@alice',
404
- * connectionConfig: {
405
- * reconnectEnabled: true,
406
- * bufferEnabled: true
407
- * }
338
+ * const peer = await rondevu.peer({
339
+ * username: 'alice',
340
+ * tags: ['chat']
408
341
  * })
409
342
  *
410
- * connection.on('connected', () => {
411
- * console.log('Connected!')
412
- * connection.send('Hello!')
343
+ * peer.on('open', () => {
344
+ * console.log('Connected to', peer.peerUsername)
345
+ * peer.send('Hello!')
413
346
  * })
414
347
  *
415
- * connection.on('message', (data) => {
348
+ * peer.on('message', (data) => {
416
349
  * console.log('Received:', data)
417
350
  * })
418
351
  *
419
- * connection.on('reconnecting', (attempt) => {
420
- * console.log(`Reconnecting, attempt ${attempt}`)
352
+ * peer.on('state', (state, prevState) => {
353
+ * console.log(`State: ${prevState} ${state}`)
421
354
  * })
422
355
  *
423
- * // Discover random service
424
- * const connection = await rondevu.connectToService({
425
- * service: 'chat:2.0.0'
426
- * })
356
+ * // Access underlying RTCPeerConnection
357
+ * if (peer.peerConnection) {
358
+ * console.log('ICE state:', peer.peerConnection.iceConnectionState)
359
+ * }
427
360
  * ```
428
361
  */
429
- async connectToService(options) {
430
- const { rtcConfig, connectionConfig } = options;
431
- // Validate inputs
432
- if (options.serviceFqn !== undefined && typeof options.serviceFqn === 'string' && !options.serviceFqn.trim()) {
433
- throw new Error('serviceFqn cannot be empty');
434
- }
435
- if (options.service !== undefined && typeof options.service === 'string' && !options.service.trim()) {
436
- throw new Error('service cannot be empty');
437
- }
438
- if (options.username !== undefined && typeof options.username === 'string' && !options.username.trim()) {
439
- throw new Error('username cannot be empty');
440
- }
441
- // Determine the full service FQN
442
- const fqn = await this.resolveServiceFqn(options);
443
- this.debug(`Connecting to service: ${fqn}`);
444
- // Get service offer
445
- const serviceData = await this.api.getService(fqn);
446
- this.debug(`Found service from @${serviceData.username}`);
447
- // Create RTCConfiguration
448
- const rtcConfiguration = rtcConfig || {
449
- iceServers: this.iceServers
450
- };
451
- // Create AnswererConnection
452
- const connection = new AnswererConnection({
362
+ async peer(options) {
363
+ const peer = new Peer({
364
+ ...options,
453
365
  api: this.api,
454
- serviceFqn: serviceData.serviceFqn,
455
- offerId: serviceData.offerId,
456
- offerSdp: serviceData.sdp,
457
- rtcConfig: rtcConfiguration,
458
- config: {
459
- ...connectionConfig,
460
- debug: this.debugEnabled,
461
- },
366
+ iceServers: this.iceServers,
367
+ iceTransportPolicy: this.iceTransportPolicy,
368
+ webrtcAdapter: this.webrtcAdapter,
369
+ debug: this.debugEnabled,
462
370
  });
463
- // Initialize the connection
464
- await connection.initialize();
465
- return connection;
371
+ await peer.initialize();
372
+ // Subscribe to poll:ice events for this peer's connection
373
+ const peerOfferId = peer.offerId;
374
+ const peerConnection = peer.getConnection();
375
+ if (peerConnection) {
376
+ const pollIceHandler = (data) => {
377
+ if (data.offerId === peerOfferId) {
378
+ peerConnection.handleRemoteIceCandidates(data.candidates);
379
+ }
380
+ };
381
+ this.on('poll:ice', pollIceHandler);
382
+ // Clean up handler when connection closes
383
+ peerConnection.on('closed', () => {
384
+ this.off('poll:ice', pollIceHandler);
385
+ });
386
+ }
387
+ // Start polling if not already running
388
+ if (!this.pollingManager.isRunning()) {
389
+ this.debug('Starting polling for peer connection');
390
+ this.pollingManager.start();
391
+ }
392
+ return peer;
466
393
  }
467
394
  // ============================================
468
- // Service Discovery
395
+ // Discovery
469
396
  // ============================================
470
397
  /**
471
- * Find a service - unified discovery method
398
+ * Discover offers by tags
472
399
  *
473
- * @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
474
- * @param options - Discovery options
400
+ * @param tags - Tags to search for (OR logic - matches any tag)
401
+ * @param options - Discovery options (pagination)
475
402
  *
476
403
  * @example
477
404
  * ```typescript
478
- * // Direct lookup (has username)
479
- * const service = await rondevu.findService('chat:1.0.0@alice')
480
- *
481
- * // Random discovery (no username)
482
- * const service = await rondevu.findService('chat:1.0.0')
405
+ * // Discover offers matching any of the tags
406
+ * const result = await rondevu.discover(['chat', 'video'])
483
407
  *
484
408
  * // Paginated discovery
485
- * const result = await rondevu.findService('chat:1.0.0', {
486
- * mode: 'paginated',
409
+ * const result = await rondevu.discover(['chat'], {
487
410
  * limit: 20,
488
411
  * offset: 0
489
412
  * })
413
+ *
414
+ * // Access offers
415
+ * for (const offer of result.offers) {
416
+ * console.log(offer.username, offer.tags)
417
+ * }
490
418
  * ```
491
419
  */
492
- async findService(serviceFqn, options) {
493
- const { mode, limit = 10, offset = 0 } = options || {};
494
- // Auto-detect mode if not specified
495
- const hasUsername = serviceFqn.includes('@');
496
- const effectiveMode = mode || (hasUsername ? 'direct' : 'random');
497
- if (effectiveMode === 'paginated') {
498
- return await this.api.getService(serviceFqn, { limit, offset });
499
- }
500
- else {
501
- // Both 'direct' and 'random' use the same API call
502
- return await this.api.getService(serviceFqn);
503
- }
420
+ async discover(tags, options) {
421
+ const { limit = 10, offset = 0 } = options || {};
422
+ // Always pass limit to ensure we get DiscoverResponse (paginated mode)
423
+ return (await this.api.discover({ tags, limit, offset }));
504
424
  }
505
425
  // ============================================
506
426
  // WebRTC Signaling
@@ -508,15 +428,15 @@ export class Rondevu extends EventEmitter {
508
428
  /**
509
429
  * Post answer SDP to specific offer
510
430
  */
511
- async postOfferAnswer(serviceFqn, offerId, sdp) {
512
- await this.api.answerOffer(serviceFqn, offerId, sdp);
431
+ async postOfferAnswer(offerId, sdp) {
432
+ await this.api.answerOffer(offerId, sdp);
513
433
  return { success: true, offerId };
514
434
  }
515
435
  /**
516
436
  * Get answer SDP (offerer polls this)
517
437
  */
518
- async getOfferAnswer(serviceFqn, offerId) {
519
- return await this.api.getOfferAnswer(serviceFqn, offerId);
438
+ async getOfferAnswer(offerId) {
439
+ return await this.api.getOfferAnswer(offerId);
520
440
  }
521
441
  /**
522
442
  * Combined polling for answers and ICE candidates
@@ -528,73 +448,26 @@ export class Rondevu extends EventEmitter {
528
448
  /**
529
449
  * Add ICE candidates to specific offer
530
450
  */
531
- async addOfferIceCandidates(serviceFqn, offerId, candidates) {
532
- return await this.api.addOfferIceCandidates(serviceFqn, offerId, candidates);
451
+ async addOfferIceCandidates(offerId, candidates) {
452
+ return await this.api.addOfferIceCandidates(offerId, candidates);
533
453
  }
534
454
  /**
535
455
  * Get ICE candidates for specific offer (with polling support)
536
456
  */
537
- async getOfferIceCandidates(serviceFqn, offerId, since = 0) {
538
- return await this.api.getOfferIceCandidates(serviceFqn, offerId, since);
457
+ async getOfferIceCandidates(offerId, since = 0) {
458
+ return await this.api.getOfferIceCandidates(offerId, since);
539
459
  }
540
460
  // ============================================
541
461
  // Utility Methods
542
462
  // ============================================
543
- /**
544
- * Get the current keypair (for backup/storage)
545
- */
546
- getKeypair() {
547
- return this.keypair;
548
- }
549
- /**
550
- * Get the username
551
- */
552
- getUsername() {
553
- return this.username;
554
- }
555
- /**
556
- * Get the public key
557
- */
558
- getPublicKey() {
559
- return this.keypair.publicKey;
560
- }
561
463
  /**
562
464
  * Get active connections (for offerer side)
563
465
  */
564
466
  getActiveConnections() {
565
467
  return this.offerPool?.getActiveConnections() ?? new Map();
566
468
  }
567
- /**
568
- * Get all active offers (legacy compatibility)
569
- * @deprecated Use getActiveConnections() instead
570
- */
571
- getActiveOffers() {
572
- const offers = [];
573
- const connections = this.offerPool?.getActiveConnections() ?? new Map();
574
- for (const [offerId, connection] of connections.entries()) {
575
- const pc = connection.getPeerConnection();
576
- const dc = connection.getDataChannel();
577
- if (pc) {
578
- offers.push({
579
- offerId,
580
- serviceFqn: this.currentService ? `${this.currentService}@${this.username}` : '',
581
- pc,
582
- dc: dc || undefined,
583
- answered: connection.getState() === 'connected',
584
- createdAt: Date.now(),
585
- });
586
- }
587
- }
588
- return offers;
589
- }
590
- /**
591
- * Access to underlying API for advanced operations
592
- * @deprecated Use direct methods on Rondevu instance instead
593
- */
594
- getAPIPublic() {
595
- return this.api;
596
- }
597
469
  }
598
470
  // Constants
471
+ Rondevu.DEFAULT_API_URL = 'https://api.ronde.vu';
599
472
  Rondevu.DEFAULT_TTL_MS = 300000; // 5 minutes
600
473
  Rondevu.POLLING_INTERVAL_MS = 1000; // 1 second