15 KiB
@xtr-dev/rondevu-client
🌐 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:
- @xtr-dev/rondevu-client - TypeScript client library (npm)
- @xtr-dev/rondevu-server - HTTP signaling server (npm)
- @xtr-dev/rondevu-demo - Interactive demo (live)
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
hasSecretboolean 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
- idle - Initial state
- creating-offer - Creating WebRTC offer
- waiting-for-answer - Polling for answer from peer
- exchanging-ice - Exchanging ICE candidates
- connected - Successfully connected
- failed - Connection failed
- closed - Connection closed
Answerer States
- idle - Initial state
- answering - Creating WebRTC answer
- exchanging-ice - Exchanging ICE candidates
- connected - Successfully connected
- failed - Connection failed
- 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 answersdp: The WebRTC answer SDPsecret(optional): Required if the offer hashasSecret: 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