@xtr-dev/rondevu-client 0.18.9 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +324 -47
  2. package/dist/{api.d.ts → api/client.d.ts} +17 -8
  3. package/dist/{api.js → api/client.js} +114 -81
  4. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +13 -5
  5. package/dist/{answerer-connection.js → connections/answerer.js} +17 -32
  6. package/dist/{connection.d.ts → connections/base.d.ts} +26 -5
  7. package/dist/{connection.js → connections/base.js} +45 -4
  8. package/dist/{offerer-connection.d.ts → connections/offerer.d.ts} +30 -5
  9. package/dist/{offerer-connection.js → connections/offerer.js} +93 -32
  10. package/dist/core/index.d.ts +22 -0
  11. package/dist/core/index.js +17 -0
  12. package/dist/core/offer-pool.d.ts +94 -0
  13. package/dist/core/offer-pool.js +267 -0
  14. package/dist/{rondevu.d.ts → core/rondevu.d.ts} +7 -28
  15. package/dist/{rondevu.js → core/rondevu.js} +32 -175
  16. package/dist/{node-crypto-adapter.d.ts → crypto/node.d.ts} +1 -1
  17. package/dist/{web-crypto-adapter.d.ts → crypto/web.d.ts} +1 -1
  18. package/dist/utils/async-lock.d.ts +42 -0
  19. package/dist/utils/async-lock.js +75 -0
  20. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  21. package/package.json +3 -3
  22. package/dist/index.d.ts +0 -22
  23. package/dist/index.js +0 -17
  24. package/dist/rondevu-signaler.d.ts +0 -112
  25. package/dist/rondevu-signaler.js +0 -401
  26. /package/dist/{rpc-batcher.d.ts → api/batcher.d.ts} +0 -0
  27. /package/dist/{rpc-batcher.js → api/batcher.js} +0 -0
  28. /package/dist/{connection-config.d.ts → connections/config.d.ts} +0 -0
  29. /package/dist/{connection-config.js → connections/config.js} +0 -0
  30. /package/dist/{connection-events.d.ts → connections/events.d.ts} +0 -0
  31. /package/dist/{connection-events.js → connections/events.js} +0 -0
  32. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  33. /package/dist/{types.js → core/types.js} +0 -0
  34. /package/dist/{crypto-adapter.d.ts → crypto/adapter.d.ts} +0 -0
  35. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  36. /package/dist/{node-crypto-adapter.js → crypto/node.js} +0 -0
  37. /package/dist/{web-crypto-adapter.js → crypto/web.js} +0 -0
  38. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  39. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
  40. /package/dist/{message-buffer.js → utils/message-buffer.js} +0 -0
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
- 🌐 **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.
7
+ TypeScript/JavaScript client for Rondevu, providing WebRTC signaling with **automatic reconnection**, **message buffering**, username claiming, service publishing/discovery, and efficient batch polling.
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,15 +15,24 @@ TypeScript/JavaScript client for Rondevu, providing WebRTC signaling with userna
15
15
 
16
16
  ## Features
17
17
 
18
+ ### ✨ New in v0.20.0
19
+ - **🔄 Automatic Reconnection**: Built-in exponential backoff for failed connections
20
+ - **📦 Message Buffering**: Queues messages during disconnections, replays on reconnect
21
+ - **🔄 Connection Persistence**: OffererConnection objects persist across disconnections via offer rotation
22
+ - **📊 Connection State Machine**: Explicit lifecycle tracking with native RTC events
23
+ - **🎯 Rich Event System**: 20+ events for monitoring connection health including `connection:rotated`
24
+ - **⚡ Improved Reliability**: ICE polling lifecycle management, proper cleanup, rotation fallback
25
+ - **🏗️ Internal Refactoring**: Cleaner codebase with OfferPool extraction and consolidated ICE polling
26
+
27
+ ### Core Features
18
28
  - **Username Claiming**: Secure ownership with Ed25519 signatures
19
29
  - **Anonymous Users**: Auto-generated anonymous usernames for quick testing
20
30
  - **Service Publishing**: Publish services with multiple offers for connection pooling
21
31
  - **Service Discovery**: Direct lookup, random discovery, or paginated search
22
- - **Efficient Batch Polling**: Single endpoint for answers and ICE candidates (50% fewer requests)
32
+ - **Efficient Batch Polling**: Single endpoint for answers and ICE candidates
23
33
  - **Semantic Version Matching**: Compatible version resolution (chat:1.0.0 matches any 1.x.x)
24
34
  - **TypeScript**: Full type safety and autocomplete
25
35
  - **Keypair Management**: Generate or reuse Ed25519 keypairs
26
- - **Automatic Signatures**: All authenticated requests signed automatically
27
36
 
28
37
  ## Installation
29
38
 
@@ -49,27 +58,35 @@ const rondevu = await Rondevu.connect({
49
58
  await rondevu.publishService({
50
59
  service: 'chat:1.0.0',
51
60
  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 }
61
+ connectionConfig: {
62
+ reconnectEnabled: true, // Auto-reconnect on failures
63
+ bufferEnabled: true, // Buffer messages during disconnections
64
+ connectionTimeout: 30000 // 30 second timeout
68
65
  }
69
66
  })
70
67
 
71
68
  // 3. Start accepting connections
72
69
  await rondevu.startFilling()
70
+
71
+ // 4. Handle incoming connections
72
+ rondevu.on('connection:opened', (offerId, connection) => {
73
+ console.log('New connection:', offerId)
74
+
75
+ // Listen for messages
76
+ connection.on('message', (data) => {
77
+ console.log('Received:', data)
78
+ })
79
+
80
+ // Monitor connection state
81
+ connection.on('connected', () => {
82
+ console.log('Fully connected!')
83
+ connection.send('Hello from Alice!')
84
+ })
85
+
86
+ connection.on('disconnected', () => {
87
+ console.log('Connection lost, will auto-reconnect')
88
+ })
89
+ })
73
90
  ```
74
91
 
75
92
  ### Connecting to a Service (Answerer)
@@ -84,25 +101,38 @@ const rondevu = await Rondevu.connect({
84
101
  iceServers: 'ipv4-turn'
85
102
  })
86
103
 
87
- // 2. Connect to service (automatic WebRTC setup)
104
+ // 2. Connect to service - returns AnswererConnection
88
105
  const connection = await rondevu.connectToService({
89
106
  serviceFqn: 'chat:1.0.0@alice',
90
- onConnection: ({ dc, peerUsername }) => {
91
- console.log('Connected to', peerUsername)
107
+ connectionConfig: {
108
+ reconnectEnabled: true,
109
+ bufferEnabled: true,
110
+ maxReconnectAttempts: 5
111
+ }
112
+ })
92
113
 
93
- dc.addEventListener('message', (e) => {
94
- console.log('Received:', e.data)
95
- })
114
+ // 3. Setup event handlers
115
+ connection.on('connected', () => {
116
+ console.log('Connected to alice!')
117
+ connection.send('Hello from Bob!')
118
+ })
96
119
 
97
- dc.addEventListener('open', () => {
98
- dc.send('Hello from Bob!')
99
- })
100
- }
120
+ connection.on('message', (data) => {
121
+ console.log('Received:', data)
122
+ })
123
+
124
+ // 4. Monitor connection health
125
+ connection.on('reconnecting', (attempt) => {
126
+ console.log(`Reconnecting... attempt ${attempt}`)
101
127
  })
102
128
 
103
- // Access connection
104
- connection.dc.send('Another message')
105
- connection.pc.close() // Close when done
129
+ connection.on('reconnect:success', () => {
130
+ console.log('Back online!')
131
+ })
132
+
133
+ connection.on('failed', (error) => {
134
+ console.error('Connection failed:', error)
135
+ })
106
136
  ```
107
137
 
108
138
  ## Core API
@@ -126,52 +156,299 @@ await rondevu.publishService({
126
156
  service: string, // e.g., 'chat:1.0.0' (username auto-appended)
127
157
  maxOffers: number, // Maximum concurrent offers to maintain
128
158
  offerFactory?: OfferFactory, // Optional: custom offer creation
129
- ttl?: number // Optional: offer lifetime in ms (default: 300000)
159
+ ttl?: number, // Optional: offer lifetime in ms (default: 300000)
160
+ connectionConfig?: Partial<ConnectionConfig> // Optional: durability settings
130
161
  })
131
162
 
132
163
  await rondevu.startFilling() // Start accepting connections
133
164
  rondevu.stopFilling() // Stop and close all connections
134
165
  ```
135
166
 
167
+ ### Connecting to Services
168
+
169
+ **⚠️ Breaking Change in v0.18.9+:** `connectToService()` now returns `AnswererConnection` instead of `ConnectionContext`.
170
+
171
+ ```typescript
172
+ // New API (v0.18.9/v0.18.11+)
173
+ const connection = await rondevu.connectToService({
174
+ serviceFqn?: string, // Full FQN like 'chat:1.0.0@alice'
175
+ service?: string, // Service without username (for discovery)
176
+ username?: string, // Target username (combined with service)
177
+ connectionConfig?: Partial<ConnectionConfig>, // Durability settings
178
+ rtcConfig?: RTCConfiguration // Optional: override ICE servers
179
+ })
180
+
181
+ // Setup event handlers
182
+ connection.on('connected', () => {
183
+ connection.send('Hello!')
184
+ })
185
+
186
+ connection.on('message', (data) => {
187
+ console.log(data)
188
+ })
189
+ ```
190
+
191
+ ### Connection Configuration
192
+
193
+ ```typescript
194
+ interface ConnectionConfig {
195
+ // Timeouts
196
+ connectionTimeout: number // Default: 30000ms (30s)
197
+ iceGatheringTimeout: number // Default: 10000ms (10s)
198
+
199
+ // Reconnection
200
+ reconnectEnabled: boolean // Default: true
201
+ maxReconnectAttempts: number // Default: 5 (0 = infinite)
202
+ reconnectBackoffBase: number // Default: 1000ms
203
+ reconnectBackoffMax: number // Default: 30000ms (30s)
204
+
205
+ // Message buffering
206
+ bufferEnabled: boolean // Default: true
207
+ maxBufferSize: number // Default: 100 messages
208
+ maxBufferAge: number // Default: 60000ms (1 min)
209
+
210
+ // Debug
211
+ debug: boolean // Default: false
212
+ }
213
+ ```
214
+
215
+ ### Connection Events
216
+
217
+ ```typescript
218
+ // Lifecycle events
219
+ connection.on('connecting', () => {})
220
+ connection.on('connected', () => {})
221
+ connection.on('disconnected', (reason) => {})
222
+ connection.on('failed', (error) => {})
223
+ connection.on('closed', (reason) => {})
224
+
225
+ // Reconnection events
226
+ connection.on('reconnecting', (attempt) => {})
227
+ connection.on('reconnect:success', () => {})
228
+ connection.on('reconnect:failed', (error) => {})
229
+ connection.on('reconnect:exhausted', (attempts) => {})
230
+
231
+ // Message events
232
+ connection.on('message', (data) => {})
233
+ connection.on('message:buffered', (data) => {})
234
+ connection.on('message:replayed', (message) => {})
235
+
236
+ // ICE events
237
+ connection.on('ice:connection:state', (state) => {})
238
+ connection.on('ice:polling:started', () => {})
239
+ connection.on('ice:polling:stopped', () => {})
240
+ ```
241
+
136
242
  ### Service Discovery
137
243
 
138
244
  ```typescript
139
- // Direct lookup (with username)
140
- await rondevu.getService('chat:1.0.0@alice')
245
+ // Unified discovery API
246
+ const service = await rondevu.findService(
247
+ 'chat:1.0.0@alice', // Direct lookup (with username)
248
+ { mode: 'direct' }
249
+ )
250
+
251
+ const service = await rondevu.findService(
252
+ 'chat:1.0.0', // Random discovery (without username)
253
+ { mode: 'random' }
254
+ )
255
+
256
+ const result = await rondevu.findService(
257
+ 'chat:1.0.0',
258
+ {
259
+ mode: 'paginated',
260
+ limit: 20,
261
+ offset: 0
262
+ }
263
+ )
264
+ ```
265
+
266
+ ## Migration Guide
267
+
268
+ **Upgrading from v0.18.10 or earlier?** See [MIGRATION.md](./MIGRATION.md) for detailed upgrade instructions.
141
269
 
142
- // Random discovery (without username)
143
- await rondevu.discoverService('chat:1.0.0')
270
+ ### Quick Migration Summary
144
271
 
145
- // Paginated discovery
146
- await rondevu.discoverServices('chat:1.0.0', limit, offset)
272
+ **Before (v0.18.7/v0.18.10):**
273
+ ```typescript
274
+ const context = await rondevu.connectToService({
275
+ serviceFqn: 'chat:1.0.0@alice',
276
+ onConnection: ({ dc }) => {
277
+ dc.addEventListener('message', (e) => console.log(e.data))
278
+ dc.send('Hello')
279
+ }
280
+ })
147
281
  ```
148
282
 
149
- ### Connecting to Services
283
+ **After (v0.18.9/v0.18.11):**
284
+ ```typescript
285
+ const connection = await rondevu.connectToService({
286
+ serviceFqn: 'chat:1.0.0@alice'
287
+ })
288
+
289
+ connection.on('connected', () => {
290
+ connection.send('Hello') // Use connection.send()
291
+ })
292
+
293
+ connection.on('message', (data) => {
294
+ console.log(data) // data is already extracted
295
+ })
296
+ ```
297
+
298
+ ## Advanced Usage
299
+
300
+ ### Custom Offer Factory
301
+
302
+ ```typescript
303
+ await rondevu.publishService({
304
+ service: 'file-transfer:1.0.0',
305
+ maxOffers: 3,
306
+ offerFactory: async (pc) => {
307
+ // Customize data channel settings
308
+ const dc = pc.createDataChannel('files', {
309
+ ordered: true,
310
+ maxRetransmits: 10
311
+ })
312
+
313
+ // Add custom listeners
314
+ dc.addEventListener('open', () => {
315
+ console.log('Transfer channel ready')
316
+ })
317
+
318
+ const offer = await pc.createOffer()
319
+ await pc.setLocalDescription(offer)
320
+ return { dc, offer }
321
+ }
322
+ })
323
+ ```
324
+
325
+ ### Accessing Raw RTCPeerConnection
326
+
327
+ ```typescript
328
+ const connection = await rondevu.connectToService({ ... })
329
+
330
+ // Get raw objects if needed
331
+ const pc = connection.getPeerConnection()
332
+ const dc = connection.getDataChannel()
333
+
334
+ // Note: Using raw DataChannel bypasses buffering/reconnection features
335
+ if (dc) {
336
+ dc.addEventListener('message', (e) => {
337
+ console.log('Raw message:', e.data)
338
+ })
339
+ }
340
+ ```
341
+
342
+ ### Disabling Durability Features
150
343
 
151
344
  ```typescript
152
345
  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
346
+ serviceFqn: 'chat:1.0.0@alice',
347
+ connectionConfig: {
348
+ reconnectEnabled: false, // Disable auto-reconnect
349
+ bufferEnabled: false, // Disable message buffering
350
+ }
158
351
  })
159
352
  ```
160
353
 
161
354
  ## Documentation
162
355
 
356
+ 📚 **[MIGRATION.md](./MIGRATION.md)** - Upgrade guide from v0.18.7 to v0.18.9
357
+
163
358
  📚 **[ADVANCED.md](./ADVANCED.md)** - Comprehensive guide including:
164
359
  - Detailed API reference for all methods
165
360
  - Type definitions and interfaces
166
361
  - Platform support (Browser & Node.js)
167
362
  - Advanced usage patterns
168
363
  - Username rules and service FQN format
169
- - Examples and migration guides
364
+
365
+ ## Connection Persistence (v0.20.0+)
366
+
367
+ Connection objects now persist across disconnections via **"offer rotation"**. When a connection fails, the same connection object is rebound to a new offer instead of being destroyed:
368
+
369
+ ```typescript
370
+ rondevu.on('connection:opened', (offerId, connection) => {
371
+ console.log(`Connection ${offerId} opened`)
372
+
373
+ // Listen for offer rotation
374
+ rondevu.on('connection:rotated', (oldOfferId, newOfferId, conn) => {
375
+ if (conn === connection) {
376
+ console.log(`Connection rotated: ${oldOfferId} → ${newOfferId}`)
377
+ // Same connection object! Event listeners still work
378
+ // Message buffer preserved
379
+ }
380
+ })
381
+
382
+ connection.on('message', (data) => {
383
+ console.log('Received:', data)
384
+ // This listener continues working even after rotation
385
+ })
386
+
387
+ connection.on('failed', () => {
388
+ console.log('Connection failed, will auto-rotate to new offer')
389
+ })
390
+ })
391
+ ```
392
+
393
+ **Benefits:**
394
+ - ✅ Same connection object remains usable through disconnections
395
+ - ✅ Message buffer preserved during temporary disconnections
396
+ - ✅ Event listeners don't need to be re-registered
397
+ - ✅ Seamless reconnection experience for offerer side
170
398
 
171
399
  ## Examples
172
400
 
173
401
  - [React Demo](https://github.com/xtr-dev/rondevu-demo) - Full browser UI ([live](https://ronde.vu))
174
402
 
403
+ ## Changelog
404
+
405
+ ### v0.20.0 (Latest)
406
+ - **Connection Persistence** - OffererConnection objects now persist across disconnections
407
+ - **Offer Rotation** - When connection fails, same object is rebound to new offer
408
+ - **Message Buffering** - Now works seamlessly on offerer side through rotations
409
+ - **New Event**: `connection:rotated` emitted when offer is rotated
410
+ - **Internal**: Added `OffererConnection.rebindToOffer()` method
411
+ - **Internal**: Modified OfferPool failure handler to rotate offers instead of destroying connections
412
+ - **Internal**: Added rotation lock to prevent concurrent rotations
413
+ - **Internal**: Added max rotation attempts limit (default: 5)
414
+ - 100% backward compatible - no breaking changes
415
+
416
+ ### v0.19.0
417
+ - **Internal Refactoring** - Improved codebase maintainability (no API changes)
418
+ - Extract OfferPool class for offer lifecycle management
419
+ - Consolidate ICE polling logic (remove ~86 lines of duplicate code)
420
+ - Add AsyncLock utility for race-free concurrent operations
421
+ - Disable reconnection for offerer connections (offers are ephemeral)
422
+ - 100% backward compatible - upgrade without code changes
423
+
424
+ ### v0.18.11
425
+ - Restore EventEmitter-based durable connections (same as v0.18.9)
426
+ - Durable WebRTC connections with state machine
427
+ - Automatic reconnection with exponential backoff
428
+ - Message buffering during disconnections
429
+ - ICE polling lifecycle management
430
+ - **Breaking:** `connectToService()` returns `AnswererConnection` instead of `ConnectionContext`
431
+ - See [MIGRATION.md](./MIGRATION.md) for upgrade guide
432
+
433
+ ### v0.18.10
434
+ - Temporary revert to callback-based API (reverted in v0.18.11)
435
+
436
+ ### v0.18.9
437
+ - Add durable WebRTC connections with state machine
438
+ - Implement automatic reconnection with exponential backoff
439
+ - Add message buffering during disconnections
440
+ - Fix ICE polling lifecycle (stops when connected)
441
+ - Add fillOffers() semaphore to prevent exceeding maxOffers
442
+ - **Breaking:** `connectToService()` returns `AnswererConnection` instead of `ConnectionContext`
443
+ - **Breaking:** `connection:opened` event signature changed
444
+ - See [MIGRATION.md](./MIGRATION.md) for upgrade guide
445
+
446
+ ### v0.18.8
447
+ - Initial durable connections implementation
448
+
449
+ ### v0.18.3
450
+ - Fix EventEmitter cross-platform compatibility
451
+
175
452
  ## License
176
453
 
177
454
  MIT
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Rondevu API Client - RPC interface
3
3
  */
4
- import { CryptoAdapter, Keypair } from './crypto-adapter.js';
5
- import { BatcherOptions } from './rpc-batcher.js';
6
- export type { Keypair } from './crypto-adapter.js';
7
- export type { BatcherOptions } from './rpc-batcher.js';
4
+ import { CryptoAdapter, Keypair } from '../crypto/adapter.js';
5
+ import { BatcherOptions } from './batcher.js';
6
+ export type { Keypair } from '../crypto/adapter.js';
7
+ export type { BatcherOptions } from './batcher.js';
8
8
  export interface OfferRequest {
9
9
  sdp: string;
10
10
  }
@@ -12,8 +12,6 @@ export interface ServiceRequest {
12
12
  serviceFqn: string;
13
13
  offers: OfferRequest[];
14
14
  ttl?: number;
15
- signature: string;
16
- message: string;
17
15
  }
18
16
  export interface ServiceOffer {
19
17
  offerId: string;
@@ -45,9 +43,19 @@ export declare class RondevuAPI {
45
43
  private batcher;
46
44
  constructor(baseUrl: string, username: string, keypair: Keypair, cryptoAdapter?: CryptoAdapter, batcherOptions?: BatcherOptions | false);
47
45
  /**
48
- * Generate authentication parameters for RPC calls
46
+ * Create canonical JSON string with sorted keys for deterministic signing
49
47
  */
50
- private generateAuth;
48
+ private canonicalJSON;
49
+ /**
50
+ * Generate authentication headers for RPC request
51
+ * Signs the payload (method + params + timestamp + username)
52
+ */
53
+ private generateAuthHeaders;
54
+ /**
55
+ * Generate authentication fields embedded in request body (for batch requests)
56
+ * Signs the payload (method + params + timestamp + username)
57
+ */
58
+ private generateAuthForRequest;
51
59
  /**
52
60
  * Execute RPC call with optional batching
53
61
  */
@@ -58,6 +66,7 @@ export declare class RondevuAPI {
58
66
  private rpcDirect;
59
67
  /**
60
68
  * Execute batch RPC calls directly (bypasses batcher)
69
+ * Each request in the batch has its own embedded authentication (signature, timestamp, username, publicKey)
61
70
  */
62
71
  private rpcBatchDirect;
63
72
  /**