@xtr-dev/rondevu-client 0.12.4 → 0.17.0

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 (46) hide show
  1. package/README.md +100 -381
  2. package/dist/api.d.ts +75 -96
  3. package/dist/api.js +202 -243
  4. package/dist/crypto-adapter.d.ts +37 -0
  5. package/dist/crypto-adapter.js +4 -0
  6. package/dist/index.d.ts +8 -15
  7. package/dist/index.js +5 -8
  8. package/dist/node-crypto-adapter.d.ts +35 -0
  9. package/dist/node-crypto-adapter.js +80 -0
  10. package/dist/rondevu-signaler.d.ts +14 -12
  11. package/dist/rondevu-signaler.js +111 -95
  12. package/dist/rondevu.d.ts +329 -0
  13. package/dist/rondevu.js +648 -0
  14. package/dist/rpc-batcher.d.ts +61 -0
  15. package/dist/rpc-batcher.js +111 -0
  16. package/dist/types.d.ts +8 -21
  17. package/dist/types.js +4 -6
  18. package/dist/web-crypto-adapter.d.ts +16 -0
  19. package/dist/web-crypto-adapter.js +52 -0
  20. package/package.json +1 -1
  21. package/dist/bin.d.ts +0 -35
  22. package/dist/bin.js +0 -35
  23. package/dist/connection-manager.d.ts +0 -104
  24. package/dist/connection-manager.js +0 -324
  25. package/dist/connection.d.ts +0 -112
  26. package/dist/connection.js +0 -194
  27. package/dist/durable-connection.d.ts +0 -120
  28. package/dist/durable-connection.js +0 -244
  29. package/dist/event-bus.d.ts +0 -52
  30. package/dist/event-bus.js +0 -84
  31. package/dist/noop-signaler.d.ts +0 -14
  32. package/dist/noop-signaler.js +0 -27
  33. package/dist/quick-start.d.ts +0 -29
  34. package/dist/quick-start.js +0 -44
  35. package/dist/rondevu-context.d.ts +0 -10
  36. package/dist/rondevu-context.js +0 -20
  37. package/dist/rondevu-service.d.ts +0 -87
  38. package/dist/rondevu-service.js +0 -170
  39. package/dist/service-client.d.ts +0 -77
  40. package/dist/service-client.js +0 -158
  41. package/dist/service-host.d.ts +0 -67
  42. package/dist/service-host.js +0 -120
  43. package/dist/signaler.d.ts +0 -25
  44. package/dist/signaler.js +0 -89
  45. package/dist/webrtc-context.d.ts +0 -5
  46. package/dist/webrtc-context.js +0 -35
@@ -0,0 +1,648 @@
1
+ import { RondevuAPI } from './api.js';
2
+ // ICE server presets
3
+ export const ICE_SERVER_PRESETS = {
4
+ 'ipv4-turn': [
5
+ { urls: 'stun:57.129.61.67:3478' },
6
+ {
7
+ urls: [
8
+ 'turn:57.129.61.67:3478?transport=tcp',
9
+ 'turn:57.129.61.67:3478?transport=udp',
10
+ ],
11
+ username: 'webrtcuser',
12
+ credential: 'supersecretpassword'
13
+ }
14
+ ],
15
+ 'hostname-turns': [
16
+ { urls: 'stun:turn.share.fish:3478' },
17
+ {
18
+ urls: [
19
+ 'turns:turn.share.fish:5349?transport=tcp',
20
+ 'turns:turn.share.fish:5349?transport=udp',
21
+ 'turn:turn.share.fish:3478?transport=tcp',
22
+ 'turn:turn.share.fish:3478?transport=udp',
23
+ ],
24
+ username: 'webrtcuser',
25
+ credential: 'supersecretpassword'
26
+ }
27
+ ],
28
+ 'google-stun': [
29
+ { urls: 'stun:stun.l.google.com:19302' },
30
+ { urls: 'stun:stun1.l.google.com:19302' }
31
+ ],
32
+ 'relay-only': [
33
+ { urls: 'stun:57.129.61.67:3478' },
34
+ {
35
+ urls: [
36
+ 'turn:57.129.61.67:3478?transport=tcp',
37
+ 'turn:57.129.61.67:3478?transport=udp',
38
+ ],
39
+ username: 'webrtcuser',
40
+ credential: 'supersecretpassword',
41
+ // @ts-expect-error - iceTransportPolicy is valid but not in RTCIceServer type
42
+ iceTransportPolicy: 'relay'
43
+ }
44
+ ]
45
+ };
46
+ /**
47
+ * Rondevu - Complete WebRTC signaling client
48
+ *
49
+ * Provides a unified API for:
50
+ * - Implicit username claiming (auto-claimed on first authenticated request)
51
+ * - Service publishing with automatic signature generation
52
+ * - Service discovery (direct, random, paginated)
53
+ * - WebRTC signaling (offer/answer exchange, ICE relay)
54
+ * - Keypair management
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // Create and initialize Rondevu instance with preset ICE servers
59
+ * const rondevu = await Rondevu.connect({
60
+ * apiUrl: 'https://signal.example.com',
61
+ * username: 'alice',
62
+ * iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
63
+ * })
64
+ *
65
+ * // Or use custom ICE servers
66
+ * const rondevu2 = await Rondevu.connect({
67
+ * apiUrl: 'https://signal.example.com',
68
+ * username: 'bob',
69
+ * iceServers: [
70
+ * { urls: 'stun:stun.l.google.com:19302' },
71
+ * { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' }
72
+ * ]
73
+ * })
74
+ *
75
+ * // Publish a service with automatic offer management
76
+ * await rondevu.publishService({
77
+ * service: 'chat:2.0.0',
78
+ * maxOffers: 5, // Maintain up to 5 concurrent offers
79
+ * offerFactory: async (rtcConfig) => {
80
+ * const pc = new RTCPeerConnection(rtcConfig)
81
+ * const dc = pc.createDataChannel('chat')
82
+ * const offer = await pc.createOffer()
83
+ * await pc.setLocalDescription(offer)
84
+ * return { pc, dc, offer }
85
+ * }
86
+ * })
87
+ *
88
+ * // Start accepting connections (auto-fills offers and polls)
89
+ * await rondevu.startFilling()
90
+ *
91
+ * // Access active connections
92
+ * for (const offer of rondevu.getActiveOffers()) {
93
+ * offer.dc?.addEventListener('message', (e) => console.log(e.data))
94
+ * }
95
+ *
96
+ * // Stop when done
97
+ * rondevu.stopFilling()
98
+ * ```
99
+ */
100
+ export class Rondevu {
101
+ constructor(apiUrl, username, keypair, api, iceServers, cryptoAdapter, batchingOptions, debugEnabled = false) {
102
+ this.usernameClaimed = false;
103
+ // Service management
104
+ this.currentService = null;
105
+ this.maxOffers = 0;
106
+ this.offerFactory = null;
107
+ this.ttl = Rondevu.DEFAULT_TTL_MS;
108
+ this.activeOffers = new Map();
109
+ // Polling
110
+ this.filling = false;
111
+ this.pollingInterval = null;
112
+ this.lastPollTimestamp = 0;
113
+ this.apiUrl = apiUrl;
114
+ this.username = username;
115
+ this.keypair = keypair;
116
+ this.api = api;
117
+ this.iceServers = iceServers;
118
+ this.cryptoAdapter = cryptoAdapter;
119
+ this.batchingOptions = batchingOptions;
120
+ this.debugEnabled = debugEnabled;
121
+ this.debug('Instance created:', {
122
+ username: this.username,
123
+ publicKey: this.keypair.publicKey,
124
+ hasIceServers: iceServers.length > 0,
125
+ batchingEnabled: batchingOptions !== false
126
+ });
127
+ }
128
+ /**
129
+ * Internal debug logging - only logs if debug mode is enabled
130
+ */
131
+ debug(message, ...args) {
132
+ if (this.debugEnabled) {
133
+ console.log(`[Rondevu] ${message}`, ...args);
134
+ }
135
+ }
136
+ /**
137
+ * Create and initialize a Rondevu client
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const rondevu = await Rondevu.connect({
142
+ * apiUrl: 'https://api.ronde.vu',
143
+ * username: 'alice'
144
+ * })
145
+ * ```
146
+ */
147
+ static async connect(options) {
148
+ const username = options.username || Rondevu.generateAnonymousUsername();
149
+ // Handle preset string or custom array
150
+ let iceServers;
151
+ if (typeof options.iceServers === 'string') {
152
+ iceServers = ICE_SERVER_PRESETS[options.iceServers];
153
+ }
154
+ else {
155
+ iceServers = options.iceServers || [
156
+ { urls: 'stun:stun.l.google.com:19302' }
157
+ ];
158
+ }
159
+ if (options.debug) {
160
+ console.log('[Rondevu] Connecting:', {
161
+ username,
162
+ hasKeypair: !!options.keypair,
163
+ iceServers: iceServers.length,
164
+ batchingEnabled: options.batching !== false
165
+ });
166
+ }
167
+ // Generate keypair if not provided
168
+ let keypair = options.keypair;
169
+ if (!keypair) {
170
+ if (options.debug)
171
+ console.log('[Rondevu] Generating new keypair...');
172
+ keypair = await RondevuAPI.generateKeypair(options.cryptoAdapter);
173
+ if (options.debug)
174
+ console.log('[Rondevu] Generated keypair, publicKey:', keypair.publicKey);
175
+ }
176
+ else {
177
+ if (options.debug)
178
+ console.log('[Rondevu] Using existing keypair, publicKey:', keypair.publicKey);
179
+ }
180
+ // Create API instance
181
+ const api = new RondevuAPI(options.apiUrl, username, keypair, options.cryptoAdapter, options.batching);
182
+ if (options.debug)
183
+ console.log('[Rondevu] Created API instance');
184
+ return new Rondevu(options.apiUrl, username, keypair, api, iceServers, options.cryptoAdapter, options.batching, options.debug || false);
185
+ }
186
+ /**
187
+ * Generate an anonymous username with timestamp and random component
188
+ */
189
+ static generateAnonymousUsername() {
190
+ const timestamp = Date.now().toString(36);
191
+ const random = Array.from(crypto.getRandomValues(new Uint8Array(3)))
192
+ .map(b => b.toString(16).padStart(2, '0')).join('');
193
+ return `anon-${timestamp}-${random}`;
194
+ }
195
+ // ============================================
196
+ // Username Management
197
+ // ============================================
198
+ /**
199
+ * Check if username has been claimed (checks with server)
200
+ */
201
+ async isUsernameClaimed() {
202
+ try {
203
+ const claimed = await this.api.isUsernameClaimed();
204
+ // Update internal flag to match server state
205
+ this.usernameClaimed = claimed;
206
+ return claimed;
207
+ }
208
+ catch (err) {
209
+ console.error('Failed to check username claim status:', err);
210
+ return false;
211
+ }
212
+ }
213
+ // ============================================
214
+ // Service Publishing
215
+ // ============================================
216
+ /**
217
+ * Default offer factory - creates a simple data channel connection
218
+ */
219
+ async defaultOfferFactory(rtcConfig) {
220
+ const pc = new RTCPeerConnection(rtcConfig);
221
+ const dc = pc.createDataChannel('default');
222
+ const offer = await pc.createOffer();
223
+ await pc.setLocalDescription(offer);
224
+ return { pc, dc, offer };
225
+ }
226
+ /**
227
+ * Publish a service with automatic offer management
228
+ * Call startFilling() to begin accepting connections
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * await rondevu.publishService({
233
+ * service: 'chat:2.0.0',
234
+ * maxOffers: 5
235
+ * })
236
+ * await rondevu.startFilling()
237
+ * ```
238
+ */
239
+ async publishService(options) {
240
+ const { service, maxOffers, offerFactory, ttl } = options;
241
+ this.currentService = service;
242
+ this.maxOffers = maxOffers;
243
+ this.offerFactory = offerFactory || this.defaultOfferFactory.bind(this);
244
+ this.ttl = ttl || Rondevu.DEFAULT_TTL_MS;
245
+ this.debug(`Publishing service: ${service} with maxOffers: ${maxOffers}`);
246
+ this.usernameClaimed = true;
247
+ }
248
+ /**
249
+ * Set up ICE candidate handler to send candidates to the server
250
+ */
251
+ setupIceCandidateHandler(pc, serviceFqn, offerId) {
252
+ pc.onicecandidate = async (event) => {
253
+ if (event.candidate) {
254
+ try {
255
+ await this.api.addOfferIceCandidates(serviceFqn, offerId, [event.candidate.toJSON()]);
256
+ }
257
+ catch (err) {
258
+ console.error('[Rondevu] Failed to send ICE candidate:', err);
259
+ }
260
+ }
261
+ };
262
+ }
263
+ /**
264
+ * Create a single offer and publish it to the server
265
+ */
266
+ async createOffer() {
267
+ if (!this.currentService || !this.offerFactory) {
268
+ throw new Error('Service not published. Call publishService() first.');
269
+ }
270
+ const rtcConfig = {
271
+ iceServers: this.iceServers
272
+ };
273
+ this.debug('Creating new offer...');
274
+ // Create the offer using the factory
275
+ const { pc, dc, offer } = await this.offerFactory(rtcConfig);
276
+ // Auto-append username to service
277
+ const serviceFqn = `${this.currentService}@${this.username}`;
278
+ // Publish to server
279
+ const result = await this.api.publishService({
280
+ serviceFqn,
281
+ offers: [{ sdp: offer.sdp }],
282
+ ttl: this.ttl,
283
+ signature: '',
284
+ message: '',
285
+ });
286
+ const offerId = result.offers[0].offerId;
287
+ // Store active offer
288
+ this.activeOffers.set(offerId, {
289
+ offerId,
290
+ serviceFqn,
291
+ pc,
292
+ dc,
293
+ answered: false,
294
+ createdAt: Date.now()
295
+ });
296
+ this.debug(`Offer created: ${offerId}`);
297
+ // Set up ICE candidate handler
298
+ this.setupIceCandidateHandler(pc, serviceFqn, offerId);
299
+ // Monitor connection state
300
+ pc.onconnectionstatechange = () => {
301
+ this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`);
302
+ if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
303
+ this.activeOffers.delete(offerId);
304
+ this.fillOffers(); // Try to replace failed offer
305
+ }
306
+ };
307
+ }
308
+ /**
309
+ * Fill offers to reach maxOffers count
310
+ */
311
+ async fillOffers() {
312
+ if (!this.filling || !this.currentService)
313
+ return;
314
+ const currentCount = this.activeOffers.size;
315
+ const needed = this.maxOffers - currentCount;
316
+ this.debug(`Filling offers: current=${currentCount}, needed=${needed}`);
317
+ for (let i = 0; i < needed; i++) {
318
+ try {
319
+ await this.createOffer();
320
+ }
321
+ catch (err) {
322
+ console.error('[Rondevu] Failed to create offer:', err);
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Poll for answers and ICE candidates (internal use for automatic offer management)
328
+ */
329
+ async pollInternal() {
330
+ if (!this.filling)
331
+ return;
332
+ try {
333
+ const result = await this.api.poll(this.lastPollTimestamp);
334
+ // Process answers
335
+ for (const answer of result.answers) {
336
+ const activeOffer = this.activeOffers.get(answer.offerId);
337
+ if (activeOffer && !activeOffer.answered) {
338
+ this.debug(`Received answer for offer ${answer.offerId}`);
339
+ await activeOffer.pc.setRemoteDescription({
340
+ type: 'answer',
341
+ sdp: answer.sdp
342
+ });
343
+ activeOffer.answered = true;
344
+ this.lastPollTimestamp = answer.answeredAt;
345
+ // Create replacement offer
346
+ this.fillOffers();
347
+ }
348
+ }
349
+ // Process ICE candidates
350
+ for (const [offerId, candidates] of Object.entries(result.iceCandidates)) {
351
+ const activeOffer = this.activeOffers.get(offerId);
352
+ if (activeOffer) {
353
+ const answererCandidates = candidates.filter(c => c.role === 'answerer');
354
+ for (const item of answererCandidates) {
355
+ if (item.candidate) {
356
+ await activeOffer.pc.addIceCandidate(new RTCIceCandidate(item.candidate));
357
+ this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ catch (err) {
364
+ console.error('[Rondevu] Polling error:', err);
365
+ }
366
+ }
367
+ /**
368
+ * Start filling offers and polling for answers/ICE
369
+ * Call this after publishService() to begin accepting connections
370
+ */
371
+ async startFilling() {
372
+ if (this.filling) {
373
+ this.debug('Already filling');
374
+ return;
375
+ }
376
+ if (!this.currentService) {
377
+ throw new Error('No service published. Call publishService() first.');
378
+ }
379
+ this.debug('Starting offer filling and polling');
380
+ this.filling = true;
381
+ // Fill initial offers
382
+ await this.fillOffers();
383
+ // Start polling
384
+ this.pollingInterval = setInterval(() => {
385
+ this.pollInternal();
386
+ }, Rondevu.POLLING_INTERVAL_MS);
387
+ }
388
+ /**
389
+ * Stop filling offers and polling
390
+ * Closes all active peer connections
391
+ */
392
+ stopFilling() {
393
+ this.debug('Stopping offer filling and polling');
394
+ this.filling = false;
395
+ // Stop polling
396
+ if (this.pollingInterval) {
397
+ clearInterval(this.pollingInterval);
398
+ this.pollingInterval = null;
399
+ }
400
+ // Close all active connections
401
+ for (const [offerId, offer] of this.activeOffers.entries()) {
402
+ this.debug(`Closing offer ${offerId}`);
403
+ offer.dc?.close();
404
+ offer.pc.close();
405
+ }
406
+ this.activeOffers.clear();
407
+ }
408
+ /**
409
+ * Resolve the full service FQN from various input options
410
+ * Supports direct FQN, service+username, or service discovery
411
+ */
412
+ async resolveServiceFqn(options) {
413
+ const { serviceFqn, service, username } = options;
414
+ if (serviceFqn) {
415
+ return serviceFqn;
416
+ }
417
+ else if (service && username) {
418
+ return `${service}@${username}`;
419
+ }
420
+ else if (service) {
421
+ // Discovery mode - get random service
422
+ this.debug(`Discovering service: ${service}`);
423
+ const discovered = await this.discoverService(service);
424
+ return discovered.serviceFqn;
425
+ }
426
+ else {
427
+ throw new Error('Either serviceFqn or service must be provided');
428
+ }
429
+ }
430
+ /**
431
+ * Start polling for remote ICE candidates
432
+ * Returns the polling interval ID
433
+ */
434
+ startIcePolling(pc, serviceFqn, offerId) {
435
+ let lastIceTimestamp = 0;
436
+ return setInterval(async () => {
437
+ try {
438
+ const result = await this.api.getOfferIceCandidates(serviceFqn, offerId, lastIceTimestamp);
439
+ for (const item of result.candidates) {
440
+ if (item.candidate) {
441
+ await pc.addIceCandidate(new RTCIceCandidate(item.candidate));
442
+ lastIceTimestamp = item.createdAt;
443
+ }
444
+ }
445
+ }
446
+ catch (err) {
447
+ console.error('[Rondevu] Failed to poll ICE candidates:', err);
448
+ }
449
+ }, Rondevu.POLLING_INTERVAL_MS);
450
+ }
451
+ /**
452
+ * Automatically connect to a service (answerer side)
453
+ * Handles the entire connection flow: discovery, WebRTC setup, answer exchange, ICE candidates
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * // Connect to specific user
458
+ * const connection = await rondevu.connectToService({
459
+ * serviceFqn: 'chat:2.0.0@alice',
460
+ * onConnection: ({ dc, peerUsername }) => {
461
+ * console.log('Connected to', peerUsername)
462
+ * dc.addEventListener('message', (e) => console.log(e.data))
463
+ * dc.addEventListener('open', () => dc.send('Hello!'))
464
+ * }
465
+ * })
466
+ *
467
+ * // Discover random service
468
+ * const connection = await rondevu.connectToService({
469
+ * service: 'chat:2.0.0',
470
+ * onConnection: ({ dc, peerUsername }) => {
471
+ * console.log('Connected to', peerUsername)
472
+ * }
473
+ * })
474
+ * ```
475
+ */
476
+ async connectToService(options) {
477
+ const { onConnection, rtcConfig } = options;
478
+ // Validate inputs
479
+ if (options.serviceFqn !== undefined && typeof options.serviceFqn === 'string' && !options.serviceFqn.trim()) {
480
+ throw new Error('serviceFqn cannot be empty');
481
+ }
482
+ if (options.service !== undefined && typeof options.service === 'string' && !options.service.trim()) {
483
+ throw new Error('service cannot be empty');
484
+ }
485
+ if (options.username !== undefined && typeof options.username === 'string' && !options.username.trim()) {
486
+ throw new Error('username cannot be empty');
487
+ }
488
+ // Determine the full service FQN
489
+ const fqn = await this.resolveServiceFqn(options);
490
+ this.debug(`Connecting to service: ${fqn}`);
491
+ // 1. Get service offer
492
+ const serviceData = await this.api.getService(fqn);
493
+ this.debug(`Found service from @${serviceData.username}`);
494
+ // 2. Create RTCPeerConnection
495
+ const rtcConfiguration = rtcConfig || {
496
+ iceServers: this.iceServers
497
+ };
498
+ const pc = new RTCPeerConnection(rtcConfiguration);
499
+ // 3. Set up data channel handler (answerer receives it from offerer)
500
+ let dc = null;
501
+ const dataChannelPromise = new Promise((resolve) => {
502
+ pc.ondatachannel = (event) => {
503
+ this.debug('Data channel received from offerer');
504
+ dc = event.channel;
505
+ resolve(dc);
506
+ };
507
+ });
508
+ // 4. Set up ICE candidate exchange
509
+ this.setupIceCandidateHandler(pc, serviceData.serviceFqn, serviceData.offerId);
510
+ // 5. Poll for remote ICE candidates
511
+ const icePollInterval = this.startIcePolling(pc, serviceData.serviceFqn, serviceData.offerId);
512
+ // 6. Set remote description
513
+ await pc.setRemoteDescription({
514
+ type: 'offer',
515
+ sdp: serviceData.sdp
516
+ });
517
+ // 7. Create and send answer
518
+ const answer = await pc.createAnswer();
519
+ await pc.setLocalDescription(answer);
520
+ await this.api.answerOffer(serviceData.serviceFqn, serviceData.offerId, answer.sdp);
521
+ // 8. Wait for data channel to be established
522
+ dc = await dataChannelPromise;
523
+ // Create connection context
524
+ const context = {
525
+ pc,
526
+ dc,
527
+ serviceFqn: serviceData.serviceFqn,
528
+ offerId: serviceData.offerId,
529
+ peerUsername: serviceData.username
530
+ };
531
+ // 9. Set up connection state monitoring
532
+ pc.onconnectionstatechange = () => {
533
+ this.debug(`Connection state: ${pc.connectionState}`);
534
+ if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
535
+ clearInterval(icePollInterval);
536
+ }
537
+ };
538
+ // 10. Wait for data channel to open and call onConnection
539
+ if (dc.readyState === 'open') {
540
+ this.debug('Data channel already open');
541
+ if (onConnection) {
542
+ await onConnection(context);
543
+ }
544
+ }
545
+ else {
546
+ await new Promise((resolve) => {
547
+ dc.addEventListener('open', async () => {
548
+ this.debug('Data channel opened');
549
+ if (onConnection) {
550
+ await onConnection(context);
551
+ }
552
+ resolve();
553
+ });
554
+ });
555
+ }
556
+ return context;
557
+ }
558
+ // ============================================
559
+ // Service Discovery
560
+ // ============================================
561
+ /**
562
+ * Get service by FQN (with username) - Direct lookup
563
+ * Example: chat:1.0.0@alice
564
+ */
565
+ async getService(serviceFqn) {
566
+ return await this.api.getService(serviceFqn);
567
+ }
568
+ /**
569
+ * Discover a random available service without knowing the username
570
+ * Example: chat:1.0.0 (without @username)
571
+ */
572
+ async discoverService(serviceVersion) {
573
+ return await this.api.getService(serviceVersion);
574
+ }
575
+ /**
576
+ * Discover multiple available services with pagination
577
+ * Example: chat:1.0.0 (without @username)
578
+ */
579
+ async discoverServices(serviceVersion, limit = 10, offset = 0) {
580
+ return await this.api.getService(serviceVersion, { limit, offset });
581
+ }
582
+ // ============================================
583
+ // WebRTC Signaling
584
+ // ============================================
585
+ /**
586
+ * Post answer SDP to specific offer
587
+ */
588
+ async postOfferAnswer(serviceFqn, offerId, sdp) {
589
+ await this.api.answerOffer(serviceFqn, offerId, sdp);
590
+ return { success: true, offerId };
591
+ }
592
+ /**
593
+ * Get answer SDP (offerer polls this)
594
+ */
595
+ async getOfferAnswer(serviceFqn, offerId) {
596
+ return await this.api.getOfferAnswer(serviceFqn, offerId);
597
+ }
598
+ /**
599
+ * Combined polling for answers and ICE candidates
600
+ * Returns all answered offers and ICE candidates for all peer's offers since timestamp
601
+ */
602
+ async poll(since) {
603
+ return await this.api.poll(since);
604
+ }
605
+ /**
606
+ * Add ICE candidates to specific offer
607
+ */
608
+ async addOfferIceCandidates(serviceFqn, offerId, candidates) {
609
+ return await this.api.addOfferIceCandidates(serviceFqn, offerId, candidates);
610
+ }
611
+ /**
612
+ * Get ICE candidates for specific offer (with polling support)
613
+ */
614
+ async getOfferIceCandidates(serviceFqn, offerId, since = 0) {
615
+ return await this.api.getOfferIceCandidates(serviceFqn, offerId, since);
616
+ }
617
+ // ============================================
618
+ // Utility Methods
619
+ // ============================================
620
+ /**
621
+ * Get the current keypair (for backup/storage)
622
+ */
623
+ getKeypair() {
624
+ return this.keypair;
625
+ }
626
+ /**
627
+ * Get the username
628
+ */
629
+ getUsername() {
630
+ return this.username;
631
+ }
632
+ /**
633
+ * Get the public key
634
+ */
635
+ getPublicKey() {
636
+ return this.keypair.publicKey;
637
+ }
638
+ /**
639
+ * Access to underlying API for advanced operations
640
+ * @deprecated Use direct methods on Rondevu instance instead
641
+ */
642
+ getAPIPublic() {
643
+ return this.api;
644
+ }
645
+ }
646
+ // Constants
647
+ Rondevu.DEFAULT_TTL_MS = 300000; // 5 minutes
648
+ Rondevu.POLLING_INTERVAL_MS = 1000; // 1 second
@@ -0,0 +1,61 @@
1
+ /**
2
+ * RPC Batcher - Throttles and batches RPC requests to reduce HTTP overhead
3
+ */
4
+ export interface BatcherOptions {
5
+ /**
6
+ * Maximum number of requests to batch together
7
+ * Default: 10
8
+ */
9
+ maxBatchSize?: number;
10
+ /**
11
+ * Maximum time to wait before sending a batch (ms)
12
+ * Default: 50ms
13
+ */
14
+ maxWaitTime?: number;
15
+ /**
16
+ * Minimum time between batches (ms)
17
+ * Default: 10ms
18
+ */
19
+ throttleInterval?: number;
20
+ }
21
+ /**
22
+ * Batches and throttles RPC requests to optimize network usage
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const batcher = new RpcBatcher(
27
+ * (requests) => api.rpcBatch(requests),
28
+ * { maxBatchSize: 10, maxWaitTime: 50 }
29
+ * )
30
+ *
31
+ * // These will be batched together if called within maxWaitTime
32
+ * const result1 = await batcher.add(request1)
33
+ * const result2 = await batcher.add(request2)
34
+ * const result3 = await batcher.add(request3)
35
+ * ```
36
+ */
37
+ export declare class RpcBatcher {
38
+ private queue;
39
+ private batchTimeout;
40
+ private lastBatchTime;
41
+ private options;
42
+ private sendBatch;
43
+ constructor(sendBatch: (requests: any[]) => Promise<any[]>, options?: BatcherOptions);
44
+ /**
45
+ * Add an RPC request to the batch queue
46
+ * Returns a promise that resolves when the request completes
47
+ */
48
+ add(request: any): Promise<any>;
49
+ /**
50
+ * Flush the queue immediately
51
+ */
52
+ flush(): Promise<void>;
53
+ /**
54
+ * Get current queue size
55
+ */
56
+ getQueueSize(): number;
57
+ /**
58
+ * Clear the queue without sending
59
+ */
60
+ clear(): void;
61
+ }