@xtr-dev/rondevu-client 0.18.10 → 0.21.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 (70) hide show
  1. package/README.md +92 -117
  2. package/dist/api/batcher.d.ts +83 -0
  3. package/dist/api/batcher.js +155 -0
  4. package/dist/api/client.d.ts +198 -0
  5. package/dist/api/client.js +400 -0
  6. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
  7. package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
  8. package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
  9. package/dist/{connection.js → connections/base.js} +65 -14
  10. package/dist/connections/config.d.ts +51 -0
  11. package/dist/{connection-config.js → connections/config.js} +20 -0
  12. package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
  13. package/dist/connections/offerer.d.ts +108 -0
  14. package/dist/connections/offerer.js +306 -0
  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 +22 -0
  18. package/dist/core/index.js +22 -0
  19. package/dist/core/offer-pool.d.ts +113 -0
  20. package/dist/core/offer-pool.js +281 -0
  21. package/dist/core/peer.d.ts +155 -0
  22. package/dist/core/peer.js +252 -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 +296 -0
  30. package/dist/core/rondevu.js +472 -0
  31. package/dist/crypto/adapter.d.ts +53 -0
  32. package/dist/crypto/node.d.ts +57 -0
  33. package/dist/crypto/node.js +149 -0
  34. package/dist/crypto/web.d.ts +38 -0
  35. package/dist/crypto/web.js +129 -0
  36. package/dist/utils/async-lock.d.ts +42 -0
  37. package/dist/utils/async-lock.js +75 -0
  38. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  39. package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
  40. package/dist/webrtc/adapter.d.ts +22 -0
  41. package/dist/webrtc/adapter.js +5 -0
  42. package/dist/webrtc/browser.d.ts +12 -0
  43. package/dist/webrtc/browser.js +15 -0
  44. package/dist/webrtc/node.d.ts +32 -0
  45. package/dist/webrtc/node.js +32 -0
  46. package/package.json +20 -9
  47. package/dist/api.d.ts +0 -146
  48. package/dist/api.js +0 -279
  49. package/dist/connection-config.d.ts +0 -21
  50. package/dist/crypto-adapter.d.ts +0 -37
  51. package/dist/index.d.ts +0 -13
  52. package/dist/index.js +0 -10
  53. package/dist/node-crypto-adapter.d.ts +0 -35
  54. package/dist/node-crypto-adapter.js +0 -78
  55. package/dist/offerer-connection.d.ts +0 -54
  56. package/dist/offerer-connection.js +0 -177
  57. package/dist/rondevu-signaler.d.ts +0 -112
  58. package/dist/rondevu-signaler.js +0 -401
  59. package/dist/rondevu.d.ts +0 -407
  60. package/dist/rondevu.js +0 -847
  61. package/dist/rpc-batcher.d.ts +0 -61
  62. package/dist/rpc-batcher.js +0 -111
  63. package/dist/web-crypto-adapter.d.ts +0 -16
  64. package/dist/web-crypto-adapter.js +0 -52
  65. /package/dist/{connection-events.js → connections/events.js} +0 -0
  66. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  67. /package/dist/{types.js → core/types.js} +0 -0
  68. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  69. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  70. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
package/README.md CHANGED
@@ -2,28 +2,16 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@xtr-dev/rondevu-client)](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
4
4
 
5
- 🌐 **Simple WebRTC signaling client with username-based discovery**
5
+ **WebRTC signaling client with durable connections**
6
6
 
7
- TypeScript/JavaScript client for Rondevu, providing WebRTC signaling with username claiming, service publishing/discovery, and efficient batch polling.
8
-
9
- **Related repositories:**
10
- - [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
11
- - [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server), [live](https://api.ronde.vu))
12
- - [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://ronde.vu))
13
-
14
- ---
7
+ TypeScript client for [Rondevu](https://github.com/xtr-dev/rondevu-server), providing WebRTC signaling with automatic reconnection, message buffering, and tags-based discovery.
15
8
 
16
9
  ## Features
17
10
 
18
- - **Username Claiming**: Secure ownership with Ed25519 signatures
19
- - **Anonymous Users**: Auto-generated anonymous usernames for quick testing
20
- - **Service Publishing**: Publish services with multiple offers for connection pooling
21
- - **Service Discovery**: Direct lookup, random discovery, or paginated search
22
- - **Efficient Batch Polling**: Single endpoint for answers and ICE candidates (50% fewer requests)
23
- - **Semantic Version Matching**: Compatible version resolution (chat:1.0.0 matches any 1.x.x)
24
- - **TypeScript**: Full type safety and autocomplete
25
- - **Keypair Management**: Generate or reuse Ed25519 keypairs
26
- - **Automatic Signatures**: All authenticated requests signed automatically
11
+ - **Simple Peer API**: Connect with `rondevu.peer({ tags, username })`
12
+ - **Tags-Based Discovery**: Find peers using tags (e.g., `["chat", "video"]`)
13
+ - **Automatic Reconnection**: Built-in exponential backoff
14
+ - **Message Buffering**: Queues messages during disconnections
27
15
 
28
16
  ## Installation
29
17
 
@@ -33,144 +21,131 @@ npm install @xtr-dev/rondevu-client
33
21
 
34
22
  ## Quick Start
35
23
 
36
- ### Publishing a Service (Offerer)
37
-
38
24
  ```typescript
39
25
  import { Rondevu } from '@xtr-dev/rondevu-client'
40
26
 
41
- // 1. Connect to Rondevu
42
- const rondevu = await Rondevu.connect({
43
- apiUrl: 'https://api.ronde.vu',
44
- username: 'alice', // Or omit for anonymous username
45
- iceServers: 'ipv4-turn' // Preset: 'ipv4-turn', 'hostname-turns', 'google-stun', 'relay-only'
46
- })
47
-
48
- // 2. Publish service with automatic offer management
49
- await rondevu.publishService({
50
- service: 'chat:1.0.0',
51
- maxOffers: 5, // Maintain up to 5 concurrent offers
52
- offerFactory: async (pc) => {
53
- // pc is created by Rondevu with ICE handlers already attached
54
- const dc = pc.createDataChannel('chat')
55
-
56
- dc.addEventListener('open', () => {
57
- console.log('Connection opened!')
58
- dc.send('Hello from Alice!')
59
- })
60
-
61
- dc.addEventListener('message', (e) => {
62
- console.log('Received:', e.data)
63
- })
64
-
65
- const offer = await pc.createOffer()
66
- await pc.setLocalDescription(offer)
67
- return { dc, offer }
68
- }
69
- })
70
-
71
- // 3. Start accepting connections
72
- await rondevu.startFilling()
73
- ```
74
-
75
- ### Connecting to a Service (Answerer)
27
+ // ============================================
28
+ // ALICE: Host and wait for connections
29
+ // ============================================
30
+ const alice = await Rondevu.connect({ username: 'alice' })
76
31
 
77
- ```typescript
78
- import { Rondevu } from '@xtr-dev/rondevu-client'
79
-
80
- // 1. Connect to Rondevu
81
- const rondevu = await Rondevu.connect({
82
- apiUrl: 'https://api.ronde.vu',
83
- username: 'bob',
84
- iceServers: 'ipv4-turn'
32
+ alice.on('connection:opened', (offerId, connection) => {
33
+ console.log('Connected to', connection.peerUsername)
34
+ connection.on('message', (data) => console.log('Received:', data))
35
+ connection.send('Hello!')
85
36
  })
86
37
 
87
- // 2. Connect to service (automatic WebRTC setup)
88
- const connection = await rondevu.connectToService({
89
- serviceFqn: 'chat:1.0.0@alice',
90
- onConnection: ({ dc, peerUsername }) => {
91
- console.log('Connected to', peerUsername)
38
+ const offer = await alice.offer({ tags: ['chat'], maxOffers: 5 })
39
+ // Later: offer.cancel() to stop accepting connections
92
40
 
93
- dc.addEventListener('message', (e) => {
94
- console.log('Received:', e.data)
95
- })
41
+ // ============================================
42
+ // BOB: Connect to Alice
43
+ // ============================================
44
+ const bob = await Rondevu.connect()
96
45
 
97
- dc.addEventListener('open', () => {
98
- dc.send('Hello from Bob!')
99
- })
100
- }
46
+ const peer = await bob.peer({
47
+ username: 'alice',
48
+ tags: ['chat']
101
49
  })
102
50
 
103
- // Access connection
104
- connection.dc.send('Another message')
105
- connection.pc.close() // Close when done
51
+ peer.on('open', () => peer.send('Hello Alice!'))
52
+ peer.on('message', (data) => console.log('Received:', data))
106
53
  ```
107
54
 
108
- ## Core API
55
+ ## API Reference
109
56
 
110
57
  ### Rondevu.connect()
111
58
 
112
59
  ```typescript
113
60
  const rondevu = await Rondevu.connect({
114
- apiUrl: string, // Required: Signaling server URL
115
- username?: string, // Optional: your username (auto-generates anonymous if omitted)
116
- keypair?: Keypair, // Optional: reuse existing keypair
117
- iceServers?: IceServerPreset | RTCIceServer[], // Optional: preset or custom config
118
- debug?: boolean // Optional: enable debug logging (default: false)
61
+ apiUrl?: string, // Default: 'https://api.ronde.vu'
62
+ credential?: Credential, // Reuse existing credential
63
+ username?: string, // Claim username (4-32 chars)
64
+ iceServers?: IceServerPreset | RTCIceServer[], // Default: 'rondevu'
65
+ debug?: boolean
119
66
  })
67
+
68
+ rondevu.getName() // Get username
69
+ rondevu.getCredential() // Get credential for reuse
120
70
  ```
121
71
 
122
- ### Service Publishing
72
+ **ICE Presets**: `'rondevu'` (default), `'rondevu-relay'`, `'google-stun'`, `'public-stun'`
73
+
74
+ ### rondevu.peer()
123
75
 
124
76
  ```typescript
125
- await rondevu.publishService({
126
- service: string, // e.g., 'chat:1.0.0' (username auto-appended)
127
- maxOffers: number, // Maximum concurrent offers to maintain
128
- offerFactory?: OfferFactory, // Optional: custom offer creation
129
- ttl?: number // Optional: offer lifetime in ms (default: 300000)
77
+ const peer = await rondevu.peer({
78
+ tags: string[],
79
+ username?: string,
80
+ rtcConfig?: RTCConfiguration
130
81
  })
131
82
 
132
- await rondevu.startFilling() // Start accepting connections
133
- rondevu.stopFilling() // Stop and close all connections
83
+ // Events
84
+ peer.on('open', () => {})
85
+ peer.on('close', (reason) => {})
86
+ peer.on('message', (data) => {})
87
+ peer.on('error', (error) => {})
88
+ peer.on('reconnecting', (attempt, max) => {})
89
+
90
+ // Properties & Methods
91
+ peer.state // 'connecting' | 'connected' | 'reconnecting' | ...
92
+ peer.peerUsername
93
+ peer.send(data)
94
+ peer.close()
134
95
  ```
135
96
 
136
- ### Service Discovery
97
+ ### rondevu.offer()
137
98
 
138
99
  ```typescript
139
- // Direct lookup (with username)
140
- await rondevu.getService('chat:1.0.0@alice')
100
+ const offer = await rondevu.offer({
101
+ tags: string[],
102
+ maxOffers: number,
103
+ ttl?: number, // Offer lifetime in ms (default: 300000)
104
+ autoStart?: boolean // Auto-start filling (default: true)
105
+ })
141
106
 
142
- // Random discovery (without username)
143
- await rondevu.discoverService('chat:1.0.0')
107
+ offer.cancel() // Stop accepting connections
144
108
 
145
- // Paginated discovery
146
- await rondevu.discoverServices('chat:1.0.0', limit, offset)
109
+ rondevu.on('connection:opened', (offerId, connection) => {
110
+ connection.on('message', (data) => {})
111
+ connection.send('Hello!')
112
+ })
147
113
  ```
148
114
 
149
- ### Connecting to Services
115
+ ### rondevu.discover()
150
116
 
151
117
  ```typescript
152
- const connection = await rondevu.connectToService({
153
- serviceFqn?: string, // Full FQN like 'chat:1.0.0@alice'
154
- service?: string, // Service without username (for discovery)
155
- username?: string, // Target username (combined with service)
156
- onConnection?: (context) => void, // Called when data channel opens
157
- rtcConfig?: RTCConfiguration // Optional: override ICE servers
158
- })
118
+ const result = await rondevu.discover(['chat'], { limit: 20 })
119
+ result.offers.forEach(o => console.log(o.username, o.tags))
159
120
  ```
160
121
 
161
- ## Documentation
122
+ ## Credentials
123
+
124
+ ```typescript
125
+ // Auto-generated username
126
+ const rondevu = await Rondevu.connect()
127
+ // rondevu.getName() === 'friendly-panda-a1b2c3'
128
+
129
+ // Claimed username
130
+ const rondevu = await Rondevu.connect({ username: 'alice' })
131
+
132
+ // Save and restore credentials
133
+ const credential = rondevu.getCredential()
134
+ localStorage.setItem('cred', JSON.stringify(credential))
135
+
136
+ const saved = JSON.parse(localStorage.getItem('cred'))
137
+ const rondevu = await Rondevu.connect({ credential: saved })
138
+ ```
139
+
140
+ ## Tag Validation
141
+
142
+ Tags: 1-64 chars, lowercase alphanumeric with dots/dashes.
162
143
 
163
- 📚 **[ADVANCED.md](./ADVANCED.md)** - Comprehensive guide including:
164
- - Detailed API reference for all methods
165
- - Type definitions and interfaces
166
- - Platform support (Browser & Node.js)
167
- - Advanced usage patterns
168
- - Username rules and service FQN format
169
- - Examples and migration guides
144
+ Valid: `chat`, `video-call`, `com.example.service`
170
145
 
171
- ## Examples
146
+ ## Links
172
147
 
173
- - [React Demo](https://github.com/xtr-dev/rondevu-demo) - Full browser UI ([live](https://ronde.vu))
148
+ - [Live Demo](https://ronde.vu) | [Server](https://github.com/xtr-dev/rondevu-server) | [API](https://api.ronde.vu)
174
149
 
175
150
  ## License
176
151
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * RPC Request Batcher with throttling
3
+ *
4
+ * Collects RPC requests over a short time window and sends them efficiently.
5
+ *
6
+ * Due to server authentication design (signature covers method+params),
7
+ * authenticated requests are sent individually while unauthenticated
8
+ * requests can be truly batched together.
9
+ */
10
+ export interface RpcRequest {
11
+ method: string;
12
+ params?: any;
13
+ }
14
+ export interface RpcResponse {
15
+ success: boolean;
16
+ result?: any;
17
+ error?: string;
18
+ errorCode?: string;
19
+ }
20
+ export interface BatcherOptions {
21
+ /** Delay in ms before flushing queued requests (default: 10) */
22
+ delay?: number;
23
+ /** Maximum batch size for unauthenticated requests (default: 50) */
24
+ maxBatchSize?: number;
25
+ }
26
+ /**
27
+ * RpcBatcher - Batches RPC requests with throttling
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const batcher = new RpcBatcher('https://api.example.com', {
32
+ * delay: 10,
33
+ * maxBatchSize: 50
34
+ * })
35
+ *
36
+ * // Requests made within the delay window are batched
37
+ * const [result1, result2] = await Promise.all([
38
+ * batcher.add({ method: 'getOffer', params: {...} }, null),
39
+ * batcher.add({ method: 'getOffer', params: {...} }, null)
40
+ * ])
41
+ * ```
42
+ */
43
+ export declare class RpcBatcher {
44
+ private readonly baseUrl;
45
+ private queue;
46
+ private flushTimer;
47
+ private readonly delay;
48
+ private readonly maxBatchSize;
49
+ constructor(baseUrl: string, options?: BatcherOptions);
50
+ /**
51
+ * Add a request to the batch queue
52
+ * @param request - The RPC request
53
+ * @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
54
+ * @returns Promise that resolves with the request result
55
+ */
56
+ add(request: RpcRequest, authHeaders: Record<string, string> | null): Promise<any>;
57
+ /**
58
+ * Schedule a flush after the delay
59
+ */
60
+ private scheduleFlush;
61
+ /**
62
+ * Flush all queued requests
63
+ */
64
+ private flush;
65
+ /**
66
+ * Process unauthenticated requests in batches
67
+ */
68
+ private processUnauthenticatedBatches;
69
+ /**
70
+ * Process authenticated requests individually
71
+ * Each authenticated request needs its own HTTP call because
72
+ * the signature covers the specific method+params
73
+ */
74
+ private processAuthenticatedRequests;
75
+ /**
76
+ * Send a batch of requests
77
+ */
78
+ private sendBatch;
79
+ /**
80
+ * Flush immediately (useful for cleanup/testing)
81
+ */
82
+ flushNow(): Promise<void>;
83
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * RPC Request Batcher with throttling
3
+ *
4
+ * Collects RPC requests over a short time window and sends them efficiently.
5
+ *
6
+ * Due to server authentication design (signature covers method+params),
7
+ * authenticated requests are sent individually while unauthenticated
8
+ * requests can be truly batched together.
9
+ */
10
+ /**
11
+ * RpcBatcher - Batches RPC requests with throttling
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const batcher = new RpcBatcher('https://api.example.com', {
16
+ * delay: 10,
17
+ * maxBatchSize: 50
18
+ * })
19
+ *
20
+ * // Requests made within the delay window are batched
21
+ * const [result1, result2] = await Promise.all([
22
+ * batcher.add({ method: 'getOffer', params: {...} }, null),
23
+ * batcher.add({ method: 'getOffer', params: {...} }, null)
24
+ * ])
25
+ * ```
26
+ */
27
+ export class RpcBatcher {
28
+ constructor(baseUrl, options = {}) {
29
+ this.baseUrl = baseUrl;
30
+ this.queue = [];
31
+ this.flushTimer = null;
32
+ this.delay = options.delay ?? 10;
33
+ this.maxBatchSize = options.maxBatchSize ?? 50;
34
+ }
35
+ /**
36
+ * Add a request to the batch queue
37
+ * @param request - The RPC request
38
+ * @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
39
+ * @returns Promise that resolves with the request result
40
+ */
41
+ add(request, authHeaders) {
42
+ return new Promise((resolve, reject) => {
43
+ this.queue.push({ request, authHeaders, resolve, reject });
44
+ this.scheduleFlush();
45
+ });
46
+ }
47
+ /**
48
+ * Schedule a flush after the delay
49
+ */
50
+ scheduleFlush() {
51
+ if (this.flushTimer)
52
+ return;
53
+ this.flushTimer = setTimeout(() => {
54
+ this.flushTimer = null;
55
+ this.flush();
56
+ }, this.delay);
57
+ }
58
+ /**
59
+ * Flush all queued requests
60
+ */
61
+ async flush() {
62
+ if (this.queue.length === 0)
63
+ return;
64
+ const items = this.queue;
65
+ this.queue = [];
66
+ // Separate authenticated vs unauthenticated requests
67
+ const unauthenticated = [];
68
+ const authenticated = [];
69
+ for (const item of items) {
70
+ if (item.authHeaders) {
71
+ authenticated.push(item);
72
+ }
73
+ else {
74
+ unauthenticated.push(item);
75
+ }
76
+ }
77
+ // Process unauthenticated requests in batches
78
+ await this.processUnauthenticatedBatches(unauthenticated);
79
+ // Process authenticated requests individually (each needs unique signature)
80
+ await this.processAuthenticatedRequests(authenticated);
81
+ }
82
+ /**
83
+ * Process unauthenticated requests in batches
84
+ */
85
+ async processUnauthenticatedBatches(items) {
86
+ if (items.length === 0)
87
+ return;
88
+ // Split into chunks of maxBatchSize
89
+ for (let i = 0; i < items.length; i += this.maxBatchSize) {
90
+ const chunk = items.slice(i, i + this.maxBatchSize);
91
+ await this.sendBatch(chunk, null);
92
+ }
93
+ }
94
+ /**
95
+ * Process authenticated requests individually
96
+ * Each authenticated request needs its own HTTP call because
97
+ * the signature covers the specific method+params
98
+ */
99
+ async processAuthenticatedRequests(items) {
100
+ // Send all authenticated requests in parallel, each as its own batch of 1
101
+ await Promise.all(items.map(item => this.sendBatch([item], item.authHeaders)));
102
+ }
103
+ /**
104
+ * Send a batch of requests
105
+ */
106
+ async sendBatch(items, authHeaders) {
107
+ try {
108
+ const requests = items.map(item => item.request);
109
+ const headers = {
110
+ 'Content-Type': 'application/json',
111
+ };
112
+ if (authHeaders) {
113
+ Object.assign(headers, authHeaders);
114
+ }
115
+ const response = await fetch(`${this.baseUrl}/rpc`, {
116
+ method: 'POST',
117
+ headers,
118
+ body: JSON.stringify(requests), // Always send as array
119
+ });
120
+ if (!response.ok) {
121
+ const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
122
+ items.forEach(item => item.reject(error));
123
+ return;
124
+ }
125
+ const results = await response.json();
126
+ // Match responses to requests (server returns array in same order)
127
+ items.forEach((item, index) => {
128
+ const result = results[index];
129
+ if (!result) {
130
+ item.reject(new Error('Missing response from server'));
131
+ }
132
+ else if (!result.success) {
133
+ item.reject(new Error(result.error || 'RPC call failed'));
134
+ }
135
+ else {
136
+ item.resolve(result.result);
137
+ }
138
+ });
139
+ }
140
+ catch (error) {
141
+ // Network or parsing error - reject all
142
+ items.forEach(item => item.reject(error));
143
+ }
144
+ }
145
+ /**
146
+ * Flush immediately (useful for cleanup/testing)
147
+ */
148
+ async flushNow() {
149
+ if (this.flushTimer) {
150
+ clearTimeout(this.flushTimer);
151
+ this.flushTimer = null;
152
+ }
153
+ await this.flush();
154
+ }
155
+ }