mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-10 02:43:25 +00:00
Add WebRTC connection manager and fix race condition
- Add RondevuConnection class for high-level WebRTC management - Handles offer/answer exchange, ICE candidates, and data channels - Fix race condition in answer() method (register answerer before sending ICE) - Add event-driven API (connecting, connected, disconnected, error, datachannel, track) - Update README with connection manager examples - Export new connection types and classes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
507
README.md
507
README.md
@@ -1,112 +1,469 @@
|
||||
# Rondevu
|
||||
|
||||
🎯 **Simple WebRTC peer signaling**
|
||||
|
||||
Connect peers directly by ID with automatic WebRTC negotiation.
|
||||
|
||||
**Related repositories:**
|
||||
- [rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server
|
||||
- [rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo
|
||||
|
||||
---
|
||||
|
||||
## @xtr-dev/rondevu-client
|
||||
# @xtr-dev/rondevu-client
|
||||
|
||||
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
||||
|
||||
TypeScript client library for Rondevu peer signaling and WebRTC connection management. Handles automatic signaling, ICE candidate exchange, and connection establishment.
|
||||
🌐 **Topic-based peer discovery and WebRTC signaling client**
|
||||
|
||||
### Install
|
||||
TypeScript/JavaScript client for Rondevu, providing topic-based peer discovery, stateless authentication, and complete WebRTC signaling.
|
||||
|
||||
**Related repositories:**
|
||||
- [rondevu-server](https://github.com/xtr-dev/rondevu) - HTTP signaling server
|
||||
- [rondevu-demo](https://rondevu-demo.pages.dev) - Interactive demo
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Topic-Based Discovery**: Find peers by topics (e.g., torrent infohashes)
|
||||
- **Stateless Authentication**: No server-side sessions, portable credentials
|
||||
- **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
|
||||
- **TypeScript**: Full type safety and autocomplete
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @xtr-dev/rondevu-client
|
||||
```
|
||||
|
||||
### Usage
|
||||
## Quick Start
|
||||
|
||||
#### Browser
|
||||
### Browser (Modern with native fetch)
|
||||
|
||||
```typescript
|
||||
import { Rondevu, BloomFilter } from '@xtr-dev/rondevu-client';
|
||||
|
||||
const client = new Rondevu({ baseUrl: 'https://rondevu.xtrdev.workers.dev' });
|
||||
|
||||
// 1. 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));
|
||||
|
||||
// 2. 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
|
||||
}]);
|
||||
|
||||
// 3. Discover peers by topic
|
||||
const discovered = await client.offers.findByTopic('movie-xyz', {
|
||||
limit: 50
|
||||
});
|
||||
|
||||
console.log(`Found ${discovered.length} peers`);
|
||||
|
||||
// 4. 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
|
||||
});
|
||||
```
|
||||
|
||||
### Node.js (< 18 without native fetch)
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const client = new Rondevu({
|
||||
baseUrl: 'https://rondevu.xtrdev.workers.dev',
|
||||
fetch: fetch as any
|
||||
});
|
||||
|
||||
const creds = await client.register();
|
||||
console.log('Registered:', creds.peerId);
|
||||
```
|
||||
|
||||
### Node.js 18+ (with native fetch)
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||
|
||||
const rdv = new Rondevu({
|
||||
baseUrl: 'https://api.ronde.vu',
|
||||
rtcConfig: {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' }
|
||||
]
|
||||
// No need to provide fetch, it's available globally
|
||||
const client = new Rondevu({
|
||||
baseUrl: 'https://rondevu.xtrdev.workers.dev'
|
||||
});
|
||||
|
||||
const creds = await client.register();
|
||||
```
|
||||
|
||||
### Deno
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from 'npm:@xtr-dev/rondevu-client';
|
||||
|
||||
// Deno has native fetch, no polyfill needed
|
||||
const client = new Rondevu({
|
||||
baseUrl: 'https://rondevu.xtrdev.workers.dev'
|
||||
});
|
||||
|
||||
const creds = await client.register();
|
||||
```
|
||||
|
||||
### Bun
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||
|
||||
// Bun has native fetch, no polyfill needed
|
||||
const client = new Rondevu({
|
||||
baseUrl: 'https://rondevu.xtrdev.workers.dev'
|
||||
});
|
||||
|
||||
const creds = await client.register();
|
||||
```
|
||||
|
||||
### Cloudflare Workers
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const client = new Rondevu({
|
||||
baseUrl: 'https://rondevu.xtrdev.workers.dev'
|
||||
});
|
||||
|
||||
const creds = await client.register();
|
||||
return new Response(JSON.stringify(creds));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## WebRTC Connection Manager
|
||||
|
||||
For most use cases, you should use the high-level `RondevuConnection` class instead of manually managing WebRTC connections. It handles all the complexity of offer/answer exchange, ICE candidates, and connection lifecycle.
|
||||
|
||||
### Creating an Offer (Peer A)
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||
|
||||
const client = new Rondevu({ baseUrl: 'https://rondevu.xtrdev.workers.dev' });
|
||||
await client.register();
|
||||
|
||||
// Create a connection
|
||||
const conn = client.createConnection();
|
||||
|
||||
// Set up event listeners
|
||||
conn.on('connected', () => {
|
||||
console.log('Connected to peer!');
|
||||
});
|
||||
|
||||
// Create an offer with custom ID
|
||||
const connection = await rdv.offer('my-room-123');
|
||||
conn.on('datachannel', (channel) => {
|
||||
console.log('Data channel ready');
|
||||
|
||||
// Or answer an existing offer
|
||||
const connection = await rdv.answer('my-room-123');
|
||||
channel.onmessage = (event) => {
|
||||
console.log('Received:', event.data);
|
||||
};
|
||||
|
||||
// Use data channels
|
||||
connection.on('connect', () => {
|
||||
const channel = connection.dataChannel('chat');
|
||||
channel.send('Hello!');
|
||||
channel.send('Hello from peer A!');
|
||||
});
|
||||
|
||||
connection.on('datachannel', (channel) => {
|
||||
if (channel.label === 'chat') {
|
||||
// Create offer and advertise on topics
|
||||
const offerId = await conn.createOffer({
|
||||
topics: ['my-app', 'room-123'],
|
||||
ttl: 300000 // 5 minutes
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
const client = new Rondevu({ baseUrl: 'https://rondevu.xtrdev.workers.dev' });
|
||||
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 connection
|
||||
const conn = client.createConnection();
|
||||
|
||||
// Set up event listeners
|
||||
conn.on('connecting', () => {
|
||||
console.log('Connecting...');
|
||||
});
|
||||
|
||||
conn.on('connected', () => {
|
||||
console.log('Connected!');
|
||||
});
|
||||
|
||||
conn.on('datachannel', (channel) => {
|
||||
console.log('Data channel ready');
|
||||
|
||||
channel.onmessage = (event) => {
|
||||
console.log('Received:', event.data);
|
||||
};
|
||||
}
|
||||
|
||||
channel.send('Hello from peer B!');
|
||||
});
|
||||
|
||||
// Answer the offer
|
||||
await conn.answer(offer.id, offer.sdp);
|
||||
}
|
||||
```
|
||||
|
||||
### Connection Events
|
||||
|
||||
```typescript
|
||||
conn.on('connecting', () => {
|
||||
// Connection is being established
|
||||
});
|
||||
|
||||
conn.on('connected', () => {
|
||||
// Connection established successfully
|
||||
});
|
||||
|
||||
conn.on('disconnected', () => {
|
||||
// Connection lost or closed
|
||||
});
|
||||
|
||||
conn.on('error', (error) => {
|
||||
// An error occurred
|
||||
console.error('Connection error:', error);
|
||||
});
|
||||
|
||||
conn.on('datachannel', (channel) => {
|
||||
// Data channel is ready to use
|
||||
});
|
||||
|
||||
conn.on('track', (event) => {
|
||||
// Media track received (for audio/video streaming)
|
||||
const stream = event.streams[0];
|
||||
videoElement.srcObject = stream;
|
||||
});
|
||||
```
|
||||
|
||||
#### Node.js
|
||||
### Adding Media Tracks
|
||||
|
||||
```typescript
|
||||
// Get user's camera/microphone
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: true
|
||||
});
|
||||
|
||||
// Add tracks to connection
|
||||
stream.getTracks().forEach(track => {
|
||||
conn.addTrack(track, stream);
|
||||
});
|
||||
```
|
||||
|
||||
### Connection Properties
|
||||
|
||||
```typescript
|
||||
// Get connection state
|
||||
console.log(conn.connectionState); // 'connecting', 'connected', 'disconnected', etc.
|
||||
|
||||
// Get offer ID
|
||||
console.log(conn.id);
|
||||
|
||||
// Get data channel
|
||||
console.log(conn.channel);
|
||||
```
|
||||
|
||||
### Closing a Connection
|
||||
|
||||
```typescript
|
||||
conn.close();
|
||||
```
|
||||
|
||||
## 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
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
#### `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.heartbeat(offerId)`
|
||||
Update last_seen timestamp for an offer.
|
||||
|
||||
```typescript
|
||||
await client.offers.heartbeat(offerId);
|
||||
```
|
||||
|
||||
#### `client.offers.delete(offerId)`
|
||||
Delete a specific offer.
|
||||
|
||||
```typescript
|
||||
await client.offers.delete(offerId);
|
||||
```
|
||||
|
||||
#### `client.offers.answer(offerId, sdp)`
|
||||
Answer an offer (locks it to answerer).
|
||||
|
||||
```typescript
|
||||
await client.offers.answer(offerId, answerSdp);
|
||||
```
|
||||
|
||||
#### `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:1 1 UDP...'
|
||||
]);
|
||||
```
|
||||
|
||||
#### `client.offers.getIceCandidates(offerId, since?)`
|
||||
Get ICE candidates from the other peer.
|
||||
|
||||
```typescript
|
||||
const candidates = await client.offers.getIceCandidates(offerId);
|
||||
```
|
||||
|
||||
### 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,
|
||||
ConnectionOptions,
|
||||
RondevuConnectionEvents
|
||||
} from '@xtr-dev/rondevu-client';
|
||||
```
|
||||
|
||||
## Environment Compatibility
|
||||
|
||||
The client library is designed to work across different JavaScript runtimes:
|
||||
|
||||
| Environment | Native Fetch | Custom Fetch Needed |
|
||||
|-------------|--------------|---------------------|
|
||||
| Modern Browsers | ✅ Yes | ❌ No |
|
||||
| Node.js 18+ | ✅ Yes | ❌ No |
|
||||
| Node.js < 18 | ❌ No | ✅ Yes (node-fetch) |
|
||||
| Deno | ✅ Yes | ❌ No |
|
||||
| Bun | ✅ Yes | ❌ No |
|
||||
| Cloudflare Workers | ✅ Yes | ❌ No |
|
||||
|
||||
**If your environment doesn't have native fetch:**
|
||||
|
||||
```bash
|
||||
npm install node-fetch
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Rondevu } from '@xtr-dev/rondevu-client';
|
||||
import wrtc from '@roamhq/wrtc';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const rdv = new Rondevu({
|
||||
baseUrl: 'https://api.ronde.vu',
|
||||
fetch: fetch as any,
|
||||
wrtc: {
|
||||
RTCPeerConnection: wrtc.RTCPeerConnection,
|
||||
RTCSessionDescription: wrtc.RTCSessionDescription,
|
||||
RTCIceCandidate: wrtc.RTCIceCandidate,
|
||||
}
|
||||
});
|
||||
|
||||
const connection = await rdv.offer('my-room-123');
|
||||
|
||||
connection.on('connect', () => {
|
||||
const channel = connection.dataChannel('chat');
|
||||
channel.send('Hello from Node.js!');
|
||||
const client = new Rondevu({
|
||||
baseUrl: 'https://rondevu.xtrdev.workers.dev',
|
||||
fetch: fetch as any
|
||||
});
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
**Main Methods:**
|
||||
- `rdv.offer(id)` - Create an offer with custom ID
|
||||
- `rdv.answer(id)` - Answer an existing offer by ID
|
||||
|
||||
**Connection Events:**
|
||||
- `connect` - Connection established
|
||||
- `disconnect` - Connection closed
|
||||
- `error` - Connection error
|
||||
- `datachannel` - New data channel received
|
||||
- `stream` - Media stream received
|
||||
|
||||
**Connection Methods:**
|
||||
- `connection.dataChannel(label)` - Get or create data channel
|
||||
- `connection.addStream(stream)` - Add media stream
|
||||
- `connection.close()` - Close connection
|
||||
|
||||
### Version Compatibility
|
||||
|
||||
The client automatically checks server compatibility via the `/health` endpoint. If the server version is incompatible, an error will be thrown during initialization.
|
||||
|
||||
### License
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Reference in New Issue
Block a user