@xtr-dev/rondevu-client 0.10.1 → 0.11.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.
package/README.md CHANGED
@@ -2,9 +2,9 @@
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
- 🌐 **WebRTC with durable connections and automatic reconnection**
5
+ 🌐 **Simple, high-level WebRTC peer-to-peer connections**
6
6
 
7
- TypeScript/JavaScript client for Rondevu, providing durable WebRTC connections that survive network interruptions with automatic reconnection and message queuing.
7
+ TypeScript/JavaScript client for Rondevu, providing easy-to-use WebRTC connections with automatic signaling, username-based discovery, and built-in reconnection support.
8
8
 
9
9
  **Related repositories:**
10
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))
@@ -15,14 +15,16 @@ TypeScript/JavaScript client for Rondevu, providing durable WebRTC connections t
15
15
 
16
16
  ## Features
17
17
 
18
- - **Durable Connections**: Automatic reconnection on network drops
19
- - **Message Queuing**: Messages sent during disconnections are queued and flushed on reconnect
20
- - **Durable Channels**: RTCDataChannel wrappers that survive connection drops
21
- - **TTL Auto-Refresh**: Services automatically republish before expiration
22
- - **Username Claiming**: Cryptographic ownership with Ed25519 signatures
23
- - **Service Publishing**: Package-style naming (com.example.chat@1.0.0)
18
+ - **High-Level Wrappers**: ServiceHost and ServiceClient eliminate WebRTC boilerplate
19
+ - **Username-Based Discovery**: Connect to peers by username, not complex offer/answer exchange
20
+ - **Semver-Compatible Matching**: Requesting chat@1.0.0 matches any compatible 1.x.x version
21
+ - **Privacy-First Design**: Services are hidden by default - no enumeration possible
22
+ - **Automatic Reconnection**: Built-in retry logic with exponential backoff
23
+ - **Message Queuing**: Messages sent while disconnected are queued and flushed on reconnect
24
+ - **Cryptographic Username Claiming**: Secure ownership with Ed25519 signatures
25
+ - **Service Publishing**: Package-style naming (chat.app@1.0.0) with multiple simultaneous offers
24
26
  - **TypeScript**: Full type safety and autocomplete
25
- - **Configurable**: All timeouts, retry limits, and queue sizes are configurable
27
+ - **Configurable Polling**: Exponential backoff with jitter to reduce server load
26
28
 
27
29
  ## Install
28
30
 
@@ -32,588 +34,400 @@ npm install @xtr-dev/rondevu-client
32
34
 
33
35
  ## Quick Start
34
36
 
35
- ### Publishing a Service (Alice)
37
+ ### Hosting a Service (Alice)
36
38
 
37
39
  ```typescript
38
- import { Rondevu } from '@xtr-dev/rondevu-client';
39
-
40
- // Initialize client and register
41
- const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
42
- await client.register();
43
-
44
- // Step 1: Claim username (one-time)
45
- const claim = await client.usernames.claimUsername('alice');
46
- client.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
47
-
48
- // Step 2: Expose service with handler
49
- const keypair = client.usernames.loadKeypairFromStorage('alice');
50
-
51
- const service = await client.exposeService({
52
- username: 'alice',
53
- privateKey: keypair.privateKey,
54
- serviceFqn: 'chat@1.0.0',
55
- isPublic: true,
56
- poolSize: 10, // Handle 10 concurrent connections
57
- handler: (channel, connectionId) => {
58
- console.log(`📡 New connection: ${connectionId}`);
59
-
60
- channel.on('message', (data) => {
61
- console.log('📥 Received:', data);
62
- channel.send(`Echo: ${data}`);
63
- });
64
-
65
- channel.on('close', () => {
66
- console.log(`👋 Connection ${connectionId} closed`);
67
- });
68
- }
69
- });
70
-
71
- // Start the service
72
- const info = await service.start();
73
- console.log(`Service published with UUID: ${info.uuid}`);
74
- console.log('Waiting for connections...');
75
-
76
- // Later: stop the service
77
- await service.stop();
40
+ import { RondevuService, ServiceHost } from '@xtr-dev/rondevu-client'
41
+
42
+ // Step 1: Create and initialize service
43
+ const service = new RondevuService({
44
+ apiUrl: 'https://api.ronde.vu',
45
+ username: 'alice'
46
+ })
47
+
48
+ await service.initialize() // Generates keypair
49
+ await service.claimUsername() // Claims username with signature
50
+
51
+ // Step 2: Create ServiceHost
52
+ const host = new ServiceHost({
53
+ service: 'chat.app@1.0.0',
54
+ rondevuService: service,
55
+ maxPeers: 5, // Accept up to 5 connections
56
+ ttl: 300000 // 5 minutes
57
+ })
58
+
59
+ // Step 3: Listen for incoming connections
60
+ host.events.on('connection', (connection) => {
61
+ console.log('✅ New connection!')
62
+
63
+ connection.events.on('message', (msg) => {
64
+ console.log('📨 Received:', msg)
65
+ connection.sendMessage('Hello from Alice!')
66
+ })
67
+
68
+ connection.events.on('state-change', (state) => {
69
+ console.log('Connection state:', state)
70
+ })
71
+ })
72
+
73
+ host.events.on('error', (error) => {
74
+ console.error('Host error:', error)
75
+ })
76
+
77
+ // Step 4: Start hosting
78
+ await host.start()
79
+ console.log('Service is now live! Others can connect to @alice')
80
+
81
+ // Later: stop hosting
82
+ host.dispose()
78
83
  ```
79
84
 
80
85
  ### Connecting to a Service (Bob)
81
86
 
82
87
  ```typescript
83
- import { Rondevu } from '@xtr-dev/rondevu-client';
84
-
85
- // Initialize client and register
86
- const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
87
- await client.register();
88
-
89
- // Connect to Alice's service
90
- const connection = await client.connect('alice', 'chat@1.0.0', {
91
- maxReconnectAttempts: 5
92
- });
88
+ import { RondevuService, ServiceClient } from '@xtr-dev/rondevu-client'
89
+
90
+ // Step 1: Create and initialize service
91
+ const service = new RondevuService({
92
+ apiUrl: 'https://api.ronde.vu',
93
+ username: 'bob'
94
+ })
95
+
96
+ await service.initialize()
97
+ await service.claimUsername()
98
+
99
+ // Step 2: Create ServiceClient
100
+ const client = new ServiceClient({
101
+ username: 'alice', // Connect to Alice
102
+ serviceFqn: 'chat.app@1.0.0',
103
+ rondevuService: service,
104
+ autoReconnect: true,
105
+ maxReconnectAttempts: 5
106
+ })
107
+
108
+ // Step 3: Listen for connection events
109
+ client.events.on('connected', (connection) => {
110
+ console.log('✅ Connected to Alice!')
111
+
112
+ connection.events.on('message', (msg) => {
113
+ console.log('📨 Received:', msg)
114
+ })
115
+
116
+ // Send a message
117
+ connection.sendMessage('Hello from Bob!')
118
+ })
119
+
120
+ client.events.on('disconnected', () => {
121
+ console.log('🔌 Disconnected')
122
+ })
123
+
124
+ client.events.on('reconnecting', ({ attempt, maxAttempts }) => {
125
+ console.log(`🔄 Reconnecting (${attempt}/${maxAttempts})...`)
126
+ })
127
+
128
+ client.events.on('error', (error) => {
129
+ console.error('❌ Error:', error)
130
+ })
131
+
132
+ // Step 4: Connect
133
+ await client.connect()
134
+
135
+ // Later: disconnect
136
+ client.dispose()
137
+ ```
93
138
 
94
- // Create a durable channel
95
- const channel = connection.createChannel('main');
139
+ ## Core Concepts
96
140
 
97
- channel.on('message', (data) => {
98
- console.log('📥 Received:', data);
99
- });
141
+ ### RondevuService
100
142
 
101
- channel.on('open', () => {
102
- console.log('✅ Channel open');
103
- channel.send('Hello Alice!');
104
- });
143
+ Handles authentication and username management:
144
+ - Generates Ed25519 keypair for signing
145
+ - Claims usernames with cryptographic proof
146
+ - Provides API client for signaling server
105
147
 
106
- // Listen for connection events
107
- connection.on('connected', () => {
108
- console.log('🎉 Connected to Alice');
109
- });
148
+ ### ServiceHost
110
149
 
111
- connection.on('reconnecting', (attempt, max, delay) => {
112
- console.log(`🔄 Reconnecting... (${attempt}/${max}, retry in ${delay}ms)`);
113
- });
150
+ High-level wrapper for hosting a WebRTC service:
151
+ - Automatically creates and publishes offers
152
+ - Handles incoming connections
153
+ - Manages ICE candidate exchange
154
+ - Supports multiple simultaneous peers
114
155
 
115
- connection.on('disconnected', () => {
116
- console.log('🔌 Disconnected');
117
- });
156
+ ### ServiceClient
118
157
 
119
- connection.on('failed', (error) => {
120
- console.error('❌ Connection failed permanently:', error);
121
- });
158
+ High-level wrapper for connecting to services:
159
+ - Discovers services by username
160
+ - Handles offer/answer exchange automatically
161
+ - Built-in auto-reconnection with exponential backoff
162
+ - Event-driven API
122
163
 
123
- // Establish the connection
124
- await connection.connect();
164
+ ### RTCDurableConnection
125
165
 
126
- // Messages sent during disconnection are automatically queued
127
- channel.send('This will be queued if disconnected');
166
+ Low-level connection wrapper (used internally):
167
+ - Manages WebRTC PeerConnection lifecycle
168
+ - Handles ICE candidate polling
169
+ - Provides message queue for reliability
170
+ - State management and events
128
171
 
129
- // Later: close the connection
130
- await connection.close();
131
- ```
172
+ ## API Reference
132
173
 
133
- ## Core Concepts
174
+ ### RondevuService
134
175
 
135
- ### DurableConnection
176
+ ```typescript
177
+ const service = new RondevuService({
178
+ apiUrl: string, // Signaling server URL
179
+ username: string, // Your username
180
+ keypair?: Keypair // Optional: reuse existing keypair
181
+ })
136
182
 
137
- Manages WebRTC peer lifecycle with automatic reconnection:
138
- - Automatically reconnects when connection drops
139
- - Exponential backoff with jitter (1s → 2s → 4s → 8s → ... max 30s)
140
- - Configurable max retry attempts (default: 10)
141
- - Manages multiple DurableChannel instances
183
+ // Initialize service (generates keypair if not provided)
184
+ await service.initialize(): Promise<void>
142
185
 
143
- ### DurableChannel
186
+ // Claim username with cryptographic signature
187
+ await service.claimUsername(): Promise<void>
144
188
 
145
- Wraps RTCDataChannel with message queuing:
146
- - Queues messages during disconnection
147
- - Flushes queue on reconnection
148
- - Configurable queue size and message age limits
149
- - RTCDataChannel-compatible API with event emitters
189
+ // Check if username is claimed
190
+ service.isUsernameClaimed(): boolean
150
191
 
151
- ### DurableService
192
+ // Get current username
193
+ service.getUsername(): string
152
194
 
153
- Server-side service with TTL auto-refresh:
154
- - Automatically republishes service before TTL expires
155
- - Creates DurableConnection for each incoming peer
156
- - Manages connection pool for multiple simultaneous connections
195
+ // Get keypair
196
+ service.getKeypair(): Keypair
157
197
 
158
- ## API Reference
198
+ // Get API client
199
+ service.getAPI(): RondevuAPI
200
+ ```
159
201
 
160
- ### Main Client
202
+ ### ServiceHost
161
203
 
162
204
  ```typescript
163
- const client = new Rondevu({
164
- baseUrl: 'https://api.ronde.vu', // optional, default shown
165
- credentials?: { peerId, secret }, // optional, skip registration
166
- fetch?: customFetch, // optional, for Node.js < 18
167
- RTCPeerConnection?: RTCPeerConnection, // optional, for Node.js
168
- RTCSessionDescription?: RTCSessionDescription,
169
- RTCIceCandidate?: RTCIceCandidate
170
- });
171
-
172
- // Register and get credentials
173
- const creds = await client.register();
174
- // { peerId: '...', secret: '...' }
175
-
176
- // Check if authenticated
177
- client.isAuthenticated(); // boolean
178
-
179
- // Get current credentials
180
- client.getCredentials(); // { peerId, secret } | undefined
205
+ const host = new ServiceHost({
206
+ service: string, // Service FQN (e.g., 'chat.app@1.0.0')
207
+ rondevuService: RondevuService,
208
+ maxPeers?: number, // Default: 5
209
+ ttl?: number, // Default: 300000 (5 minutes)
210
+ isPublic?: boolean, // Default: true
211
+ rtcConfiguration?: RTCConfiguration
212
+ })
213
+
214
+ // Start hosting
215
+ await host.start(): Promise<void>
216
+
217
+ // Stop hosting and cleanup
218
+ host.dispose(): void
219
+
220
+ // Get all active connections
221
+ host.getConnections(): RTCDurableConnection[]
222
+
223
+ // Events
224
+ host.events.on('connection', (conn: RTCDurableConnection) => {})
225
+ host.events.on('error', (error: Error) => {})
181
226
  ```
182
227
 
183
- ### Username API
228
+ ### ServiceClient
184
229
 
185
230
  ```typescript
186
- // Check username availability
187
- const check = await client.usernames.checkUsername('alice');
188
- // { available: true } or { available: false, expiresAt: number, publicKey: string }
189
-
190
- // Claim username with new keypair
191
- const claim = await client.usernames.claimUsername('alice');
192
- // { username, publicKey, privateKey, claimedAt, expiresAt }
193
-
194
- // Save keypair to localStorage
195
- client.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
196
-
197
- // Load keypair from localStorage
198
- const keypair = client.usernames.loadKeypairFromStorage('alice');
199
- // { publicKey, privateKey } | null
231
+ const client = new ServiceClient({
232
+ username: string, // Host username to connect to
233
+ serviceFqn: string, // Service FQN (e.g., 'chat.app@1.0.0')
234
+ rondevuService: RondevuService,
235
+ autoReconnect?: boolean, // Default: true
236
+ maxReconnectAttempts?: number, // Default: 5
237
+ rtcConfiguration?: RTCConfiguration
238
+ })
239
+
240
+ // Connect to service
241
+ await client.connect(): Promise<RTCDurableConnection>
242
+
243
+ // Disconnect and cleanup
244
+ client.dispose(): void
245
+
246
+ // Get current connection
247
+ client.getConnection(): RTCDurableConnection | null
248
+
249
+ // Events
250
+ client.events.on('connected', (conn: RTCDurableConnection) => {})
251
+ client.events.on('disconnected', () => {})
252
+ client.events.on('reconnecting', (info: { attempt: number, maxAttempts: number }) => {})
253
+ client.events.on('error', (error: Error) => {})
200
254
  ```
201
255
 
202
- **Username Rules:**
203
- - Format: Lowercase alphanumeric + dash (`a-z`, `0-9`, `-`)
204
- - Length: 3-32 characters
205
- - Pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`
206
- - Validity: 365 days from claim/last use
207
- - Ownership: Secured by Ed25519 public key
208
-
209
- ### Durable Service API
256
+ ### RTCDurableConnection
210
257
 
211
258
  ```typescript
212
- // Expose a durable service
213
- const service = await client.exposeService({
214
- username: 'alice',
215
- privateKey: keypair.privateKey,
216
- serviceFqn: 'chat@1.0.0',
217
-
218
- // Service options
219
- isPublic: true, // optional, default: false
220
- metadata: { version: '1.0' }, // optional
221
- ttl: 300000, // optional, default: 5 minutes
222
- ttlRefreshMargin: 0.2, // optional, refresh at 80% of TTL
223
-
224
- // Connection pooling
225
- poolSize: 10, // optional, default: 1
226
- pollingInterval: 2000, // optional, default: 2000ms
227
-
228
- // Connection options (applied to incoming connections)
229
- maxReconnectAttempts: 10, // optional, default: 10
230
- reconnectBackoffBase: 1000, // optional, default: 1000ms
231
- reconnectBackoffMax: 30000, // optional, default: 30000ms
232
- reconnectJitter: 0.2, // optional, default: 0.2 (±20%)
233
- connectionTimeout: 30000, // optional, default: 30000ms
234
-
235
- // Message queuing
236
- maxQueueSize: 1000, // optional, default: 1000
237
- maxMessageAge: 60000, // optional, default: 60000ms (1 minute)
238
-
239
- // WebRTC configuration
240
- rtcConfig: {
241
- iceServers: [
242
- { urls: 'stun:stun.l.google.com:19302' }
243
- ]
244
- },
245
-
246
- // Connection handler
247
- handler: (channel, connectionId) => {
248
- // Handle incoming connection
249
- channel.on('message', (data) => {
250
- console.log('Received:', data);
251
- channel.send(`Echo: ${data}`);
252
- });
253
- }
254
- });
255
-
256
- // Start the service
257
- const info = await service.start();
258
- // { serviceId: '...', uuid: '...', expiresAt: 1234567890 }
259
-
260
- // Get active connections
261
- const connections = service.getActiveConnections();
262
- // ['conn-123', 'conn-456']
263
-
264
- // Get service info
265
- const serviceInfo = service.getServiceInfo();
266
- // { serviceId: '...', uuid: '...', expiresAt: 1234567890 } | null
267
-
268
- // Stop the service
269
- await service.stop();
270
- ```
259
+ // Connection state
260
+ connection.state: 'connected' | 'connecting' | 'disconnected'
271
261
 
272
- **Service Events:**
273
- ```typescript
274
- service.on('published', (serviceId, uuid) => {
275
- console.log(`Service published: ${uuid}`);
276
- });
262
+ // Send message (returns true if sent, false if queued)
263
+ await connection.sendMessage(message: string): Promise<boolean>
277
264
 
278
- service.on('connection', (connectionId) => {
279
- console.log(`New connection: ${connectionId}`);
280
- });
265
+ // Queue message for sending when connected
266
+ await connection.queueMessage(message: string, options?: QueueMessageOptions): Promise<void>
281
267
 
282
- service.on('disconnection', (connectionId) => {
283
- console.log(`Connection closed: ${connectionId}`);
284
- });
268
+ // Disconnect
269
+ connection.disconnect(): void
285
270
 
286
- service.on('ttl-refreshed', (expiresAt) => {
287
- console.log(`TTL refreshed, expires at: ${new Date(expiresAt)}`);
288
- });
271
+ // Events
272
+ connection.events.on('message', (msg: string) => {})
273
+ connection.events.on('state-change', (state: ConnectionStates) => {})
274
+ ```
289
275
 
290
- service.on('error', (error, context) => {
291
- console.error(`Service error (${context}):`, error);
292
- });
276
+ ## Configuration
293
277
 
294
- service.on('closed', () => {
295
- console.log('Service stopped');
296
- });
297
- ```
278
+ ### Polling Configuration
298
279
 
299
- ### Durable Connection API
280
+ The signaling uses configurable polling with exponential backoff:
300
281
 
301
282
  ```typescript
302
- // Connect by username and service FQN
303
- const connection = await client.connect('alice', 'chat@1.0.0', {
304
- // Connection options
305
- maxReconnectAttempts: 10, // optional, default: 10
306
- reconnectBackoffBase: 1000, // optional, default: 1000ms
307
- reconnectBackoffMax: 30000, // optional, default: 30000ms
308
- reconnectJitter: 0.2, // optional, default: 0.2 (±20%)
309
- connectionTimeout: 30000, // optional, default: 30000ms
310
-
311
- // Message queuing
312
- maxQueueSize: 1000, // optional, default: 1000
313
- maxMessageAge: 60000, // optional, default: 60000ms
314
-
315
- // WebRTC configuration
316
- rtcConfig: {
317
- iceServers: [
318
- { urls: 'stun:stun.l.google.com:19302' }
319
- ]
320
- }
321
- });
322
-
323
- // Connect by UUID
324
- const connection2 = await client.connectByUuid('service-uuid-here', {
325
- maxReconnectAttempts: 5
326
- });
327
-
328
- // Create channels before connecting
329
- const channel = connection.createChannel('main');
330
- const fileChannel = connection.createChannel('files', {
331
- ordered: false,
332
- maxRetransmits: 3
333
- });
334
-
335
- // Get existing channel
336
- const existingChannel = connection.getChannel('main');
337
-
338
- // Check connection state
339
- const state = connection.getState();
340
- // 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed' | 'closed'
341
-
342
- const isConnected = connection.isConnected();
343
-
344
- // Connect
345
- await connection.connect();
346
-
347
- // Close connection
348
- await connection.close();
283
+ // Default polling config
284
+ {
285
+ initialInterval: 500, // Start at 500ms
286
+ maxInterval: 5000, // Max 5 seconds
287
+ backoffMultiplier: 1.5, // Increase by 1.5x each time
288
+ maxRetries: 50, // Max 50 attempts
289
+ jitter: true // Add random 0-100ms to prevent thundering herd
290
+ }
349
291
  ```
350
292
 
351
- **Connection Events:**
352
- ```typescript
353
- connection.on('state', (newState, previousState) => {
354
- console.log(`State: ${previousState} → ${newState}`);
355
- });
293
+ This is handled automatically - no configuration needed.
356
294
 
357
- connection.on('connected', () => {
358
- console.log('Connected');
359
- });
295
+ ### WebRTC Configuration
360
296
 
361
- connection.on('reconnecting', (attempt, maxAttempts, delay) => {
362
- console.log(`Reconnecting (${attempt}/${maxAttempts}) in ${delay}ms`);
363
- });
364
-
365
- connection.on('disconnected', () => {
366
- console.log('Disconnected');
367
- });
368
-
369
- connection.on('failed', (error, permanent) => {
370
- console.error('Connection failed:', error, 'Permanent:', permanent);
371
- });
372
-
373
- connection.on('closed', () => {
374
- console.log('Connection closed');
375
- });
376
- ```
377
-
378
- ### Durable Channel API
297
+ Provide custom STUN/TURN servers:
379
298
 
380
299
  ```typescript
381
- const channel = connection.createChannel('chat', {
382
- ordered: true, // optional, default: true
383
- maxRetransmits: undefined // optional, for unordered channels
384
- });
300
+ const host = new ServiceHost({
301
+ service: 'chat.app@1.0.0',
302
+ rondevuService: service,
303
+ rtcConfiguration: {
304
+ iceServers: [
305
+ { urls: 'stun:stun.l.google.com:19302' },
306
+ {
307
+ urls: 'turn:turn.example.com:3478',
308
+ username: 'user',
309
+ credential: 'pass'
310
+ }
311
+ ]
312
+ }
313
+ })
314
+ ```
385
315
 
386
- // Send data (queued if disconnected)
387
- channel.send('Hello!');
388
- channel.send(new ArrayBuffer(1024));
389
- channel.send(new Blob(['data']));
316
+ ## Username Rules
390
317
 
391
- // Check state
392
- const state = channel.readyState;
393
- // 'connecting' | 'open' | 'closing' | 'closed'
318
+ - **Format**: Lowercase alphanumeric + dash (`a-z`, `0-9`, `-`)
319
+ - **Length**: 3-32 characters
320
+ - **Pattern**: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`
321
+ - **Validity**: 365 days from claim/last use
322
+ - **Ownership**: Secured by Ed25519 public key signature
394
323
 
395
- // Get buffered amount
396
- const buffered = channel.bufferedAmount;
324
+ ## Examples
397
325
 
398
- // Set buffered amount low threshold
399
- channel.bufferedAmountLowThreshold = 16 * 1024; // 16KB
326
+ ### Chat Application
400
327
 
401
- // Get queue size (for debugging)
402
- const queueSize = channel.getQueueSize();
328
+ See [demo/demo.js](./demo/demo.js) for a complete working example.
403
329
 
404
- // Close channel
405
- channel.close();
406
- ```
330
+ ### Persistent Keypair
407
331
 
408
- **Channel Events:**
409
332
  ```typescript
410
- channel.on('open', () => {
411
- console.log('Channel open');
412
- });
333
+ // Save keypair to localStorage
334
+ const service = new RondevuService({
335
+ apiUrl: 'https://api.ronde.vu',
336
+ username: 'alice'
337
+ })
413
338
 
414
- channel.on('message', (data) => {
415
- console.log('Received:', data);
416
- });
339
+ await service.initialize()
340
+ await service.claimUsername()
417
341
 
418
- channel.on('error', (error) => {
419
- console.error('Channel error:', error);
420
- });
342
+ // Save for later
343
+ localStorage.setItem('rondevu-keypair', JSON.stringify(service.getKeypair()))
344
+ localStorage.setItem('rondevu-username', service.getUsername())
421
345
 
422
- channel.on('close', () => {
423
- console.log('Channel closed');
424
- });
346
+ // Load on next session
347
+ const savedKeypair = JSON.parse(localStorage.getItem('rondevu-keypair'))
348
+ const savedUsername = localStorage.getItem('rondevu-username')
425
349
 
426
- channel.on('bufferedAmountLow', () => {
427
- console.log('Buffer drained, safe to send more');
428
- });
350
+ const service2 = new RondevuService({
351
+ apiUrl: 'https://api.ronde.vu',
352
+ username: savedUsername,
353
+ keypair: savedKeypair
354
+ })
429
355
 
430
- channel.on('queueOverflow', (droppedCount) => {
431
- console.warn(`Queue overflow: ${droppedCount} messages dropped`);
432
- });
356
+ await service2.initialize() // Reuses keypair
433
357
  ```
434
358
 
435
- ## Configuration Options
436
-
437
- ### Connection Configuration
359
+ ### Message Queue Example
438
360
 
439
361
  ```typescript
440
- interface DurableConnectionConfig {
441
- maxReconnectAttempts?: number; // default: 10
442
- reconnectBackoffBase?: number; // default: 1000 (1 second)
443
- reconnectBackoffMax?: number; // default: 30000 (30 seconds)
444
- reconnectJitter?: number; // default: 0.2 (±20%)
445
- connectionTimeout?: number; // default: 30000 (30 seconds)
446
- maxQueueSize?: number; // default: 1000 messages
447
- maxMessageAge?: number; // default: 60000 (1 minute)
448
- rtcConfig?: RTCConfiguration;
449
- }
362
+ // Messages are automatically queued if not connected yet
363
+ client.events.on('connected', (connection) => {
364
+ // Send immediately
365
+ connection.sendMessage('Hello!')
366
+ })
367
+
368
+ // Or queue for later
369
+ await client.connect()
370
+ const conn = client.getConnection()
371
+ await conn.queueMessage('This will be sent when connected', {
372
+ expiresAt: Date.now() + 60000 // Expire after 1 minute
373
+ })
450
374
  ```
451
375
 
452
- ### Service Configuration
376
+ ## Migration from v0.9.x
453
377
 
454
- ```typescript
455
- interface DurableServiceConfig extends DurableConnectionConfig {
456
- username: string;
457
- privateKey: string;
458
- serviceFqn: string;
459
- isPublic?: boolean; // default: false
460
- metadata?: Record<string, any>;
461
- ttl?: number; // default: 300000 (5 minutes)
462
- ttlRefreshMargin?: number; // default: 0.2 (refresh at 80%)
463
- poolSize?: number; // default: 1
464
- pollingInterval?: number; // default: 2000 (2 seconds)
465
- }
466
- ```
378
+ v0.11.0+ introduces high-level wrappers, RESTful API changes, and semver-compatible discovery:
467
379
 
468
- ## Examples
380
+ **API Changes:**
381
+ - Server endpoints restructured (`/usernames/*` → `/users/*`)
382
+ - Added `ServiceHost` and `ServiceClient` wrappers
383
+ - Message queue fully implemented
384
+ - Configurable polling with exponential backoff
385
+ - Removed deprecated `cleanup()` methods (use `dispose()`)
386
+ - **v0.11.0+**: Services use `offers` array instead of single `sdp`
387
+ - **v0.11.0+**: Semver-compatible service discovery (chat@1.0.0 matches 1.x.x)
388
+ - **v0.11.0+**: All services are hidden - no listing endpoint
389
+ - **v0.11.0+**: Services support multiple simultaneous offers for connection pooling
469
390
 
470
- ### Chat Application
391
+ **Migration Guide:**
471
392
 
472
393
  ```typescript
473
- // Server
474
- const client = new Rondevu();
475
- await client.register();
476
-
477
- const claim = await client.usernames.claimUsername('alice');
478
- client.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
479
- const keypair = client.usernames.loadKeypairFromStorage('alice');
480
-
481
- const service = await client.exposeService({
482
- username: 'alice',
483
- privateKey: keypair.privateKey,
484
- serviceFqn: 'chat@1.0.0',
485
- isPublic: true,
486
- poolSize: 50, // Handle 50 concurrent users
487
- handler: (channel, connectionId) => {
488
- console.log(`User ${connectionId} joined`);
489
-
490
- channel.on('message', (data) => {
491
- console.log(`[${connectionId}]: ${data}`);
492
- // Broadcast to all users (implement your broadcast logic)
493
- });
494
-
495
- channel.on('close', () => {
496
- console.log(`User ${connectionId} left`);
497
- });
498
- }
499
- });
500
-
501
- await service.start();
502
-
503
- // Client
504
- const client2 = new Rondevu();
505
- await client2.register();
506
-
507
- const connection = await client2.connect('alice', 'chat@1.0.0');
508
- const channel = connection.createChannel('chat');
509
-
510
- channel.on('message', (data) => {
511
- console.log('Message:', data);
512
- });
513
-
514
- await connection.connect();
515
- channel.send('Hello everyone!');
394
+ // Before (v0.9.x) - Manual WebRTC setup
395
+ const signaler = new RondevuSignaler(service, 'chat@1.0.0')
396
+ const context = new WebRTCContext()
397
+ const pc = context.createPeerConnection()
398
+ // ... 50+ lines of boilerplate
399
+
400
+ // After (v0.11.0) - ServiceHost wrapper
401
+ const host = new ServiceHost({
402
+ service: 'chat@1.0.0',
403
+ rondevuService: service
404
+ })
405
+ await host.start()
406
+ // Done!
516
407
  ```
517
408
 
518
- ### File Transfer with Progress
519
-
520
- ```typescript
521
- // Server
522
- const service = await client.exposeService({
523
- username: 'alice',
524
- privateKey: keypair.privateKey,
525
- serviceFqn: 'files@1.0.0',
526
- handler: (channel, connectionId) => {
527
- channel.on('message', async (data) => {
528
- const request = JSON.parse(data);
529
-
530
- if (request.action === 'download') {
531
- const file = await fs.readFile(request.path);
532
- const chunkSize = 16 * 1024; // 16KB chunks
533
-
534
- for (let i = 0; i < file.byteLength; i += chunkSize) {
535
- const chunk = file.slice(i, i + chunkSize);
536
- channel.send(chunk);
537
-
538
- // Wait for buffer to drain if needed
539
- while (channel.bufferedAmount > 16 * 1024 * 1024) { // 16MB
540
- await new Promise(resolve => setTimeout(resolve, 100));
541
- }
542
- }
543
-
544
- channel.send(JSON.stringify({ done: true }));
545
- }
546
- });
547
- }
548
- });
549
-
550
- await service.start();
551
-
552
- // Client
553
- const connection = await client.connect('alice', 'files@1.0.0');
554
- const channel = connection.createChannel('files');
555
-
556
- const chunks = [];
557
- channel.on('message', (data) => {
558
- if (typeof data === 'string') {
559
- const msg = JSON.parse(data);
560
- if (msg.done) {
561
- const blob = new Blob(chunks);
562
- console.log('Download complete:', blob.size, 'bytes');
563
- }
564
- } else {
565
- chunks.push(data);
566
- console.log('Progress:', chunks.length * 16 * 1024, 'bytes');
567
- }
568
- });
569
-
570
- await connection.connect();
571
- channel.send(JSON.stringify({ action: 'download', path: '/file.zip' }));
572
- ```
573
-
574
- ## Platform-Specific Setup
409
+ ## Platform Support
575
410
 
576
411
  ### Modern Browsers
577
412
  Works out of the box - no additional setup needed.
578
413
 
579
414
  ### Node.js 18+
580
- Native fetch is available, but you need WebRTC polyfills:
415
+ Native fetch is available, but WebRTC requires polyfills:
581
416
 
582
417
  ```bash
583
418
  npm install wrtc
584
419
  ```
585
420
 
586
421
  ```typescript
587
- import { Rondevu } from '@xtr-dev/rondevu-client';
588
- import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
589
-
590
- const client = new Rondevu({
591
- baseUrl: 'https://api.ronde.vu',
592
- RTCPeerConnection,
593
- RTCSessionDescription,
594
- RTCIceCandidate
595
- });
596
- ```
597
-
598
- ### Node.js < 18
599
- Install both fetch and WebRTC polyfills:
600
-
601
- ```bash
602
- npm install node-fetch wrtc
603
- ```
604
-
605
- ```typescript
606
- import { Rondevu } from '@xtr-dev/rondevu-client';
607
- import fetch from 'node-fetch';
608
- import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
609
-
610
- const client = new Rondevu({
611
- baseUrl: 'https://api.ronde.vu',
612
- fetch: fetch as any,
613
- RTCPeerConnection,
614
- RTCSessionDescription,
615
- RTCIceCandidate
616
- });
422
+ import { WebRTCContext } from '@xtr-dev/rondevu-client'
423
+ import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'
424
+
425
+ // Configure WebRTC context
426
+ const context = new WebRTCContext({
427
+ RTCPeerConnection,
428
+ RTCSessionDescription,
429
+ RTCIceCandidate
430
+ } as any)
617
431
  ```
618
432
 
619
433
  ## TypeScript
@@ -622,38 +436,23 @@ All types are exported:
622
436
 
623
437
  ```typescript
624
438
  import type {
625
- // Client types
626
- Credentials,
627
- RondevuOptions,
628
-
629
- // Username types
630
- UsernameCheckResult,
631
- UsernameClaimResult,
632
-
633
- // Durable connection types
634
- DurableConnectionState,
635
- DurableChannelState,
636
- DurableConnectionConfig,
637
- DurableChannelConfig,
638
- DurableServiceConfig,
639
- QueuedMessage,
640
- DurableConnectionEvents,
641
- DurableChannelEvents,
642
- DurableServiceEvents,
643
- ConnectionInfo,
644
- ServiceInfo
645
- } from '@xtr-dev/rondevu-client';
439
+ RondevuServiceOptions,
440
+ ServiceHostOptions,
441
+ ServiceHostEvents,
442
+ ServiceClientOptions,
443
+ ServiceClientEvents,
444
+ ConnectionInterface,
445
+ ConnectionEvents,
446
+ ConnectionStates,
447
+ Message,
448
+ QueueMessageOptions,
449
+ Signaler,
450
+ PollingConfig,
451
+ Credentials,
452
+ Keypair
453
+ } from '@xtr-dev/rondevu-client'
646
454
  ```
647
455
 
648
- ## Migration from v0.8.x
649
-
650
- v0.9.0 is a **breaking change** that replaces the low-level APIs with high-level durable connections. See [MIGRATION.md](./MIGRATION.md) for detailed migration guide.
651
-
652
- **Key Changes:**
653
- - ❌ Removed: `client.services.*`, `client.discovery.*`, `client.createPeer()` (low-level APIs)
654
- - ✅ Added: `client.exposeService()`, `client.connect()`, `client.connectByUuid()` (durable APIs)
655
- - ✅ Changed: Focus on durable connections with automatic reconnection and message queuing
656
-
657
456
  ## License
658
457
 
659
458
  MIT