mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-10 02:43:25 +00:00
625 lines
15 KiB
Markdown
625 lines
15 KiB
Markdown
# @xtr-dev/rondevu-client
|
|
|
|
[](https://www.npmjs.com/package/@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](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
|
|
- [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server))
|
|
- [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://ronde.vu))
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
```bash
|
|
npm install @xtr-dev/rondevu-client
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
### Creating an Offer (Peer A)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
await peer.close();
|
|
```
|
|
|
|
## Custom RTCConfiguration
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```bash
|
|
npm install node-fetch
|
|
```
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```bash
|
|
npm install wrtc node-fetch
|
|
```
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
const creds = await client.register();
|
|
// { peerId: '...', secret: '...' }
|
|
```
|
|
|
|
### Topics
|
|
|
|
#### `client.offers.getTopics(options?)`
|
|
List all topics with active peer counts (paginated).
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
const offers = await client.offers.findByTopic('movie-xyz', {
|
|
limit: 50,
|
|
bloomFilter: bloomBytes // optional
|
|
});
|
|
```
|
|
|
|
#### `client.offers.getMine()`
|
|
Get all offers owned by the authenticated peer.
|
|
|
|
```typescript
|
|
const myOffers = await client.offers.getMine();
|
|
```
|
|
|
|
#### `client.offers.delete(offerId)`
|
|
Delete a specific offer.
|
|
|
|
```typescript
|
|
await client.offers.delete(offerId);
|
|
```
|
|
|
|
#### `client.offers.answer(offerId, sdp, secret?)`
|
|
Answer an offer (locks it to answerer).
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
const answers = await client.offers.getAnswers();
|
|
```
|
|
|
|
### ICE Candidates
|
|
|
|
#### `client.offers.addIceCandidates(offerId, candidates)`
|
|
Post ICE candidates for an offer.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
const candidates = await client.offers.getIceCandidates(offerId, since);
|
|
```
|
|
|
|
### Bloom Filter
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```bash
|
|
npm install wrtc node-fetch
|
|
```
|
|
|
|
```typescript
|
|
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
|