@xtr-dev/rondevu-client 0.8.3 → 0.9.2

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
- 🌐 **DNS-like WebRTC client with username claiming and service discovery**
5
+ 🌐 **WebRTC with durable connections and automatic reconnection**
6
6
 
7
- TypeScript/JavaScript client for Rondevu, providing cryptographic username claiming, service publishing, and privacy-preserving discovery.
7
+ TypeScript/JavaScript client for Rondevu, providing durable WebRTC connections that survive network interruptions with automatic reconnection and message queuing.
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,13 +15,14 @@ TypeScript/JavaScript client for Rondevu, providing cryptographic username claim
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
18
22
  - **Username Claiming**: Cryptographic ownership with Ed25519 signatures
19
23
  - **Service Publishing**: Package-style naming (com.example.chat@1.0.0)
20
- - **Privacy-Preserving Discovery**: UUID-based service index
21
- - **Public/Private Services**: Control service visibility
22
- - **Complete WebRTC Signaling**: Full offer/answer and ICE candidate exchange
23
- - **Trickle ICE**: Send ICE candidates as they're discovered
24
24
  - **TypeScript**: Full type safety and autocomplete
25
+ - **Configurable**: All timeouts, retry limits, and queue sizes are configurable
25
26
 
26
27
  ## Install
27
28
 
@@ -44,36 +45,36 @@ await client.register();
44
45
  const claim = await client.usernames.claimUsername('alice');
45
46
  client.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
46
47
 
47
- console.log(`Username claimed: ${claim.username}`);
48
- console.log(`Expires: ${new Date(claim.expiresAt)}`);
49
-
50
48
  // Step 2: Expose service with handler
51
49
  const keypair = client.usernames.loadKeypairFromStorage('alice');
52
50
 
53
- const handle = await client.services.exposeService({
51
+ const service = await client.exposeService({
54
52
  username: 'alice',
55
53
  privateKey: keypair.privateKey,
56
- serviceFqn: 'com.example.chat@1.0.0',
54
+ serviceFqn: 'chat@1.0.0',
57
55
  isPublic: true,
58
- handler: (channel, peer) => {
59
- console.log('📡 New connection established');
56
+ poolSize: 10, // Handle 10 concurrent connections
57
+ handler: (channel, connectionId) => {
58
+ console.log(`📡 New connection: ${connectionId}`);
60
59
 
61
- channel.onmessage = (e) => {
62
- console.log('📥 Received:', e.data);
63
- channel.send(`Echo: ${e.data}`);
64
- };
60
+ channel.on('message', (data) => {
61
+ console.log('📥 Received:', data);
62
+ channel.send(`Echo: ${data}`);
63
+ });
65
64
 
66
- channel.onopen = () => {
67
- console.log('✅ Data channel open');
68
- };
65
+ channel.on('close', () => {
66
+ console.log(`👋 Connection ${connectionId} closed`);
67
+ });
69
68
  }
70
69
  });
71
70
 
72
- console.log(`Service published with UUID: ${handle.uuid}`);
71
+ // Start the service
72
+ const info = await service.start();
73
+ console.log(`Service published with UUID: ${info.uuid}`);
73
74
  console.log('Waiting for connections...');
74
75
 
75
- // Later: unpublish
76
- await handle.unpublish();
76
+ // Later: stop the service
77
+ await service.stop();
77
78
  ```
78
79
 
79
80
  ### Connecting to a Service (Bob)
@@ -85,46 +86,75 @@ import { Rondevu } from '@xtr-dev/rondevu-client';
85
86
  const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
86
87
  await client.register();
87
88
 
88
- // Option 1: Connect by username + FQN
89
- const { peer, channel } = await client.discovery.connect(
90
- 'alice',
91
- 'com.example.chat@1.0.0'
92
- );
89
+ // Connect to Alice's service
90
+ const connection = await client.connect('alice', 'chat@1.0.0', {
91
+ maxReconnectAttempts: 5
92
+ });
93
+
94
+ // Create a durable channel
95
+ const channel = connection.createChannel('main');
93
96
 
94
- channel.onmessage = (e) => {
95
- console.log('📥 Received:', e.data);
96
- };
97
+ channel.on('message', (data) => {
98
+ console.log('📥 Received:', data);
99
+ });
97
100
 
98
- channel.onopen = () => {
99
- console.log('✅ Connected!');
101
+ channel.on('open', () => {
102
+ console.log('✅ Channel open');
100
103
  channel.send('Hello Alice!');
101
- };
104
+ });
102
105
 
103
- peer.on('connected', () => {
104
- console.log('🎉 WebRTC connection established');
106
+ // Listen for connection events
107
+ connection.on('connected', () => {
108
+ console.log('🎉 Connected to Alice');
105
109
  });
106
110
 
107
- peer.on('failed', (error) => {
108
- console.error('❌ Connection failed:', error);
111
+ connection.on('reconnecting', (attempt, max, delay) => {
112
+ console.log(`🔄 Reconnecting... (${attempt}/${max}, retry in ${delay}ms)`);
109
113
  });
110
114
 
111
- // Option 2: List services first, then connect
112
- const services = await client.discovery.listServices('alice');
113
- console.log(`Found ${services.services.length} services`);
115
+ connection.on('disconnected', () => {
116
+ console.log('🔌 Disconnected');
117
+ });
114
118
 
115
- for (const service of services.services) {
116
- console.log(`- UUID: ${service.uuid}`);
117
- if (service.isPublic) {
118
- console.log(` FQN: ${service.serviceFqn}`);
119
- }
120
- }
119
+ connection.on('failed', (error) => {
120
+ console.error('❌ Connection failed permanently:', error);
121
+ });
121
122
 
122
- // Connect by UUID
123
- const { peer: peer2, channel: channel2 } = await client.discovery.connectByUuid(
124
- services.services[0].uuid
125
- );
123
+ // Establish the connection
124
+ await connection.connect();
125
+
126
+ // Messages sent during disconnection are automatically queued
127
+ channel.send('This will be queued if disconnected');
128
+
129
+ // Later: close the connection
130
+ await connection.close();
126
131
  ```
127
132
 
133
+ ## Core Concepts
134
+
135
+ ### DurableConnection
136
+
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
142
+
143
+ ### DurableChannel
144
+
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
150
+
151
+ ### DurableService
152
+
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
157
+
128
158
  ## API Reference
129
159
 
130
160
  ### Main Client
@@ -161,39 +191,12 @@ const check = await client.usernames.checkUsername('alice');
161
191
  const claim = await client.usernames.claimUsername('alice');
162
192
  // { username, publicKey, privateKey, claimedAt, expiresAt }
163
193
 
164
- // Claim with existing keypair
165
- const keypair = await client.usernames.generateKeypair();
166
- const claim2 = await client.usernames.claimUsername('bob', keypair);
167
-
168
194
  // Save keypair to localStorage
169
- client.usernames.saveKeypairToStorage('alice', publicKey, privateKey);
195
+ client.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
170
196
 
171
197
  // Load keypair from localStorage
172
- const stored = client.usernames.loadKeypairFromStorage('alice');
198
+ const keypair = client.usernames.loadKeypairFromStorage('alice');
173
199
  // { publicKey, privateKey } | null
174
-
175
- // Export keypair for backup
176
- const exported = client.usernames.exportKeypair('alice');
177
- // { username, publicKey, privateKey }
178
-
179
- // Import keypair from backup
180
- client.usernames.importKeypair({ username: 'alice', publicKey, privateKey });
181
-
182
- // Low-level: Generate keypair
183
- const { publicKey, privateKey } = await client.usernames.generateKeypair();
184
-
185
- // Low-level: Sign message
186
- const signature = await client.usernames.signMessage(
187
- 'claim:alice:1234567890',
188
- privateKey
189
- );
190
-
191
- // Low-level: Verify signature
192
- const valid = await client.usernames.verifySignature(
193
- 'claim:alice:1234567890',
194
- signature,
195
- publicKey
196
- );
197
200
  ```
198
201
 
199
202
  **Username Rules:**
@@ -203,446 +206,413 @@ const valid = await client.usernames.verifySignature(
203
206
  - Validity: 365 days from claim/last use
204
207
  - Ownership: Secured by Ed25519 public key
205
208
 
206
- ### Services API
209
+ ### Durable Service API
207
210
 
208
211
  ```typescript
209
- // Publish service (returns UUID)
210
- const service = await client.services.publishService({
212
+ // Expose a durable service
213
+ const service = await client.exposeService({
211
214
  username: 'alice',
212
215
  privateKey: keypair.privateKey,
213
- serviceFqn: 'com.example.chat@1.0.0',
214
- isPublic: false, // optional, default false
215
- metadata: { description: '...' }, // optional
216
- ttl: 5 * 60 * 1000, // optional, default 5 minutes
217
- rtcConfig: { ... } // optional RTCConfiguration
218
- });
219
- // { serviceId, uuid, offerId, expiresAt }
220
-
221
- console.log(`Service UUID: ${service.uuid}`);
222
- console.log('Share this UUID to allow connections');
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
+ },
223
245
 
224
- // Expose service with automatic connection handling
225
- const handle = await client.services.exposeService({
226
- username: 'alice',
227
- privateKey: keypair.privateKey,
228
- serviceFqn: 'com.example.echo@1.0.0',
229
- isPublic: true,
230
- handler: (channel, peer) => {
231
- channel.onmessage = (e) => {
232
- console.log('Received:', e.data);
233
- channel.send(`Echo: ${e.data}`);
234
- };
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
+ });
235
253
  }
236
254
  });
237
255
 
238
- // Later: unpublish
239
- await handle.unpublish();
256
+ // Start the service
257
+ const info = await service.start();
258
+ // { serviceId: '...', uuid: '...', expiresAt: 1234567890 }
240
259
 
241
- // Unpublish service manually
242
- await client.services.unpublishService(serviceId, username);
243
- ```
260
+ // Get active connections
261
+ const connections = service.getActiveConnections();
262
+ // ['conn-123', 'conn-456']
244
263
 
245
- #### Multi-Connection Service Hosting (Offer Pooling)
264
+ // Get service info
265
+ const serviceInfo = service.getServiceInfo();
266
+ // { serviceId: '...', uuid: '...', expiresAt: 1234567890 } | null
246
267
 
247
- By default, `exposeService()` creates a single offer and can only accept one connection. To handle multiple concurrent connections, use the `poolSize` option to enable **offer pooling**:
268
+ // Stop the service
269
+ await service.stop();
270
+ ```
248
271
 
272
+ **Service Events:**
249
273
  ```typescript
250
- // Expose service with offer pooling for multiple concurrent connections
251
- const handle = await client.services.exposeService({
252
- username: 'alice',
253
- privateKey: keypair.privateKey,
254
- serviceFqn: 'com.example.chat@1.0.0',
255
- isPublic: true,
256
- poolSize: 5, // Maintain 5 simultaneous open offers
257
- pollingInterval: 2000, // Optional: polling interval in ms (default: 2000)
258
- handler: (channel, peer, connectionId) => {
259
- console.log(`📡 New connection: ${connectionId}`);
274
+ service.on('published', (serviceId, uuid) => {
275
+ console.log(`Service published: ${uuid}`);
276
+ });
260
277
 
261
- channel.onmessage = (e) => {
262
- console.log(`📥 [${connectionId}] Received:`, e.data);
263
- channel.send(`Echo: ${e.data}`);
264
- };
278
+ service.on('connection', (connectionId) => {
279
+ console.log(`New connection: ${connectionId}`);
280
+ });
265
281
 
266
- channel.onclose = () => {
267
- console.log(`👋 [${connectionId}] Connection closed`);
268
- };
269
- },
270
- onPoolStatus: (status) => {
271
- console.log('Pool status:', {
272
- activeOffers: status.activeOffers,
273
- activeConnections: status.activeConnections,
274
- totalHandled: status.totalConnectionsHandled
275
- });
276
- },
277
- onError: (error, context) => {
278
- console.error(`Pool error (${context}):`, error);
279
- }
282
+ service.on('disconnection', (connectionId) => {
283
+ console.log(`Connection closed: ${connectionId}`);
280
284
  });
281
285
 
282
- // Get current pool status
283
- const status = handle.getStatus();
284
- console.log(`Active offers: ${status.activeOffers}`);
285
- console.log(`Active connections: ${status.activeConnections}`);
286
+ service.on('ttl-refreshed', (expiresAt) => {
287
+ console.log(`TTL refreshed, expires at: ${new Date(expiresAt)}`);
288
+ });
286
289
 
287
- // Manually add more offers if needed
288
- await handle.addOffers(3);
289
- ```
290
+ service.on('error', (error, context) => {
291
+ console.error(`Service error (${context}):`, error);
292
+ });
290
293
 
291
- **How Offer Pooling Works:**
292
- 1. The pool maintains `poolSize` simultaneous open offers at all times
293
- 2. When an offer is answered (connection established), a new offer is automatically created
294
- 3. Polling checks for answers every `pollingInterval` milliseconds (default: 2000ms)
295
- 4. Each connection gets a unique `connectionId` passed to the handler
296
- 5. No limit on total concurrent connections - only pool size (open offers) is controlled
297
-
298
- **Use Cases:**
299
- - Chat servers handling multiple clients
300
- - File sharing services with concurrent downloads
301
- - Multiplayer game lobbies
302
- - Collaborative editing sessions
303
- - Any service that needs to accept multiple simultaneous connections
304
-
305
- **Pool Status Interface:**
306
- ```typescript
307
- interface PoolStatus {
308
- activeOffers: number; // Current number of open offers
309
- activeConnections: number; // Current number of connected peers
310
- totalConnectionsHandled: number; // Total connections since start
311
- failedOfferCreations: number; // Failed offer creation attempts
312
- }
294
+ service.on('closed', () => {
295
+ console.log('Service stopped');
296
+ });
313
297
  ```
314
298
 
315
- **Pooled Service Handle:**
299
+ ### Durable Connection API
300
+
316
301
  ```typescript
317
- interface PooledServiceHandle extends ServiceHandle {
318
- getStatus: () => PoolStatus; // Get current pool status
319
- addOffers: (count: number) => Promise<void>; // Manually add offers
320
- }
321
- ```
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
322
 
323
- **Service FQN Format:**
324
- - Service name: Reverse domain notation (e.g., `com.example.chat`)
325
- - Version: Semantic versioning (e.g., `1.0.0`, `2.1.3-beta`)
326
- - Complete FQN: `service-name@version`
327
- - Examples: `com.example.chat@1.0.0`, `io.github.alice.notes@0.1.0-beta`
323
+ // Connect by UUID
324
+ const connection2 = await client.connectByUuid('service-uuid-here', {
325
+ maxReconnectAttempts: 5
326
+ });
328
327
 
329
- **Validation Rules:**
330
- - Service name pattern: `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$`
331
- - Length: 3-128 characters
332
- - Minimum 2 components (at least one dot)
333
- - Version pattern: `^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$`
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
334
 
335
- ### Discovery API
335
+ // Get existing channel
336
+ const existingChannel = connection.getChannel('main');
336
337
 
337
- ```typescript
338
- // List all services for a username
339
- const services = await client.discovery.listServices('alice');
340
- // {
341
- // username: 'alice',
342
- // services: [
343
- // { uuid: 'abc123', isPublic: false },
344
- // { uuid: 'def456', isPublic: true, serviceFqn: '...', metadata: {...} }
345
- // ]
346
- // }
347
-
348
- // Query service by FQN
349
- const query = await client.discovery.queryService('alice', 'com.example.chat@1.0.0');
350
- // { uuid: 'abc123', allowed: true }
351
-
352
- // Get service details by UUID
353
- const details = await client.discovery.getServiceDetails('abc123');
354
- // { serviceId, username, serviceFqn, offerId, sdp, isPublic, metadata, ... }
355
-
356
- // Connect to service by UUID
357
- const peer = await client.discovery.connectToService('abc123', {
358
- rtcConfig: { ... }, // optional
359
- onConnected: () => { ... }, // optional
360
- onData: (data) => { ... } // optional
361
- });
362
-
363
- // Connect by username + FQN (convenience method)
364
- const { peer, channel } = await client.discovery.connect(
365
- 'alice',
366
- 'com.example.chat@1.0.0',
367
- { rtcConfig: { ... } } // optional
368
- );
369
-
370
- // Connect by UUID with channel
371
- const { peer, channel } = await client.discovery.connectByUuid('abc123');
372
- ```
338
+ // Check connection state
339
+ const state = connection.getState();
340
+ // 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed' | 'closed'
373
341
 
374
- ### Low-Level Peer Connection
342
+ const isConnected = connection.isConnected();
375
343
 
376
- ```typescript
377
- // Create peer connection
378
- const peer = client.createPeer({
379
- iceServers: [
380
- { urls: 'stun:stun.l.google.com:19302' },
381
- {
382
- urls: 'turn:turn.example.com:3478',
383
- username: 'user',
384
- credential: 'pass'
385
- }
386
- ],
387
- iceTransportPolicy: 'relay' // optional: force TURN relay
388
- });
344
+ // Connect
345
+ await connection.connect();
389
346
 
390
- // Event listeners
391
- peer.on('state', (state) => {
392
- console.log('Peer state:', state);
393
- });
347
+ // Close connection
348
+ await connection.close();
349
+ ```
394
350
 
395
- peer.on('connected', () => {
396
- console.log('✅ Connected');
351
+ **Connection Events:**
352
+ ```typescript
353
+ connection.on('state', (newState, previousState) => {
354
+ console.log(`State: ${previousState} → ${newState}`);
397
355
  });
398
356
 
399
- peer.on('disconnected', () => {
400
- console.log('🔌 Disconnected');
357
+ connection.on('connected', () => {
358
+ console.log('Connected');
401
359
  });
402
360
 
403
- peer.on('failed', (error) => {
404
- console.error('❌ Failed:', error);
361
+ connection.on('reconnecting', (attempt, maxAttempts, delay) => {
362
+ console.log(`Reconnecting (${attempt}/${maxAttempts}) in ${delay}ms`);
405
363
  });
406
364
 
407
- peer.on('datachannel', (channel) => {
408
- console.log('📡 Data channel ready');
365
+ connection.on('disconnected', () => {
366
+ console.log('Disconnected');
409
367
  });
410
368
 
411
- peer.on('track', (event) => {
412
- // Media track received
413
- const stream = event.streams[0];
414
- videoElement.srcObject = stream;
369
+ connection.on('failed', (error, permanent) => {
370
+ console.error('Connection failed:', error, 'Permanent:', permanent);
415
371
  });
416
372
 
417
- // Create offer
418
- const offerId = await peer.createOffer({
419
- ttl: 300000, // optional
420
- timeouts: { // optional
421
- iceGathering: 10000,
422
- waitingForAnswer: 30000,
423
- creatingAnswer: 10000,
424
- iceConnection: 30000
425
- }
373
+ connection.on('closed', () => {
374
+ console.log('Connection closed');
426
375
  });
376
+ ```
427
377
 
428
- // Answer offer
429
- await peer.answer(offerId, sdp);
378
+ ### Durable Channel API
430
379
 
431
- // Add media tracks
432
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
433
- stream.getTracks().forEach(track => {
434
- peer.addTrack(track, stream);
380
+ ```typescript
381
+ const channel = connection.createChannel('chat', {
382
+ ordered: true, // optional, default: true
383
+ maxRetransmits: undefined // optional, for unordered channels
435
384
  });
436
385
 
437
- // Close connection
438
- await peer.close();
386
+ // Send data (queued if disconnected)
387
+ channel.send('Hello!');
388
+ channel.send(new ArrayBuffer(1024));
389
+ channel.send(new Blob(['data']));
439
390
 
440
- // Properties
441
- peer.stateName; // 'idle', 'creating-offer', 'connected', etc.
442
- peer.connectionState; // RTCPeerConnectionState
443
- peer.offerId; // string | undefined
444
- peer.role; // 'offerer' | 'answerer' | undefined
445
- ```
391
+ // Check state
392
+ const state = channel.readyState;
393
+ // 'connecting' | 'open' | 'closing' | 'closed'
446
394
 
447
- ## Connection Lifecycle
448
-
449
- ### Service Publisher (Offerer)
450
- 1. **idle** - Initial state
451
- 2. **creating-offer** - Creating WebRTC offer
452
- 3. **waiting-for-answer** - Polling for answer from peer
453
- 4. **exchanging-ice** - Exchanging ICE candidates
454
- 5. **connected** - Successfully connected
455
- 6. **failed** - Connection failed
456
- 7. **closed** - Connection closed
457
-
458
- ### Service Consumer (Answerer)
459
- 1. **idle** - Initial state
460
- 2. **answering** - Creating WebRTC answer
461
- 3. **exchanging-ice** - Exchanging ICE candidates
462
- 4. **connected** - Successfully connected
463
- 5. **failed** - Connection failed
464
- 6. **closed** - Connection closed
395
+ // Get buffered amount
396
+ const buffered = channel.bufferedAmount;
465
397
 
466
- ## Platform-Specific Setup
398
+ // Set buffered amount low threshold
399
+ channel.bufferedAmountLowThreshold = 16 * 1024; // 16KB
467
400
 
468
- ### Modern Browsers
469
- Works out of the box - no additional setup needed.
470
-
471
- ### Node.js 18+
472
- Native fetch is available, but you need WebRTC polyfills:
401
+ // Get queue size (for debugging)
402
+ const queueSize = channel.getQueueSize();
473
403
 
474
- ```bash
475
- npm install wrtc
404
+ // Close channel
405
+ channel.close();
476
406
  ```
477
407
 
408
+ **Channel Events:**
478
409
  ```typescript
479
- import { Rondevu } from '@xtr-dev/rondevu-client';
480
- import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
481
-
482
- const client = new Rondevu({
483
- baseUrl: 'https://api.ronde.vu',
484
- RTCPeerConnection,
485
- RTCSessionDescription,
486
- RTCIceCandidate
410
+ channel.on('open', () => {
411
+ console.log('Channel open');
487
412
  });
488
- ```
489
-
490
- ### Node.js < 18
491
- Install both fetch and WebRTC polyfills:
492
413
 
493
- ```bash
494
- npm install node-fetch wrtc
495
- ```
414
+ channel.on('message', (data) => {
415
+ console.log('Received:', data);
416
+ });
496
417
 
497
- ```typescript
498
- import { Rondevu } from '@xtr-dev/rondevu-client';
499
- import fetch from 'node-fetch';
500
- import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
418
+ channel.on('error', (error) => {
419
+ console.error('Channel error:', error);
420
+ });
501
421
 
502
- const client = new Rondevu({
503
- baseUrl: 'https://api.ronde.vu',
504
- fetch: fetch as any,
505
- RTCPeerConnection,
506
- RTCSessionDescription,
507
- RTCIceCandidate
422
+ channel.on('close', () => {
423
+ console.log('Channel closed');
508
424
  });
509
- ```
510
425
 
511
- ### Deno
512
- ```typescript
513
- import { Rondevu } from 'npm:@xtr-dev/rondevu-client';
426
+ channel.on('bufferedAmountLow', () => {
427
+ console.log('Buffer drained, safe to send more');
428
+ });
514
429
 
515
- const client = new Rondevu({
516
- baseUrl: 'https://api.ronde.vu'
430
+ channel.on('queueOverflow', (droppedCount) => {
431
+ console.warn(`Queue overflow: ${droppedCount} messages dropped`);
517
432
  });
518
433
  ```
519
434
 
520
- ### Bun
521
- Works out of the box - no additional setup needed.
435
+ ## Configuration Options
436
+
437
+ ### Connection Configuration
522
438
 
523
- ### Cloudflare Workers
524
439
  ```typescript
525
- import { Rondevu } from '@xtr-dev/rondevu-client';
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
+ }
450
+ ```
526
451
 
527
- export default {
528
- async fetch(request: Request, env: Env) {
529
- const client = new Rondevu({
530
- baseUrl: 'https://api.ronde.vu'
531
- });
452
+ ### Service Configuration
532
453
 
533
- const creds = await client.register();
534
- return new Response(JSON.stringify(creds));
535
- }
536
- };
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
+ }
537
466
  ```
538
467
 
539
468
  ## Examples
540
469
 
541
- ### Echo Service
470
+ ### Chat Application
542
471
 
543
472
  ```typescript
544
- // Publisher
545
- const client1 = new Rondevu();
546
- await client1.register();
547
-
548
- const claim = await client1.usernames.claimUsername('alice');
549
- client1.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
473
+ // Server
474
+ const client = new Rondevu();
475
+ await client.register();
550
476
 
551
- const keypair = client1.usernames.loadKeypairFromStorage('alice');
477
+ const claim = await client.usernames.claimUsername('alice');
478
+ client.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
479
+ const keypair = client.usernames.loadKeypairFromStorage('alice');
552
480
 
553
- await client1.services.exposeService({
481
+ const service = await client.exposeService({
554
482
  username: 'alice',
555
483
  privateKey: keypair.privateKey,
556
- serviceFqn: 'com.example.echo@1.0.0',
484
+ serviceFqn: 'chat@1.0.0',
557
485
  isPublic: true,
558
- handler: (channel, peer) => {
559
- channel.onmessage = (e) => {
560
- console.log('Received:', e.data);
561
- channel.send(`Echo: ${e.data}`);
562
- };
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
+ });
563
498
  }
564
499
  });
565
500
 
566
- // Consumer
501
+ await service.start();
502
+
503
+ // Client
567
504
  const client2 = new Rondevu();
568
505
  await client2.register();
569
506
 
570
- const { peer, channel } = await client2.discovery.connect(
571
- 'alice',
572
- 'com.example.echo@1.0.0'
573
- );
507
+ const connection = await client2.connect('alice', 'chat@1.0.0');
508
+ const channel = connection.createChannel('chat');
574
509
 
575
- channel.onmessage = (e) => console.log('Received:', e.data);
576
- channel.send('Hello!');
510
+ channel.on('message', (data) => {
511
+ console.log('Message:', data);
512
+ });
513
+
514
+ await connection.connect();
515
+ channel.send('Hello everyone!');
577
516
  ```
578
517
 
579
- ### File Transfer Service
518
+ ### File Transfer with Progress
580
519
 
581
520
  ```typescript
582
- // Publisher
583
- await client.services.exposeService({
521
+ // Server
522
+ const service = await client.exposeService({
584
523
  username: 'alice',
585
524
  privateKey: keypair.privateKey,
586
- serviceFqn: 'com.example.files@1.0.0',
587
- isPublic: false,
588
- handler: (channel, peer) => {
589
- channel.binaryType = 'arraybuffer';
590
-
591
- channel.onmessage = (e) => {
592
- if (typeof e.data === 'string') {
593
- console.log('Request:', JSON.parse(e.data));
594
- } else {
595
- console.log('Received file chunk:', e.data.byteLength, 'bytes');
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 }));
596
545
  }
597
- };
546
+ });
598
547
  }
599
548
  });
600
549
 
601
- // Consumer
602
- const { peer, channel } = await client.discovery.connect(
603
- 'alice',
604
- 'com.example.files@1.0.0'
605
- );
550
+ await service.start();
606
551
 
607
- channel.binaryType = 'arraybuffer';
552
+ // Client
553
+ const connection = await client.connect('alice', 'files@1.0.0');
554
+ const channel = connection.createChannel('files');
608
555
 
609
- // Request file
610
- channel.send(JSON.stringify({ action: 'get', path: '/readme.txt' }));
611
-
612
- channel.onmessage = (e) => {
613
- if (e.data instanceof ArrayBuffer) {
614
- console.log('Received file:', e.data.byteLength, 'bytes');
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');
615
567
  }
616
- };
568
+ });
569
+
570
+ await connection.connect();
571
+ channel.send(JSON.stringify({ action: 'download', path: '/file.zip' }));
617
572
  ```
618
573
 
619
- ### Video Chat Service
574
+ ## Platform-Specific Setup
620
575
 
621
- ```typescript
622
- // Publisher
623
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
576
+ ### Modern Browsers
577
+ Works out of the box - no additional setup needed.
624
578
 
625
- const peer = client.createPeer();
626
- stream.getTracks().forEach(track => peer.addTrack(track, stream));
579
+ ### Node.js 18+
580
+ Native fetch is available, but you need WebRTC polyfills:
627
581
 
628
- const offerId = await peer.createOffer({ ttl: 300000 });
582
+ ```bash
583
+ npm install wrtc
584
+ ```
629
585
 
630
- await client.services.publishService({
631
- username: 'alice',
632
- privateKey: keypair.privateKey,
633
- serviceFqn: 'com.example.videochat@1.0.0',
634
- isPublic: true
586
+ ```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
635
595
  });
596
+ ```
597
+
598
+ ### Node.js < 18
599
+ Install both fetch and WebRTC polyfills:
636
600
 
637
- // Consumer
638
- const { peer, channel } = await client.discovery.connect(
639
- 'alice',
640
- 'com.example.videochat@1.0.0'
641
- );
601
+ ```bash
602
+ npm install node-fetch wrtc
603
+ ```
642
604
 
643
- peer.on('track', (event) => {
644
- const remoteStream = event.streams[0];
645
- videoElement.srcObject = remoteStream;
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
646
616
  });
647
617
  ```
648
618
 
@@ -652,41 +622,37 @@ All types are exported:
652
622
 
653
623
  ```typescript
654
624
  import type {
625
+ // Client types
655
626
  Credentials,
656
627
  RondevuOptions,
657
628
 
658
629
  // Username types
659
630
  UsernameCheckResult,
660
631
  UsernameClaimResult,
661
- Keypair,
662
-
663
- // Service types
664
- ServicePublishResult,
665
- PublishServiceOptions,
666
- ServiceHandle,
667
-
668
- // Discovery types
669
- ServiceInfo,
670
- ServiceListResult,
671
- ServiceQueryResult,
672
- ServiceDetails,
673
- ConnectResult,
674
-
675
- // Peer types
676
- PeerOptions,
677
- PeerEvents,
678
- PeerTimeouts
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
679
645
  } from '@xtr-dev/rondevu-client';
680
646
  ```
681
647
 
682
- ## Migration from V1
648
+ ## Migration from v0.8.x
683
649
 
684
- V2 is a **breaking change** that replaces topic-based discovery with username claiming and service publishing. See the main [MIGRATION.md](../MIGRATION.md) for detailed migration guide.
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.
685
651
 
686
652
  **Key Changes:**
687
- - ❌ Removed: `offers.findByTopic()`, `offers.getTopics()`, bloom filters
688
- - ✅ Added: `usernames.*`, `services.*`, `discovery.*` APIs
689
- - ✅ Changed: Focus on service-based discovery instead of topics
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
690
656
 
691
657
  ## License
692
658