diff --git a/README.md b/README.md index f6561b0..16e3a39 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![npm version](https://img.shields.io/npm/v/@xtr-dev/rondevu-client)](https://www.npmjs.com/package/@xtr-dev/rondevu-client) -🌐 **Simple WebRTC signaling client with username-based discovery** +🌐 **WebRTC signaling client with durable connections** -TypeScript/JavaScript client for Rondevu, providing WebRTC signaling with username claiming, service publishing/discovery, and efficient batch polling. +TypeScript/JavaScript client for Rondevu, providing WebRTC signaling with **automatic reconnection**, **message buffering**, username claiming, service publishing/discovery, and efficient batch polling. **Related repositories:** - [@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,22 @@ TypeScript/JavaScript client for Rondevu, providing WebRTC signaling with userna ## Features +### ✨ New in v0.18.9 +- **🔄 Automatic Reconnection**: Built-in exponential backoff for failed connections +- **📦 Message Buffering**: Queues messages during disconnections, replays on reconnect +- **📊 Connection State Machine**: Explicit lifecycle tracking with native RTC events +- **🎯 Rich Event System**: 20+ events for monitoring connection health +- **⚡ Improved Reliability**: ICE polling lifecycle management, proper cleanup + +### Core Features - **Username Claiming**: Secure ownership with Ed25519 signatures - **Anonymous Users**: Auto-generated anonymous usernames for quick testing - **Service Publishing**: Publish services with multiple offers for connection pooling - **Service Discovery**: Direct lookup, random discovery, or paginated search -- **Efficient Batch Polling**: Single endpoint for answers and ICE candidates (50% fewer requests) +- **Efficient Batch Polling**: Single endpoint for answers and ICE candidates - **Semantic Version Matching**: Compatible version resolution (chat:1.0.0 matches any 1.x.x) - **TypeScript**: Full type safety and autocomplete - **Keypair Management**: Generate or reuse Ed25519 keypairs -- **Automatic Signatures**: All authenticated requests signed automatically ## Installation @@ -49,27 +56,35 @@ const rondevu = await Rondevu.connect({ await rondevu.publishService({ service: 'chat:1.0.0', maxOffers: 5, // Maintain up to 5 concurrent offers - offerFactory: async (pc) => { - // pc is created by Rondevu with ICE handlers already attached - const dc = pc.createDataChannel('chat') - - dc.addEventListener('open', () => { - console.log('Connection opened!') - dc.send('Hello from Alice!') - }) - - dc.addEventListener('message', (e) => { - console.log('Received:', e.data) - }) - - const offer = await pc.createOffer() - await pc.setLocalDescription(offer) - return { dc, offer } + connectionConfig: { + reconnectEnabled: true, // Auto-reconnect on failures + bufferEnabled: true, // Buffer messages during disconnections + connectionTimeout: 30000 // 30 second timeout } }) // 3. Start accepting connections await rondevu.startFilling() + +// 4. Handle incoming connections +rondevu.on('connection:opened', (offerId, connection) => { + console.log('New connection:', offerId) + + // Listen for messages + connection.on('message', (data) => { + console.log('Received:', data) + }) + + // Monitor connection state + connection.on('connected', () => { + console.log('Fully connected!') + connection.send('Hello from Alice!') + }) + + connection.on('disconnected', () => { + console.log('Connection lost, will auto-reconnect') + }) +}) ``` ### Connecting to a Service (Answerer) @@ -84,25 +99,38 @@ const rondevu = await Rondevu.connect({ iceServers: 'ipv4-turn' }) -// 2. Connect to service (automatic WebRTC setup) +// 2. Connect to service - returns AnswererConnection const connection = await rondevu.connectToService({ serviceFqn: 'chat:1.0.0@alice', - onConnection: ({ dc, peerUsername }) => { - console.log('Connected to', peerUsername) - - dc.addEventListener('message', (e) => { - console.log('Received:', e.data) - }) - - dc.addEventListener('open', () => { - dc.send('Hello from Bob!') - }) + connectionConfig: { + reconnectEnabled: true, + bufferEnabled: true, + maxReconnectAttempts: 5 } }) -// Access connection -connection.dc.send('Another message') -connection.pc.close() // Close when done +// 3. Setup event handlers +connection.on('connected', () => { + console.log('Connected to alice!') + connection.send('Hello from Bob!') +}) + +connection.on('message', (data) => { + console.log('Received:', data) +}) + +// 4. Monitor connection health +connection.on('reconnecting', (attempt) => { + console.log(`Reconnecting... attempt ${attempt}`) +}) + +connection.on('reconnect:success', () => { + console.log('Back online!') +}) + +connection.on('failed', (error) => { + console.error('Connection failed:', error) +}) ``` ## Core API @@ -126,52 +154,234 @@ await rondevu.publishService({ service: string, // e.g., 'chat:1.0.0' (username auto-appended) maxOffers: number, // Maximum concurrent offers to maintain offerFactory?: OfferFactory, // Optional: custom offer creation - ttl?: number // Optional: offer lifetime in ms (default: 300000) + ttl?: number, // Optional: offer lifetime in ms (default: 300000) + connectionConfig?: Partial // Optional: durability settings }) await rondevu.startFilling() // Start accepting connections rondevu.stopFilling() // Stop and close all connections ``` -### Service Discovery - -```typescript -// Direct lookup (with username) -await rondevu.getService('chat:1.0.0@alice') - -// Random discovery (without username) -await rondevu.discoverService('chat:1.0.0') - -// Paginated discovery -await rondevu.discoverServices('chat:1.0.0', limit, offset) -``` - ### Connecting to Services +**⚠️ Breaking Change in v0.18.9:** `connectToService()` now returns `AnswererConnection` instead of `ConnectionContext`. + ```typescript +// New API (v0.18.9+) const connection = await rondevu.connectToService({ serviceFqn?: string, // Full FQN like 'chat:1.0.0@alice' service?: string, // Service without username (for discovery) username?: string, // Target username (combined with service) - onConnection?: (context) => void, // Called when data channel opens + connectionConfig?: Partial, // Durability settings rtcConfig?: RTCConfiguration // Optional: override ICE servers }) + +// Setup event handlers +connection.on('connected', () => { + connection.send('Hello!') +}) + +connection.on('message', (data) => { + console.log(data) +}) +``` + +### Connection Configuration + +```typescript +interface ConnectionConfig { + // Timeouts + connectionTimeout: number // Default: 30000ms (30s) + iceGatheringTimeout: number // Default: 10000ms (10s) + + // Reconnection + reconnectEnabled: boolean // Default: true + maxReconnectAttempts: number // Default: 5 (0 = infinite) + reconnectBackoffBase: number // Default: 1000ms + reconnectBackoffMax: number // Default: 30000ms (30s) + + // Message buffering + bufferEnabled: boolean // Default: true + maxBufferSize: number // Default: 100 messages + maxBufferAge: number // Default: 60000ms (1 min) + + // Debug + debug: boolean // Default: false +} +``` + +### Connection Events + +```typescript +// Lifecycle events +connection.on('connecting', () => {}) +connection.on('connected', () => {}) +connection.on('disconnected', (reason) => {}) +connection.on('failed', (error) => {}) +connection.on('closed', (reason) => {}) + +// Reconnection events +connection.on('reconnecting', (attempt) => {}) +connection.on('reconnect:success', () => {}) +connection.on('reconnect:failed', (error) => {}) +connection.on('reconnect:exhausted', (attempts) => {}) + +// Message events +connection.on('message', (data) => {}) +connection.on('message:buffered', (data) => {}) +connection.on('message:replayed', (message) => {}) + +// ICE events +connection.on('ice:connection:state', (state) => {}) +connection.on('ice:polling:started', () => {}) +connection.on('ice:polling:stopped', () => {}) +``` + +### Service Discovery + +```typescript +// Unified discovery API +const service = await rondevu.findService( + 'chat:1.0.0@alice', // Direct lookup (with username) + { mode: 'direct' } +) + +const service = await rondevu.findService( + 'chat:1.0.0', // Random discovery (without username) + { mode: 'random' } +) + +const result = await rondevu.findService( + 'chat:1.0.0', + { + mode: 'paginated', + limit: 20, + offset: 0 + } +) +``` + +## Migration Guide + +**Upgrading from v0.18.7 or earlier?** See [MIGRATION.md](./MIGRATION.md) for detailed upgrade instructions. + +### Quick Migration Summary + +**Before (v0.18.7):** +```typescript +const context = await rondevu.connectToService({ + serviceFqn: 'chat:1.0.0@alice', + onConnection: ({ dc }) => { + dc.addEventListener('message', (e) => console.log(e.data)) + dc.send('Hello') + } +}) +``` + +**After (v0.18.9):** +```typescript +const connection = await rondevu.connectToService({ + serviceFqn: 'chat:1.0.0@alice' +}) + +connection.on('connected', () => { + connection.send('Hello') // Use connection.send() +}) + +connection.on('message', (data) => { + console.log(data) // data is already extracted +}) +``` + +## Advanced Usage + +### Custom Offer Factory + +```typescript +await rondevu.publishService({ + service: 'file-transfer:1.0.0', + maxOffers: 3, + offerFactory: async (pc) => { + // Customize data channel settings + const dc = pc.createDataChannel('files', { + ordered: true, + maxRetransmits: 10 + }) + + // Add custom listeners + dc.addEventListener('open', () => { + console.log('Transfer channel ready') + }) + + const offer = await pc.createOffer() + await pc.setLocalDescription(offer) + return { dc, offer } + } +}) +``` + +### Accessing Raw RTCPeerConnection + +```typescript +const connection = await rondevu.connectToService({ ... }) + +// Get raw objects if needed +const pc = connection.getPeerConnection() +const dc = connection.getDataChannel() + +// Note: Using raw DataChannel bypasses buffering/reconnection features +if (dc) { + dc.addEventListener('message', (e) => { + console.log('Raw message:', e.data) + }) +} +``` + +### Disabling Durability Features + +```typescript +const connection = await rondevu.connectToService({ + serviceFqn: 'chat:1.0.0@alice', + connectionConfig: { + reconnectEnabled: false, // Disable auto-reconnect + bufferEnabled: false, // Disable message buffering + } +}) ``` ## Documentation +📚 **[MIGRATION.md](./MIGRATION.md)** - Upgrade guide from v0.18.7 to v0.18.9 + 📚 **[ADVANCED.md](./ADVANCED.md)** - Comprehensive guide including: - Detailed API reference for all methods - Type definitions and interfaces - Platform support (Browser & Node.js) - Advanced usage patterns - Username rules and service FQN format -- Examples and migration guides ## Examples - [React Demo](https://github.com/xtr-dev/rondevu-demo) - Full browser UI ([live](https://ronde.vu)) +## Changelog + +### v0.18.9 (Latest) +- Add durable WebRTC connections with state machine +- Implement automatic reconnection with exponential backoff +- Add message buffering during disconnections +- Fix ICE polling lifecycle (stops when connected) +- Add fillOffers() semaphore to prevent exceeding maxOffers +- **Breaking:** `connectToService()` returns `AnswererConnection` instead of `ConnectionContext` +- **Breaking:** `connection:opened` event signature changed +- See [MIGRATION.md](./MIGRATION.md) for upgrade guide + +### v0.18.8 +- Initial durable connections implementation + +### v0.18.3 +- Fix EventEmitter cross-platform compatibility + ## License MIT