Files
rondevu-client/MIGRATION.md
Bas van den Aakster 9486376442 feat: v0.9.0 - durable WebRTC connections with automatic reconnection
Major refactor replacing low-level APIs with high-level durable connections.

New Features:
- Automatic reconnection with exponential backoff (1s → 2s → 4s → ... max 30s)
- Message queuing during disconnections
- Durable channels that survive connection drops
- TTL auto-refresh for services (refreshes at 80% of TTL by default)
- Full configuration of timeouts, retry limits, and queue sizes

New API:
- client.exposeService() - Create durable service with automatic TTL refresh
- client.connect() - Create durable connection with automatic reconnection
- client.connectByUuid() - Connect by service UUID
- DurableChannel - Event-based channel wrapper with message queuing
- DurableConnection - Connection manager with reconnection logic
- DurableService - Service manager with TTL auto-refresh

Files Added:
- src/durable/types.ts - Type definitions and enums
- src/durable/reconnection.ts - Exponential backoff utilities
- src/durable/channel.ts - DurableChannel class (358 lines)
- src/durable/connection.ts - DurableConnection class (441 lines)
- src/durable/service.ts - DurableService class (329 lines)
- MIGRATION.md - Comprehensive migration guide

Files Removed:
- src/services.ts - Replaced by DurableService
- src/discovery.ts - Replaced by DurableConnection

BREAKING CHANGES:
- Removed: client.services.*, client.discovery.*, client.createPeer()
- Added: client.exposeService(), client.connect(), client.connectByUuid()
- Handler signature: (channel, peer, connectionId?) → (channel, connectionId)
- Event handlers: .onmessage → .on('message')
- Services: Must call service.start() to begin accepting connections
- Connections: Must call connection.connect() to establish connection
2025-12-06 13:04:19 +01:00

14 KiB

Migration Guide: v0.8.x → v0.9.0

This guide helps you migrate from Rondevu Client v0.8.x to v0.9.0.

Overview

v0.9.0 is a breaking change that completely replaces low-level APIs with high-level durable connections featuring automatic reconnection and message queuing.

What's New

Durable Connections: Automatic reconnection on network drops Message Queuing: Messages sent during disconnections are queued and flushed on reconnect Durable Channels: RTCDataChannel wrappers that survive connection drops TTL Auto-Refresh: Services automatically republish before expiration Simplified API: Direct methods on main client instead of nested APIs

What's Removed

Low-level APIs: client.services.*, client.discovery.*, client.createPeer() no longer exported Manual Connection Management: No need to handle WebRTC peer lifecycle manually Service Handles: Replaced with DurableService instances

Breaking Changes

1. Service Exposure

v0.8.x (Old)

import { Rondevu } from '@xtr-dev/rondevu-client';

const client = new Rondevu();
await client.register();

const handle = await client.services.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'chat@1.0.0',
  isPublic: true,
  handler: (channel, peer) => {
    channel.onmessage = (e) => {
      console.log('Received:', e.data);
      channel.send(`Echo: ${e.data}`);
    };
  }
});

// Unpublish
await handle.unpublish();

v0.9.0 (New)

import { Rondevu } from '@xtr-dev/rondevu-client';

const client = new Rondevu();
await client.register();

const service = await client.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'chat@1.0.0',
  isPublic: true,
  poolSize: 10,  // NEW: Handle multiple concurrent connections
  handler: (channel, connectionId) => {
    // NEW: DurableChannel with event emitters
    channel.on('message', (data) => {
      console.log('Received:', data);
      channel.send(`Echo: ${data}`);
    });
  }
});

// NEW: Start the service
await service.start();

// NEW: Stop the service
await service.stop();

Key Differences:

  • client.services.exposeService()client.exposeService()
  • Returns DurableService instead of ServiceHandle
  • Handler receives DurableChannel instead of RTCDataChannel
  • Handler receives connectionId string instead of RondevuPeer
  • DurableChannel uses .on('message', ...) instead of .onmessage = ...
  • Must call service.start() to begin accepting connections
  • Use service.stop() instead of handle.unpublish()

2. Connecting to Services

v0.8.x (Old)

// Connect by username + FQN
const { peer, channel } = await client.discovery.connect(
  'alice',
  'chat@1.0.0'
);

channel.onmessage = (e) => {
  console.log('Received:', e.data);
};

channel.onopen = () => {
  channel.send('Hello!');
};

peer.on('connected', () => {
  console.log('Connected');
});

peer.on('failed', (error) => {
  console.error('Failed:', error);
});

v0.9.0 (New)

// Connect by username + FQN
const connection = await client.connect('alice', 'chat@1.0.0', {
  maxReconnectAttempts: 10  // NEW: Configurable reconnection
});

// NEW: Create durable channel
const channel = connection.createChannel('main');

channel.on('message', (data) => {
  console.log('Received:', data);
});

channel.on('open', () => {
  channel.send('Hello!');
});

// NEW: Connection lifecycle events
connection.on('connected', () => {
  console.log('Connected');
});

connection.on('reconnecting', (attempt, max, delay) => {
  console.log(`Reconnecting (${attempt}/${max})...`);
});

connection.on('failed', (error) => {
  console.error('Failed permanently:', error);
});

// NEW: Must explicitly connect
await connection.connect();

Key Differences:

  • client.discovery.connect()client.connect()
  • Returns DurableConnection instead of { peer, channel }
  • Must create channels with connection.createChannel()
  • Must call connection.connect() to establish connection
  • Automatic reconnection with configurable retry limits
  • Messages sent during disconnection are automatically queued

3. Connecting by UUID

v0.8.x (Old)

const { peer, channel } = await client.discovery.connectByUuid('service-uuid');

channel.onmessage = (e) => {
  console.log('Received:', e.data);
};

v0.9.0 (New)

const connection = await client.connectByUuid('service-uuid', {
  maxReconnectAttempts: 5
});

const channel = connection.createChannel('main');

channel.on('message', (data) => {
  console.log('Received:', data);
});

await connection.connect();

Key Differences:

  • client.discovery.connectByUuid()client.connectByUuid()
  • Returns DurableConnection instead of { peer, channel }
  • Must create channels and connect explicitly

4. Multi-Connection Services (Offer Pooling)

v0.8.x (Old)

const handle = await client.services.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'chat@1.0.0',
  poolSize: 5,
  pollingInterval: 2000,
  handler: (channel, peer, connectionId) => {
    console.log(`Connection: ${connectionId}`);
  },
  onPoolStatus: (status) => {
    console.log('Pool status:', status);
  }
});

const status = handle.getStatus();
await handle.addOffers(3);

v0.9.0 (New)

const service = await client.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'chat@1.0.0',
  poolSize: 5,          // SAME: Pool size
  pollingInterval: 2000, // SAME: Polling interval
  handler: (channel, connectionId) => {
    console.log(`Connection: ${connectionId}`);
  }
});

await service.start();

// Get active connections
const connections = service.getActiveConnections();

// Listen for connection events
service.on('connection', (connectionId) => {
  console.log('New connection:', connectionId);
});

Key Differences:

  • onPoolStatus callback removed (use service.on('connection') instead)
  • handle.getStatus() replaced with service.getActiveConnections()
  • handle.addOffers() removed (pool auto-manages offers)
  • Handler receives DurableChannel instead of RTCDataChannel

Feature Comparison

Feature v0.8.x v0.9.0
Service exposure client.services.exposeService() client.exposeService()
Connection client.discovery.connect() client.connect()
Connection by UUID client.discovery.connectByUuid() client.connectByUuid()
Channel type RTCDataChannel DurableChannel
Event handling .onmessage, .onopen, etc. .on('message'), .on('open'), etc.
Automatic reconnection No Yes (configurable)
Message queuing No Yes (during disconnections)
TTL auto-refresh No Yes (configurable)
Peer lifecycle Manual Automatic
Connection pooling Yes Yes (same API)

API Mapping

Removed Exports

These are no longer exported in v0.9.0:

// ❌ Removed
import {
  RondevuServices,
  RondevuDiscovery,
  RondevuPeer,
  ServiceHandle,
  PooledServiceHandle,
  ConnectResult
} from '@xtr-dev/rondevu-client';

New Exports

These are new in v0.9.0:

// ✅ New
import {
  DurableConnection,
  DurableChannel,
  DurableService,
  DurableConnectionState,
  DurableChannelState,
  DurableConnectionConfig,
  DurableChannelConfig,
  DurableServiceConfig,
  DurableConnectionEvents,
  DurableChannelEvents,
  DurableServiceEvents,
  ConnectionInfo,
  ServiceInfo,
  QueuedMessage
} from '@xtr-dev/rondevu-client';

Unchanged Exports

These work the same in both versions:

// ✅ Unchanged
import {
  Rondevu,
  RondevuAuth,
  RondevuUsername,
  Credentials,
  UsernameClaimResult,
  UsernameCheckResult
} from '@xtr-dev/rondevu-client';

Configuration Options

New Connection Options

v0.9.0 adds extensive configuration for automatic reconnection and message queuing:

const connection = await client.connect('alice', 'chat@1.0.0', {
  // Reconnection
  maxReconnectAttempts: 10,      // default: 10
  reconnectBackoffBase: 1000,    // default: 1000ms
  reconnectBackoffMax: 30000,    // default: 30000ms (30 seconds)
  reconnectJitter: 0.2,          // default: 0.2 (±20%)
  connectionTimeout: 30000,      // default: 30000ms

  // Message queuing
  maxQueueSize: 1000,            // default: 1000 messages
  maxMessageAge: 60000,          // default: 60000ms (1 minute)

  // WebRTC
  rtcConfig: {
    iceServers: [...]
  }
});

New Service Options

Services can now auto-refresh TTL:

const service = await client.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'chat@1.0.0',

  // TTL auto-refresh (NEW)
  ttl: 300000,              // default: 300000ms (5 minutes)
  ttlRefreshMargin: 0.2,    // default: 0.2 (refresh at 80% of TTL)

  // All connection options also apply to incoming connections
  maxReconnectAttempts: 10,
  maxQueueSize: 1000,
  // ...
});

Migration Checklist

  • Replace client.services.exposeService() with client.exposeService()
  • Add await service.start() after creating service
  • Replace handle.unpublish() with service.stop()
  • Replace client.discovery.connect() with client.connect()
  • Replace client.discovery.connectByUuid() with client.connectByUuid()
  • Create channels with connection.createChannel() instead of receiving them directly
  • Add await connection.connect() to establish connection
  • Update handlers from (channel, peer, connectionId?) to (channel, connectionId)
  • Replace .onmessage with .on('message', ...)
  • Replace .onopen with .on('open', ...)
  • Replace .onclose with .on('close', ...)
  • Replace .onerror with .on('error', ...)
  • Add reconnection event handlers (connection.on('reconnecting', ...))
  • Review and configure reconnection options if needed
  • Review and configure message queue limits if needed
  • Update TypeScript imports to use new types
  • Test automatic reconnection behavior
  • Test message queuing during disconnections

Common Migration Patterns

Pattern 1: Simple Echo Service

Before (v0.8.x)

await client.services.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'echo@1.0.0',
  handler: (channel) => {
    channel.onmessage = (e) => {
      channel.send(`Echo: ${e.data}`);
    };
  }
});

After (v0.9.0)

const service = await client.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'echo@1.0.0',
  handler: (channel) => {
    channel.on('message', (data) => {
      channel.send(`Echo: ${data}`);
    });
  }
});

await service.start();

Pattern 2: Connection with Error Handling

Before (v0.8.x)

try {
  const { peer, channel } = await client.discovery.connect('alice', 'chat@1.0.0');

  channel.onopen = () => {
    channel.send('Hello!');
  };

  peer.on('failed', (error) => {
    console.error('Connection failed:', error);
    // Manual reconnection logic here
  });
} catch (error) {
  console.error('Failed to connect:', error);
}

After (v0.9.0)

const connection = await client.connect('alice', 'chat@1.0.0', {
  maxReconnectAttempts: 5
});

const channel = connection.createChannel('main');

channel.on('open', () => {
  channel.send('Hello!');
});

connection.on('reconnecting', (attempt, max, delay) => {
  console.log(`Reconnecting (${attempt}/${max}) in ${delay}ms`);
});

connection.on('failed', (error) => {
  console.error('Connection failed permanently:', error);
});

try {
  await connection.connect();
} catch (error) {
  console.error('Initial connection failed:', error);
}

Pattern 3: Multi-User Chat Server

Before (v0.8.x)

const connections = new Map();

await client.services.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'chat@1.0.0',
  poolSize: 10,
  handler: (channel, peer, connectionId) => {
    connections.set(connectionId, channel);

    channel.onmessage = (e) => {
      // Broadcast to all
      for (const [id, ch] of connections) {
        if (id !== connectionId) {
          ch.send(e.data);
        }
      }
    };

    channel.onclose = () => {
      connections.delete(connectionId);
    };
  }
});

After (v0.9.0)

const channels = new Map();

const service = await client.exposeService({
  username: 'alice',
  privateKey: keypair.privateKey,
  serviceFqn: 'chat@1.0.0',
  poolSize: 10,
  handler: (channel, connectionId) => {
    channels.set(connectionId, channel);

    channel.on('message', (data) => {
      // Broadcast to all
      for (const [id, ch] of channels) {
        if (id !== connectionId) {
          ch.send(data);
        }
      }
    });

    channel.on('close', () => {
      channels.delete(connectionId);
    });
  }
});

await service.start();

// Optional: Track connections
service.on('connection', (connectionId) => {
  console.log(`User ${connectionId} joined`);
});

service.on('disconnection', (connectionId) => {
  console.log(`User ${connectionId} left`);
});

Benefits of Migration

  1. Reliability: Automatic reconnection handles network hiccups transparently
  2. Simplicity: No need to manage WebRTC peer lifecycle manually
  3. Durability: Messages sent during disconnections are queued and delivered when connection restores
  4. Uptime: Services automatically refresh TTL before expiration
  5. Type Safety: Better TypeScript types with DurableChannel event emitters
  6. Debugging: Queue size monitoring, connection state tracking, and detailed events

Getting Help

If you encounter issues during migration:

  1. Check the README for complete API documentation
  2. Review the examples for common patterns
  3. Open an issue on GitHub