@xtr-dev/rondevu-client 0.18.10 → 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 (41) 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} +77 -85
  15. package/dist/core/rondevu.js +600 -0
  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 +4 -4
  22. package/dist/index.d.ts +0 -13
  23. package/dist/index.js +0 -10
  24. package/dist/rondevu-signaler.d.ts +0 -112
  25. package/dist/rondevu-signaler.js +0 -401
  26. package/dist/rondevu.js +0 -847
  27. /package/dist/{rpc-batcher.d.ts → api/batcher.d.ts} +0 -0
  28. /package/dist/{rpc-batcher.js → api/batcher.js} +0 -0
  29. /package/dist/{connection-config.d.ts → connections/config.d.ts} +0 -0
  30. /package/dist/{connection-config.js → connections/config.js} +0 -0
  31. /package/dist/{connection-events.d.ts → connections/events.d.ts} +0 -0
  32. /package/dist/{connection-events.js → connections/events.js} +0 -0
  33. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  34. /package/dist/{types.js → core/types.js} +0 -0
  35. /package/dist/{crypto-adapter.d.ts → crypto/adapter.d.ts} +0 -0
  36. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  37. /package/dist/{node-crypto-adapter.js → crypto/node.js} +0 -0
  38. /package/dist/{web-crypto-adapter.js → crypto/web.js} +0 -0
  39. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  40. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
  41. /package/dist/{message-buffer.js → utils/message-buffer.js} +0 -0
@@ -0,0 +1,600 @@
1
+ import { RondevuAPI } from '../api/client.js';
2
+ import { EventEmitter } from 'eventemitter3';
3
+ import { AnswererConnection } from '../connections/answerer.js';
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
+ }
90
+ /**
91
+ * Rondevu - Complete WebRTC signaling client with durable connections
92
+ *
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.)
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * // Create and initialize Rondevu instance with preset ICE servers
102
+ * const rondevu = await Rondevu.connect({
103
+ * apiUrl: 'https://signal.example.com',
104
+ * username: 'alice',
105
+ * iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
106
+ * })
107
+ *
108
+ * // Publish a service with automatic offer management
109
+ * await rondevu.publishService({
110
+ * service: 'chat:2.0.0',
111
+ * maxOffers: 5 // Maintain up to 5 concurrent offers
112
+ * })
113
+ *
114
+ * // Start accepting connections (auto-fills offers and polls)
115
+ * await rondevu.startFilling()
116
+ *
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
+ * })
123
+ *
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
+ * })
141
+ * ```
142
+ */
143
+ export class Rondevu extends EventEmitter {
144
+ constructor(apiUrl, username, keypair, api, iceServers, cryptoAdapter, batchingOptions, debugEnabled = false, rtcPeerConnection, rtcIceCandidate) {
145
+ super();
146
+ this.usernameClaimed = false;
147
+ // Service management
148
+ this.currentService = null;
149
+ this.offerPool = null;
150
+ this.apiUrl = apiUrl;
151
+ this.username = username;
152
+ this.keypair = keypair;
153
+ this.api = api;
154
+ this.iceServers = iceServers;
155
+ this.cryptoAdapter = cryptoAdapter;
156
+ this.batchingOptions = batchingOptions;
157
+ this.debugEnabled = debugEnabled;
158
+ this.rtcPeerConnection = rtcPeerConnection;
159
+ this.rtcIceCandidate = rtcIceCandidate;
160
+ this.debug('Instance created:', {
161
+ username: this.username,
162
+ publicKey: this.keypair.publicKey,
163
+ hasIceServers: iceServers.length > 0,
164
+ batchingEnabled: batchingOptions !== false
165
+ });
166
+ }
167
+ /**
168
+ * Internal debug logging - only logs if debug mode is enabled
169
+ */
170
+ debug(message, ...args) {
171
+ if (this.debugEnabled) {
172
+ console.log(`[Rondevu] ${message}`, ...args);
173
+ }
174
+ }
175
+ /**
176
+ * Create and initialize a Rondevu client
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const rondevu = await Rondevu.connect({
181
+ * apiUrl: 'https://api.ronde.vu',
182
+ * username: 'alice'
183
+ * })
184
+ * ```
185
+ */
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
+ }
205
+ if (options.debug) {
206
+ console.log('[Rondevu] Connecting:', {
207
+ username,
208
+ hasKeypair: !!options.keypair,
209
+ iceServers: iceServers.length,
210
+ batchingEnabled: options.batching !== false
211
+ });
212
+ }
213
+ // Generate keypair if not provided
214
+ let keypair = options.keypair;
215
+ if (!keypair) {
216
+ if (options.debug)
217
+ console.log('[Rondevu] Generating new keypair...');
218
+ keypair = await RondevuAPI.generateKeypair(options.cryptoAdapter);
219
+ if (options.debug)
220
+ console.log('[Rondevu] Generated keypair, publicKey:', keypair.publicKey);
221
+ }
222
+ else {
223
+ if (options.debug)
224
+ console.log('[Rondevu] Using existing keypair, publicKey:', keypair.publicKey);
225
+ }
226
+ // Create API instance
227
+ const api = new RondevuAPI(options.apiUrl, username, keypair, options.cryptoAdapter, options.batching);
228
+ if (options.debug)
229
+ 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);
231
+ }
232
+ /**
233
+ * Generate an anonymous username with timestamp and random component
234
+ */
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}`;
240
+ }
241
+ // ============================================
242
+ // Username Management
243
+ // ============================================
244
+ /**
245
+ * Check if username has been claimed (checks with server)
246
+ */
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
+ }
258
+ }
259
+ // ============================================
260
+ // Service Publishing
261
+ // ============================================
262
+ /**
263
+ * Default offer factory - creates a simple data channel connection
264
+ * The RTCPeerConnection is created by Rondevu and passed in
265
+ */
266
+ async defaultOfferFactory(pc) {
267
+ const dc = pc.createDataChannel('default');
268
+ const offer = await pc.createOffer();
269
+ await pc.setLocalDescription(offer);
270
+ return { dc, offer };
271
+ }
272
+ /**
273
+ * Publish a service with automatic offer management
274
+ * Call startFilling() to begin accepting connections
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * await rondevu.publishService({
279
+ * service: 'chat:2.0.0',
280
+ * maxOffers: 5,
281
+ * connectionConfig: {
282
+ * reconnectEnabled: true,
283
+ * bufferEnabled: true
284
+ * }
285
+ * })
286
+ * await rondevu.startFilling()
287
+ * ```
288
+ */
289
+ async publishService(options) {
290
+ const { service, maxOffers, offerFactory, ttl, connectionConfig } = options;
291
+ this.currentService = service;
292
+ 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)
297
+ this.offerPool = new OfferPool({
298
+ api: this.api,
299
+ serviceFqn,
300
+ maxOffers,
301
+ offerFactory: offerFactory || this.defaultOfferFactory.bind(this),
302
+ ttl: ttl || Rondevu.DEFAULT_TTL_MS,
303
+ iceServers: this.iceServers,
304
+ connectionConfig,
305
+ debugEnabled: this.debugEnabled,
306
+ });
307
+ // Forward events from OfferPool
308
+ this.offerPool.on('connection:opened', (offerId, connection) => {
309
+ this.emit('connection:opened', offerId, connection);
310
+ });
311
+ this.offerPool.on('offer:created', (offerId, serviceFqn) => {
312
+ this.emit('offer:created', offerId, serviceFqn);
313
+ });
314
+ this.offerPool.on('connection:rotated', (oldOfferId, newOfferId, connection) => {
315
+ this.emit('connection:rotated', oldOfferId, newOfferId, connection);
316
+ });
317
+ this.usernameClaimed = true;
318
+ }
319
+ /**
320
+ * Start filling offers and polling for answers/ICE
321
+ * Call this after publishService() to begin accepting connections
322
+ */
323
+ async startFilling() {
324
+ if (!this.offerPool) {
325
+ throw new Error('No service published. Call publishService() first.');
326
+ }
327
+ this.debug('Starting offer filling and polling');
328
+ await this.offerPool.start();
329
+ }
330
+ /**
331
+ * Stop filling offers and polling
332
+ * Closes all active peer connections
333
+ */
334
+ stopFilling() {
335
+ this.debug('Stopping offer filling and polling');
336
+ this.offerPool?.stop();
337
+ }
338
+ /**
339
+ * Get the count of active offers
340
+ * @returns Number of active offers
341
+ */
342
+ getOfferCount() {
343
+ return this.offerPool?.getOfferCount() ?? 0;
344
+ }
345
+ /**
346
+ * Check if an offer is currently connected
347
+ * @param offerId - The offer ID to check
348
+ * @returns True if the offer exists and is connected
349
+ */
350
+ isConnected(offerId) {
351
+ return this.offerPool?.isConnected(offerId) ?? false;
352
+ }
353
+ /**
354
+ * Disconnect all active offers
355
+ * Similar to stopFilling() but doesn't stop the polling/filling process
356
+ */
357
+ disconnectAll() {
358
+ this.debug('Disconnecting all offers');
359
+ this.offerPool?.disconnectAll();
360
+ }
361
+ /**
362
+ * Get the current service status
363
+ * @returns Object with service state information
364
+ */
365
+ getServiceStatus() {
366
+ return {
367
+ active: this.currentService !== null,
368
+ offerCount: this.offerPool?.getOfferCount() ?? 0
369
+ };
370
+ }
371
+ /**
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
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * // 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
+ * }
408
+ * })
409
+ *
410
+ * connection.on('connected', () => {
411
+ * console.log('Connected!')
412
+ * connection.send('Hello!')
413
+ * })
414
+ *
415
+ * connection.on('message', (data) => {
416
+ * console.log('Received:', data)
417
+ * })
418
+ *
419
+ * connection.on('reconnecting', (attempt) => {
420
+ * console.log(`Reconnecting, attempt ${attempt}`)
421
+ * })
422
+ *
423
+ * // Discover random service
424
+ * const connection = await rondevu.connectToService({
425
+ * service: 'chat:2.0.0'
426
+ * })
427
+ * ```
428
+ */
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({
453
+ 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
+ },
462
+ });
463
+ // Initialize the connection
464
+ await connection.initialize();
465
+ return connection;
466
+ }
467
+ // ============================================
468
+ // Service Discovery
469
+ // ============================================
470
+ /**
471
+ * Find a service - unified discovery method
472
+ *
473
+ * @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
474
+ * @param options - Discovery options
475
+ *
476
+ * @example
477
+ * ```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')
483
+ *
484
+ * // Paginated discovery
485
+ * const result = await rondevu.findService('chat:1.0.0', {
486
+ * mode: 'paginated',
487
+ * limit: 20,
488
+ * offset: 0
489
+ * })
490
+ * ```
491
+ */
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
+ }
504
+ }
505
+ // ============================================
506
+ // WebRTC Signaling
507
+ // ============================================
508
+ /**
509
+ * Post answer SDP to specific offer
510
+ */
511
+ async postOfferAnswer(serviceFqn, offerId, sdp) {
512
+ await this.api.answerOffer(serviceFqn, offerId, sdp);
513
+ return { success: true, offerId };
514
+ }
515
+ /**
516
+ * Get answer SDP (offerer polls this)
517
+ */
518
+ async getOfferAnswer(serviceFqn, offerId) {
519
+ return await this.api.getOfferAnswer(serviceFqn, offerId);
520
+ }
521
+ /**
522
+ * Combined polling for answers and ICE candidates
523
+ * Returns all answered offers and ICE candidates for all peer's offers since timestamp
524
+ */
525
+ async poll(since) {
526
+ return await this.api.poll(since);
527
+ }
528
+ /**
529
+ * Add ICE candidates to specific offer
530
+ */
531
+ async addOfferIceCandidates(serviceFqn, offerId, candidates) {
532
+ return await this.api.addOfferIceCandidates(serviceFqn, offerId, candidates);
533
+ }
534
+ /**
535
+ * Get ICE candidates for specific offer (with polling support)
536
+ */
537
+ async getOfferIceCandidates(serviceFqn, offerId, since = 0) {
538
+ return await this.api.getOfferIceCandidates(serviceFqn, offerId, since);
539
+ }
540
+ // ============================================
541
+ // Utility Methods
542
+ // ============================================
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
+ /**
562
+ * Get active connections (for offerer side)
563
+ */
564
+ getActiveConnections() {
565
+ return this.offerPool?.getActiveConnections() ?? new Map();
566
+ }
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
+ }
598
+ // Constants
599
+ Rondevu.DEFAULT_TTL_MS = 300000; // 5 minutes
600
+ Rondevu.POLLING_INTERVAL_MS = 1000; // 1 second
@@ -2,7 +2,7 @@
2
2
  * Node.js Crypto adapter for Node.js environments
3
3
  * Requires Node.js 19+ or Node.js 18 with --experimental-global-webcrypto flag
4
4
  */
5
- import { CryptoAdapter, Keypair } from './crypto-adapter.js';
5
+ import { CryptoAdapter, Keypair } from './adapter.js';
6
6
  /**
7
7
  * Node.js Crypto implementation using Node.js built-in APIs
8
8
  * Uses Buffer for base64 encoding and crypto.randomBytes for random generation
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Web Crypto adapter for browser environments
3
3
  */
4
- import { CryptoAdapter, Keypair } from './crypto-adapter.js';
4
+ import { CryptoAdapter, Keypair } from './adapter.js';
5
5
  /**
6
6
  * Web Crypto implementation using browser APIs
7
7
  * Uses btoa/atob for base64 encoding and crypto.getRandomValues for random bytes
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AsyncLock provides a mutual exclusion primitive for asynchronous operations.
3
+ * Ensures only one async operation can proceed at a time while queuing others.
4
+ */
5
+ export declare class AsyncLock {
6
+ private locked;
7
+ private queue;
8
+ /**
9
+ * Acquire the lock. If already locked, waits until released.
10
+ * @returns Promise that resolves when lock is acquired
11
+ */
12
+ acquire(): Promise<void>;
13
+ /**
14
+ * Release the lock. If others are waiting, grants lock to next in queue.
15
+ */
16
+ release(): void;
17
+ /**
18
+ * Run a function with the lock acquired, automatically releasing after.
19
+ * This is the recommended way to use AsyncLock to prevent forgetting to release.
20
+ *
21
+ * @param fn - Async function to run with lock held
22
+ * @returns Promise resolving to the function's return value
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const lock = new AsyncLock()
27
+ * const result = await lock.run(async () => {
28
+ * // Critical section - only one caller at a time
29
+ * return await doSomething()
30
+ * })
31
+ * ```
32
+ */
33
+ run<T>(fn: () => Promise<T>): Promise<T>;
34
+ /**
35
+ * Check if lock is currently held
36
+ */
37
+ isLocked(): boolean;
38
+ /**
39
+ * Get number of operations waiting for the lock
40
+ */
41
+ getQueueLength(): number;
42
+ }