@xtr-dev/rondevu-client 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,112 +1,453 @@
1
- # Rondevu
1
+ # @xtr-dev/rondevu-client
2
2
 
3
- 🎯 **Simple WebRTC peer signaling**
3
+ [![npm version](https://img.shields.io/npm/v/@xtr-dev/rondevu-client)](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
4
+
5
+ 🌐 **Topic-based peer discovery and WebRTC signaling client**
4
6
 
5
- Connect peers directly by ID with automatic WebRTC negotiation.
7
+ TypeScript/JavaScript client for Rondevu, providing topic-based peer discovery, stateless authentication, and complete WebRTC signaling.
6
8
 
7
9
  **Related repositories:**
8
- - [rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server
9
- - [rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo
10
+ - [rondevu-server](https://github.com/xtr-dev/rondevu) - HTTP signaling server
11
+ - [rondevu-demo](https://rondevu-demo.pages.dev) - Interactive demo
10
12
 
11
13
  ---
12
14
 
13
- ## @xtr-dev/rondevu-client
14
-
15
- [![npm version](https://img.shields.io/npm/v/@xtr-dev/rondevu-client)](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
15
+ ## Features
16
16
 
17
- TypeScript client library for Rondevu peer signaling and WebRTC connection management. Handles automatic signaling, ICE candidate exchange, and connection establishment.
17
+ - **Topic-Based Discovery**: Find peers by topics (e.g., torrent infohashes)
18
+ - **Stateless Authentication**: No server-side sessions, portable credentials
19
+ - **Bloom Filters**: Efficient peer exclusion for repeated discoveries
20
+ - **Multi-Offer Management**: Create and manage multiple offers per peer
21
+ - **Complete WebRTC Signaling**: Full offer/answer and ICE candidate exchange
22
+ - **TypeScript**: Full type safety and autocomplete
18
23
 
19
- ### Install
24
+ ## Install
20
25
 
21
26
  ```bash
22
27
  npm install @xtr-dev/rondevu-client
23
28
  ```
24
29
 
25
- ### Usage
30
+ ## Quick Start
31
+
32
+ The easiest way to use Rondevu is with the high-level `RondevuConnection` class, which handles all WebRTC connection complexity including offer/answer exchange, ICE candidates, and connection lifecycle.
26
33
 
27
- #### Browser
34
+ ### Creating an Offer (Peer A)
28
35
 
29
36
  ```typescript
30
37
  import { Rondevu } from '@xtr-dev/rondevu-client';
31
38
 
32
- const rdv = new Rondevu({
33
- baseUrl: 'https://api.ronde.vu',
34
- rtcConfig: {
35
- iceServers: [
36
- { urls: 'stun:stun.l.google.com:19302' },
37
- { urls: 'stun:stun1.l.google.com:19302' }
38
- ]
39
- }
39
+ const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
40
+ await client.register();
41
+
42
+ // Create a connection
43
+ const conn = client.createConnection();
44
+
45
+ // Set up event listeners
46
+ conn.on('connected', () => {
47
+ console.log('Connected to peer!');
40
48
  });
41
49
 
42
- // Create an offer with custom ID
43
- const connection = await rdv.offer('my-room-123');
50
+ conn.on('datachannel', (channel) => {
51
+ console.log('Data channel ready');
44
52
 
45
- // Or answer an existing offer
46
- const connection = await rdv.answer('my-room-123');
53
+ channel.onmessage = (event) => {
54
+ console.log('Received:', event.data);
55
+ };
56
+
57
+ channel.send('Hello from peer A!');
58
+ });
47
59
 
48
- // Use data channels
49
- connection.on('connect', () => {
50
- const channel = connection.dataChannel('chat');
51
- channel.send('Hello!');
60
+ // Create offer and advertise on topics
61
+ const offerId = await conn.createOffer({
62
+ topics: ['my-app', 'room-123'],
63
+ ttl: 300000 // 5 minutes
52
64
  });
53
65
 
54
- connection.on('datachannel', (channel) => {
55
- if (channel.label === 'chat') {
66
+ console.log('Offer created:', offerId);
67
+ console.log('Share these topics with peers:', ['my-app', 'room-123']);
68
+ ```
69
+
70
+ ### Answering an Offer (Peer B)
71
+
72
+ ```typescript
73
+ import { Rondevu } from '@xtr-dev/rondevu-client';
74
+
75
+ const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
76
+ await client.register();
77
+
78
+ // Discover offers by topic
79
+ const offers = await client.offers.findByTopic('my-app', { limit: 10 });
80
+
81
+ if (offers.length > 0) {
82
+ const offer = offers[0];
83
+
84
+ // Create connection
85
+ const conn = client.createConnection();
86
+
87
+ // Set up event listeners
88
+ conn.on('connecting', () => {
89
+ console.log('Connecting...');
90
+ });
91
+
92
+ conn.on('connected', () => {
93
+ console.log('Connected!');
94
+ });
95
+
96
+ conn.on('datachannel', (channel) => {
97
+ console.log('Data channel ready');
98
+
56
99
  channel.onmessage = (event) => {
57
100
  console.log('Received:', event.data);
58
101
  };
59
- }
102
+
103
+ channel.send('Hello from peer B!');
104
+ });
105
+
106
+ // Answer the offer
107
+ await conn.answer(offer.id, offer.sdp);
108
+ }
109
+ ```
110
+
111
+ ### Connection Events
112
+
113
+ ```typescript
114
+ conn.on('connecting', () => {
115
+ // Connection is being established
116
+ });
117
+
118
+ conn.on('connected', () => {
119
+ // Connection established successfully
120
+ });
121
+
122
+ conn.on('disconnected', () => {
123
+ // Connection lost or closed
124
+ });
125
+
126
+ conn.on('error', (error) => {
127
+ // An error occurred
128
+ console.error('Connection error:', error);
129
+ });
130
+
131
+ conn.on('datachannel', (channel) => {
132
+ // Data channel is ready to use
133
+ });
134
+
135
+ conn.on('track', (event) => {
136
+ // Media track received (for audio/video streaming)
137
+ const stream = event.streams[0];
138
+ videoElement.srcObject = stream;
60
139
  });
61
140
  ```
62
141
 
63
- #### Node.js
142
+ ### Adding Media Tracks
143
+
144
+ ```typescript
145
+ // Get user's camera/microphone
146
+ const stream = await navigator.mediaDevices.getUserMedia({
147
+ video: true,
148
+ audio: true
149
+ });
150
+
151
+ // Add tracks to connection
152
+ stream.getTracks().forEach(track => {
153
+ conn.addTrack(track, stream);
154
+ });
155
+ ```
156
+
157
+ ### Connection Properties
158
+
159
+ ```typescript
160
+ // Get connection state
161
+ console.log(conn.connectionState); // 'connecting', 'connected', 'disconnected', etc.
162
+
163
+ // Get offer ID
164
+ console.log(conn.id);
165
+
166
+ // Get data channel
167
+ console.log(conn.channel);
168
+ ```
169
+
170
+ ### Closing a Connection
171
+
172
+ ```typescript
173
+ conn.close();
174
+ ```
175
+
176
+ ## Platform-Specific Setup
177
+
178
+ ### Node.js 18+ (with native fetch)
179
+
180
+ Works out of the box - no additional setup needed.
181
+
182
+ ### Node.js < 18 (without native fetch)
183
+
184
+ Install node-fetch and provide it to the client:
185
+
186
+ ```bash
187
+ npm install node-fetch
188
+ ```
64
189
 
65
190
  ```typescript
66
191
  import { Rondevu } from '@xtr-dev/rondevu-client';
67
- import wrtc from '@roamhq/wrtc';
68
192
  import fetch from 'node-fetch';
69
193
 
70
- const rdv = new Rondevu({
194
+ const client = new Rondevu({
71
195
  baseUrl: 'https://api.ronde.vu',
72
- fetch: fetch as any,
73
- wrtc: {
74
- RTCPeerConnection: wrtc.RTCPeerConnection,
75
- RTCSessionDescription: wrtc.RTCSessionDescription,
76
- RTCIceCandidate: wrtc.RTCIceCandidate,
196
+ fetch: fetch as any
197
+ });
198
+ ```
199
+
200
+ ### Deno
201
+
202
+ ```typescript
203
+ import { Rondevu } from 'npm:@xtr-dev/rondevu-client';
204
+
205
+ const client = new Rondevu({
206
+ baseUrl: 'https://api.ronde.vu'
207
+ });
208
+ ```
209
+
210
+ ### Bun
211
+
212
+ Works out of the box - no additional setup needed.
213
+
214
+ ### Cloudflare Workers
215
+
216
+ ```typescript
217
+ import { Rondevu } from '@xtr-dev/rondevu-client';
218
+
219
+ export default {
220
+ async fetch(request: Request, env: Env) {
221
+ const client = new Rondevu({
222
+ baseUrl: 'https://api.ronde.vu'
223
+ });
224
+
225
+ const creds = await client.register();
226
+ return new Response(JSON.stringify(creds));
77
227
  }
228
+ };
229
+ ```
230
+
231
+ ## Low-Level API Usage
232
+
233
+ For advanced use cases where you need direct control over the signaling process, you can use the low-level API:
234
+
235
+ ```typescript
236
+ import { Rondevu, BloomFilter } from '@xtr-dev/rondevu-client';
237
+
238
+ const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
239
+
240
+ // Register and get credentials
241
+ const creds = await client.register();
242
+ console.log('Peer ID:', creds.peerId);
243
+
244
+ // Save credentials for later use
245
+ localStorage.setItem('rondevu-creds', JSON.stringify(creds));
246
+
247
+ // Create offer with topics
248
+ const offers = await client.offers.create([{
249
+ sdp: 'v=0...', // Your WebRTC offer SDP
250
+ topics: ['movie-xyz', 'hd-content'],
251
+ ttl: 300000 // 5 minutes
252
+ }]);
253
+
254
+ // Discover peers by topic
255
+ const discovered = await client.offers.findByTopic('movie-xyz', {
256
+ limit: 50
257
+ });
258
+
259
+ console.log(`Found ${discovered.length} peers`);
260
+
261
+ // Use bloom filter to exclude known peers
262
+ const knownPeers = new Set(['peer-id-1', 'peer-id-2']);
263
+ const bloom = new BloomFilter(1024, 3);
264
+ knownPeers.forEach(id => bloom.add(id));
265
+
266
+ const newPeers = await client.offers.findByTopic('movie-xyz', {
267
+ bloomFilter: bloom.toBytes(),
268
+ limit: 50
269
+ });
270
+ ```
271
+
272
+ ## API Reference
273
+
274
+ ### Authentication
275
+
276
+ #### `client.register()`
277
+ Register a new peer and receive credentials.
278
+
279
+ ```typescript
280
+ const creds = await client.register();
281
+ // { peerId: '...', secret: '...' }
282
+ ```
283
+
284
+ ### Topics
285
+
286
+ #### `client.offers.getTopics(options?)`
287
+ List all topics with active peer counts (paginated).
288
+
289
+ ```typescript
290
+ const result = await client.offers.getTopics({
291
+ limit: 50,
292
+ offset: 0
78
293
  });
79
294
 
80
- const connection = await rdv.offer('my-room-123');
295
+ // {
296
+ // topics: [
297
+ // { topic: 'movie-xyz', activePeers: 42 },
298
+ // { topic: 'torrent-abc', activePeers: 15 }
299
+ // ],
300
+ // total: 123,
301
+ // limit: 50,
302
+ // offset: 0
303
+ // }
304
+ ```
305
+
306
+ ### Offers
307
+
308
+ #### `client.offers.create(offers)`
309
+ Create one or more offers with topics.
310
+
311
+ ```typescript
312
+ const offers = await client.offers.create([
313
+ {
314
+ sdp: 'v=0...',
315
+ topics: ['topic-1', 'topic-2'],
316
+ ttl: 300000 // optional, default 5 minutes
317
+ }
318
+ ]);
319
+ ```
320
+
321
+ #### `client.offers.findByTopic(topic, options?)`
322
+ Find offers by topic with optional bloom filter.
81
323
 
82
- connection.on('connect', () => {
83
- const channel = connection.dataChannel('chat');
84
- channel.send('Hello from Node.js!');
324
+ ```typescript
325
+ const offers = await client.offers.findByTopic('movie-xyz', {
326
+ limit: 50,
327
+ bloomFilter: bloomBytes // optional
85
328
  });
86
329
  ```
87
330
 
88
- ### API
331
+ #### `client.offers.getMine()`
332
+ Get all offers owned by the authenticated peer.
333
+
334
+ ```typescript
335
+ const myOffers = await client.offers.getMine();
336
+ ```
337
+
338
+ #### `client.offers.heartbeat(offerId)`
339
+ Update last_seen timestamp for an offer.
89
340
 
90
- **Main Methods:**
91
- - `rdv.offer(id)` - Create an offer with custom ID
92
- - `rdv.answer(id)` - Answer an existing offer by ID
341
+ ```typescript
342
+ await client.offers.heartbeat(offerId);
343
+ ```
93
344
 
94
- **Connection Events:**
95
- - `connect` - Connection established
96
- - `disconnect` - Connection closed
97
- - `error` - Connection error
98
- - `datachannel` - New data channel received
99
- - `stream` - Media stream received
345
+ #### `client.offers.delete(offerId)`
346
+ Delete a specific offer.
100
347
 
101
- **Connection Methods:**
102
- - `connection.dataChannel(label)` - Get or create data channel
103
- - `connection.addStream(stream)` - Add media stream
104
- - `connection.close()` - Close connection
348
+ ```typescript
349
+ await client.offers.delete(offerId);
350
+ ```
105
351
 
106
- ### Version Compatibility
352
+ #### `client.offers.answer(offerId, sdp)`
353
+ Answer an offer (locks it to answerer).
107
354
 
108
- The client automatically checks server compatibility via the `/health` endpoint. If the server version is incompatible, an error will be thrown during initialization.
355
+ ```typescript
356
+ await client.offers.answer(offerId, answerSdp);
357
+ ```
358
+
359
+ #### `client.offers.getAnswers()`
360
+ Poll for answers to your offers.
361
+
362
+ ```typescript
363
+ const answers = await client.offers.getAnswers();
364
+ ```
365
+
366
+ ### ICE Candidates
367
+
368
+ #### `client.offers.addIceCandidates(offerId, candidates)`
369
+ Post ICE candidates for an offer.
370
+
371
+ ```typescript
372
+ await client.offers.addIceCandidates(offerId, [
373
+ 'candidate:1 1 UDP...'
374
+ ]);
375
+ ```
376
+
377
+ #### `client.offers.getIceCandidates(offerId, since?)`
378
+ Get ICE candidates from the other peer.
379
+
380
+ ```typescript
381
+ const candidates = await client.offers.getIceCandidates(offerId);
382
+ ```
383
+
384
+ ### Bloom Filter
385
+
386
+ ```typescript
387
+ import { BloomFilter } from '@xtr-dev/rondevu-client';
388
+
389
+ // Create filter: size=1024 bits, hash=3 functions
390
+ const bloom = new BloomFilter(1024, 3);
391
+
392
+ // Add items
393
+ bloom.add('peer-id-1');
394
+ bloom.add('peer-id-2');
395
+
396
+ // Test membership
397
+ bloom.test('peer-id-1'); // true (probably)
398
+ bloom.test('unknown'); // false (definitely)
399
+
400
+ // Export for API
401
+ const bytes = bloom.toBytes();
402
+ ```
403
+
404
+ ## TypeScript
405
+
406
+ All types are exported:
407
+
408
+ ```typescript
409
+ import type {
410
+ Credentials,
411
+ Offer,
412
+ CreateOfferRequest,
413
+ TopicInfo,
414
+ IceCandidate,
415
+ FetchFunction,
416
+ RondevuOptions,
417
+ ConnectionOptions,
418
+ RondevuConnectionEvents
419
+ } from '@xtr-dev/rondevu-client';
420
+ ```
421
+
422
+ ## Environment Compatibility
423
+
424
+ The client library is designed to work across different JavaScript runtimes:
425
+
426
+ | Environment | Native Fetch | Custom Fetch Needed |
427
+ |-------------|--------------|---------------------|
428
+ | Modern Browsers | ✅ Yes | ❌ No |
429
+ | Node.js 18+ | ✅ Yes | ❌ No |
430
+ | Node.js < 18 | ❌ No | ✅ Yes (node-fetch) |
431
+ | Deno | ✅ Yes | ❌ No |
432
+ | Bun | ✅ Yes | ❌ No |
433
+ | Cloudflare Workers | ✅ Yes | ❌ No |
434
+
435
+ **If your environment doesn't have native fetch:**
436
+
437
+ ```bash
438
+ npm install node-fetch
439
+ ```
440
+
441
+ ```typescript
442
+ import { Rondevu } from '@xtr-dev/rondevu-client';
443
+ import fetch from 'node-fetch';
444
+
445
+ const client = new Rondevu({
446
+ baseUrl: 'https://rondevu.xtrdev.workers.dev',
447
+ fetch: fetch as any
448
+ });
449
+ ```
109
450
 
110
- ### License
451
+ ## License
111
452
 
112
453
  MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ export interface Credentials {
2
+ peerId: string;
3
+ secret: string;
4
+ }
5
+ export type FetchFunction = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
6
+ export declare class RondevuAuth {
7
+ private baseUrl;
8
+ private fetchFn;
9
+ constructor(baseUrl: string, fetchFn?: FetchFunction);
10
+ /**
11
+ * Register a new peer and receive credentials
12
+ */
13
+ register(): Promise<Credentials>;
14
+ /**
15
+ * Create Authorization header value
16
+ */
17
+ static createAuthHeader(credentials: Credentials): string;
18
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,39 @@
1
+ export class RondevuAuth {
2
+ constructor(baseUrl, fetchFn) {
3
+ this.baseUrl = baseUrl;
4
+ // Use provided fetch or fall back to global fetch
5
+ this.fetchFn = fetchFn || ((...args) => {
6
+ if (typeof globalThis.fetch === 'function') {
7
+ return globalThis.fetch(...args);
8
+ }
9
+ throw new Error('fetch is not available. Please provide a fetch implementation in the constructor options.');
10
+ });
11
+ }
12
+ /**
13
+ * Register a new peer and receive credentials
14
+ */
15
+ async register() {
16
+ const response = await this.fetchFn(`${this.baseUrl}/register`, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ body: JSON.stringify({}),
22
+ });
23
+ if (!response.ok) {
24
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
25
+ throw new Error(`Registration failed: ${error.error || response.statusText}`);
26
+ }
27
+ const data = await response.json();
28
+ return {
29
+ peerId: data.peerId,
30
+ secret: data.secret,
31
+ };
32
+ }
33
+ /**
34
+ * Create Authorization header value
35
+ */
36
+ static createAuthHeader(credentials) {
37
+ return `Bearer ${credentials.peerId}:${credentials.secret}`;
38
+ }
39
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Simple bloom filter implementation for peer ID exclusion
3
+ * Uses multiple hash functions for better distribution
4
+ */
5
+ export declare class BloomFilter {
6
+ private bits;
7
+ private size;
8
+ private numHashes;
9
+ constructor(size?: number, numHashes?: number);
10
+ /**
11
+ * Add a peer ID to the filter
12
+ */
13
+ add(peerId: string): void;
14
+ /**
15
+ * Test if peer ID might be in the filter
16
+ */
17
+ test(peerId: string): boolean;
18
+ /**
19
+ * Get raw bits for transmission
20
+ */
21
+ toBytes(): Uint8Array;
22
+ /**
23
+ * Convert to base64 for URL parameters
24
+ */
25
+ toBase64(): string;
26
+ /**
27
+ * Simple hash function (FNV-1a variant)
28
+ */
29
+ private hash;
30
+ }
package/dist/bloom.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Simple bloom filter implementation for peer ID exclusion
3
+ * Uses multiple hash functions for better distribution
4
+ */
5
+ export class BloomFilter {
6
+ constructor(size = 1024, numHashes = 3) {
7
+ this.size = size;
8
+ this.numHashes = numHashes;
9
+ this.bits = new Uint8Array(Math.ceil(size / 8));
10
+ }
11
+ /**
12
+ * Add a peer ID to the filter
13
+ */
14
+ add(peerId) {
15
+ for (let i = 0; i < this.numHashes; i++) {
16
+ const hash = this.hash(peerId, i);
17
+ const index = hash % this.size;
18
+ const byteIndex = Math.floor(index / 8);
19
+ const bitIndex = index % 8;
20
+ this.bits[byteIndex] |= 1 << bitIndex;
21
+ }
22
+ }
23
+ /**
24
+ * Test if peer ID might be in the filter
25
+ */
26
+ test(peerId) {
27
+ for (let i = 0; i < this.numHashes; i++) {
28
+ const hash = this.hash(peerId, i);
29
+ const index = hash % this.size;
30
+ const byteIndex = Math.floor(index / 8);
31
+ const bitIndex = index % 8;
32
+ if (!(this.bits[byteIndex] & (1 << bitIndex))) {
33
+ return false;
34
+ }
35
+ }
36
+ return true;
37
+ }
38
+ /**
39
+ * Get raw bits for transmission
40
+ */
41
+ toBytes() {
42
+ return this.bits;
43
+ }
44
+ /**
45
+ * Convert to base64 for URL parameters
46
+ */
47
+ toBase64() {
48
+ // Convert Uint8Array to regular array then to string
49
+ const binaryString = String.fromCharCode(...Array.from(this.bits));
50
+ // Use btoa for browser, or Buffer for Node.js
51
+ if (typeof btoa !== 'undefined') {
52
+ return btoa(binaryString);
53
+ }
54
+ else if (typeof Buffer !== 'undefined') {
55
+ return Buffer.from(this.bits).toString('base64');
56
+ }
57
+ else {
58
+ // Fallback: manual base64 encoding
59
+ throw new Error('No base64 encoding available');
60
+ }
61
+ }
62
+ /**
63
+ * Simple hash function (FNV-1a variant)
64
+ */
65
+ hash(str, seed) {
66
+ let hash = 2166136261 ^ seed;
67
+ for (let i = 0; i < str.length; i++) {
68
+ hash ^= str.charCodeAt(i);
69
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
70
+ }
71
+ return hash >>> 0;
72
+ }
73
+ }
package/dist/client.d.ts CHANGED
@@ -108,4 +108,19 @@ export declare class RondevuAPI {
108
108
  * ```
109
109
  */
110
110
  health(): Promise<HealthResponse>;
111
+ /**
112
+ * Ends a session by deleting the offer from the server
113
+ *
114
+ * @param code - The offer code
115
+ * @returns Success confirmation
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
120
+ * await api.leave('my-offer-code');
121
+ * ```
122
+ */
123
+ leave(code: string): Promise<{
124
+ success: boolean;
125
+ }>;
111
126
  }