Files
rondevu-client/README.md

15 KiB

@xtr-dev/rondevu-client

npm version

🌐 Topic-based peer discovery and WebRTC signaling client

TypeScript/JavaScript client for Rondevu, providing topic-based peer discovery, stateless authentication, and complete WebRTC signaling with trickle ICE support.

Related repositories:


Features

  • Topic-Based Discovery: Find peers by topics (e.g., torrent infohashes)
  • Stateless Authentication: No server-side sessions, portable credentials
  • Protected Connections: Optional secret-protected offers for access control
  • Bloom Filters: Efficient peer exclusion for repeated discoveries
  • Multi-Offer Management: Create and manage multiple offers per peer
  • Complete WebRTC Signaling: Full offer/answer and ICE candidate exchange
  • Trickle ICE: Send ICE candidates as they're discovered (faster connections)
  • State Machine: Clean state-based connection lifecycle
  • TypeScript: Full type safety and autocomplete

Install

npm install @xtr-dev/rondevu-client

Quick Start

Creating an Offer (Peer A)

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

// Initialize client and register
const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
await client.register();

// Create peer connection
const peer = client.createPeer();

// Set up event listeners
peer.on('state', (state) => {
  console.log('Peer state:', state);
  // States: idle → creating-offer → waiting-for-answer → exchanging-ice → connected
});

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

peer.on('datachannel', (channel) => {
  console.log('📡 Data channel ready');

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

  channel.addEventListener('open', () => {
    channel.send('Hello from peer A!');
  });
});

// Create offer and advertise on topics
const offerId = await peer.createOffer({
  topics: ['my-app', 'room-123'],
  ttl: 300000,  // 5 minutes
  secret: 'my-secret-password'  // Optional: protect offer (max 128 chars)
});

console.log('Offer created:', offerId);
console.log('Share these topics with peers:', ['my-app', 'room-123']);

Answering an Offer (Peer B)

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

// Initialize client and register
const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
await client.register();

// Discover offers by topic
const offers = await client.offers.findByTopic('my-app', { limit: 10 });

if (offers.length > 0) {
  const offer = offers[0];

  // Create peer connection
  const peer = client.createPeer();

  // Set up event listeners
  peer.on('state', (state) => {
    console.log('Peer state:', state);
    // States: idle → answering → exchanging-ice → connected
  });

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

  peer.on('datachannel', (channel) => {
    console.log('📡 Data channel ready');

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

    channel.addEventListener('open', () => {
      channel.send('Hello from peer B!');
    });
  });

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

  // Answer the offer
  await peer.answer(offer.id, offer.sdp, {
    topics: offer.topics,
    secret: 'my-secret-password'  // Required if offer.hasSecret is true
  });
}

Protected Offers

You can protect offers with a secret to control who can answer them. This is useful for private rooms or invite-only connections.

Creating a Protected Offer

const offerId = await peer.createOffer({
  topics: ['private-room'],
  secret: 'my-secret-password'  // Max 128 characters
});

// Share the secret with authorized peers through a secure channel

Answering a Protected Offer

const offers = await client.offers.findByTopic('private-room');

// Check if offer requires a secret
if (offers[0].hasSecret) {
  console.log('This offer requires a secret');
}

// Provide the secret when answering
await peer.answer(offers[0].id, offers[0].sdp, {
  topics: offers[0].topics,
  secret: 'my-secret-password'  // Must match the offer's secret
});

Notes:

  • The actual secret is never exposed in public API responses - only a hasSecret boolean flag
  • Answerers must provide the correct secret, or the answer will be rejected
  • Secrets are limited to 128 characters
  • Use this for access control, not for cryptographic security (use end-to-end encryption for that)

Connection Lifecycle

The RondevuPeer uses a state machine for connection management:

Offerer States

  1. idle - Initial state
  2. creating-offer - Creating WebRTC offer
  3. waiting-for-answer - Polling for answer from peer
  4. exchanging-ice - Exchanging ICE candidates
  5. connected - Successfully connected
  6. failed - Connection failed
  7. closed - Connection closed

Answerer States

  1. idle - Initial state
  2. answering - Creating WebRTC answer
  3. exchanging-ice - Exchanging ICE candidates
  4. connected - Successfully connected
  5. failed - Connection failed
  6. closed - Connection closed

State Events

peer.on('state', (stateName) => {
  console.log('Current state:', stateName);
});

peer.on('connected', () => {
  // Connection established successfully
});

peer.on('disconnected', () => {
  // Connection lost or closed
});

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

peer.on('datachannel', (channel) => {
  // Data channel is ready (use channel.addEventListener)
});

peer.on('track', (event) => {
  // Media track received (for audio/video streaming)
  const stream = event.streams[0];
  videoElement.srcObject = stream;
});

Trickle ICE

This library implements trickle ICE for faster connection establishment:

  • ICE candidates are sent to the server as they're discovered
  • No waiting for all candidates before sending offer/answer
  • Connections establish much faster (milliseconds vs seconds)
  • Proper event listener cleanup to prevent memory leaks

Adding Media Tracks

// Get user's camera/microphone
const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});

// Add tracks to peer connection
stream.getTracks().forEach(track => {
  peer.addTrack(track, stream);
});

Peer Properties

// Get current state name
console.log(peer.stateName); // 'idle', 'creating-offer', 'connected', etc.

// Get connection state
console.log(peer.connectionState); // RTCPeerConnectionState

// Get offer ID (after creating offer or answering)
console.log(peer.offerId);

// Get role
console.log(peer.role); // 'offerer' or 'answerer'

Closing a Connection

await peer.close();

Custom RTCConfiguration

const peer = client.createPeer({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:turn.example.com:3478',
      username: 'user',
      credential: 'pass'
    }
  ],
  iceTransportPolicy: 'relay' // Force TURN relay (useful for testing)
});

Timeouts

Configure connection timeouts:

await peer.createOffer({
  topics: ['my-topic'],
  timeouts: {
    iceGathering: 10000,        // ICE gathering timeout (10s)
    waitingForAnswer: 30000,    // Waiting for answer timeout (30s)
    creatingAnswer: 10000,      // Creating answer timeout (10s)
    iceConnection: 30000        // ICE connection timeout (30s)
  }
});

Platform-Specific Setup

Node.js 18+ (with native fetch)

Works out of the box - no additional setup needed.

Node.js < 18 (without native fetch)

Install node-fetch and provide it to the client:

npm install node-fetch
import { Rondevu } from '@xtr-dev/rondevu-client';
import fetch from 'node-fetch';

const client = new Rondevu({
  baseUrl: 'https://api.ronde.vu',
  fetch: fetch as any
});

Node.js with WebRTC (wrtc)

For WebRTC functionality in Node.js, you need to provide WebRTC polyfills since Node.js doesn't have native WebRTC support:

npm install wrtc node-fetch
import { Rondevu } from '@xtr-dev/rondevu-client';
import fetch from 'node-fetch';
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';

const client = new Rondevu({
  baseUrl: 'https://api.ronde.vu',
  fetch: fetch as any,
  RTCPeerConnection,
  RTCSessionDescription,
  RTCIceCandidate
});

// Now you can use WebRTC features
await client.register();
const peer = client.createPeer({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }
  ]
});

// Create offers, answer, etc.
const offerId = await peer.createOffer({
  topics: ['my-topic']
});

Note: The wrtc package provides WebRTC bindings for Node.js. Alternative packages like node-webrtc can also be used - just pass their implementations to the Rondevu constructor.

Deno

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

const client = new Rondevu({
  baseUrl: 'https://api.ronde.vu'
});

Bun

Works out of the box - no additional setup needed.

Cloudflare Workers

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

export default {
  async fetch(request: Request, env: Env) {
    const client = new Rondevu({
      baseUrl: 'https://api.ronde.vu'
    });

    const creds = await client.register();
    return new Response(JSON.stringify(creds));
  }
};

Low-Level API Usage

For direct control over the signaling process without WebRTC:

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

const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });

// Register and get credentials
const creds = await client.register();
console.log('Peer ID:', creds.peerId);

// Save credentials for later use
localStorage.setItem('rondevu-creds', JSON.stringify(creds));

// Create offer with topics
const offers = await client.offers.create([{
  sdp: 'v=0...',  // Your WebRTC offer SDP
  topics: ['movie-xyz', 'hd-content'],
  ttl: 300000,  // 5 minutes
  secret: 'my-secret-password'  // Optional: protect offer (max 128 chars)
}]);

// Discover peers by topic
const discovered = await client.offers.findByTopic('movie-xyz', {
  limit: 50
});

console.log(`Found ${discovered.length} peers`);

// Use bloom filter to exclude known peers
const knownPeers = new Set(['peer-id-1', 'peer-id-2']);
const bloom = new BloomFilter(1024, 3);
knownPeers.forEach(id => bloom.add(id));

const newPeers = await client.offers.findByTopic('movie-xyz', {
  bloomFilter: bloom.toBytes(),
  limit: 50
});

API Reference

Authentication

client.register()

Register a new peer and receive credentials.

const creds = await client.register();
// { peerId: '...', secret: '...' }

Topics

client.offers.getTopics(options?)

List all topics with active peer counts (paginated).

const result = await client.offers.getTopics({
  limit: 50,
  offset: 0
});

// {
//   topics: [
//     { topic: 'movie-xyz', activePeers: 42 },
//     { topic: 'torrent-abc', activePeers: 15 }
//   ],
//   total: 123,
//   limit: 50,
//   offset: 0
// }

Offers

client.offers.create(offers)

Create one or more offers with topics.

const offers = await client.offers.create([
  {
    sdp: 'v=0...',
    topics: ['topic-1', 'topic-2'],
    ttl: 300000,  // optional, default 5 minutes
    secret: 'my-secret-password'  // optional, max 128 chars
  }
]);

client.offers.findByTopic(topic, options?)

Find offers by topic with optional bloom filter.

const offers = await client.offers.findByTopic('movie-xyz', {
  limit: 50,
  bloomFilter: bloomBytes  // optional
});

client.offers.getMine()

Get all offers owned by the authenticated peer.

const myOffers = await client.offers.getMine();

client.offers.delete(offerId)

Delete a specific offer.

await client.offers.delete(offerId);

client.offers.answer(offerId, sdp, secret?)

Answer an offer (locks it to answerer).

await client.offers.answer(offerId, answerSdp, 'my-secret-password');

Parameters:

  • offerId: The offer ID to answer
  • sdp: The WebRTC answer SDP
  • secret (optional): Required if the offer has hasSecret: true

client.offers.getAnswers()

Poll for answers to your offers.

const answers = await client.offers.getAnswers();

ICE Candidates

client.offers.addIceCandidates(offerId, candidates)

Post ICE candidates for an offer.

await client.offers.addIceCandidates(offerId, [
  { candidate: 'candidate:1 1 UDP...', sdpMid: '0', sdpMLineIndex: 0 }
]);

client.offers.getIceCandidates(offerId, since?)

Get ICE candidates from the other peer.

const candidates = await client.offers.getIceCandidates(offerId, since);

Bloom Filter

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

// Create filter: size=1024 bits, hash=3 functions
const bloom = new BloomFilter(1024, 3);

// Add items
bloom.add('peer-id-1');
bloom.add('peer-id-2');

// Test membership
bloom.test('peer-id-1');  // true (probably)
bloom.test('unknown');    // false (definitely)

// Export for API
const bytes = bloom.toBytes();

TypeScript

All types are exported:

import type {
  Credentials,
  Offer,
  CreateOfferRequest,
  TopicInfo,
  IceCandidate,
  FetchFunction,
  RondevuOptions,
  PeerOptions,
  PeerEvents,
  PeerTimeouts
} from '@xtr-dev/rondevu-client';

Environment Compatibility

The client library is designed to work across different JavaScript runtimes:

Environment Native Fetch Native WebRTC Polyfills Needed
Modern Browsers Yes Yes None
Node.js 18+ Yes No WebRTC (wrtc)
Node.js < 18 No No Fetch + WebRTC
Deno Yes ⚠️ Partial None (signaling only)
Bun Yes No WebRTC (wrtc)
Cloudflare Workers Yes No None (signaling only)

For signaling-only (no WebRTC peer connections):

Use the low-level API with client.offers - no WebRTC polyfills needed.

For full WebRTC support in Node.js:

npm install wrtc node-fetch
import { Rondevu } from '@xtr-dev/rondevu-client';
import fetch from 'node-fetch';
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';

const client = new Rondevu({
  baseUrl: 'https://api.ronde.vu',
  fetch: fetch as any,
  RTCPeerConnection,
  RTCSessionDescription,
  RTCIceCandidate
});

License

MIT