Simplify client: remove topics, ID-based connections only

- Remove join(), listTopics(), listSessions() methods
- Simplify to just create(id) and connect(id)
- Remove topic-related types and interfaces
- Add automatic version checking against server
- Update README with simplified API
- Client version: 0.3.2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-12 23:17:51 +01:00
parent 6466a6f52a
commit c8e5e4d17a
7 changed files with 122 additions and 292 deletions

View File

@@ -1,8 +1,8 @@
# Rondevu # Rondevu
🎯 **Simple WebRTC peer signaling and discovery** 🎯 **Simple WebRTC peer signaling**
Meet peers by topic, by peer ID, or by connection ID. Connect peers directly by ID with automatic WebRTC negotiation.
**Related repositories:** **Related repositories:**
- [rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server - [rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server
@@ -30,68 +30,57 @@ npm install @xtr-dev/rondevu-client
import { Rondevu } from '@xtr-dev/rondevu-client'; import { Rondevu } from '@xtr-dev/rondevu-client';
const rdv = new Rondevu({ const rdv = new Rondevu({
baseUrl: 'https://server.com', baseUrl: 'https://api.ronde.vu',
rtcConfig: { rtcConfig: {
iceServers: [ iceServers: [
// your ICE servers here
{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }
{
urls: 'turn:relay1.example.com:3480',
username: 'example',
credential: 'example'
}
] ]
} }
}); });
// Connect by topic // Create a connection with custom ID
const conn = await rdv.join('room'); const connection = await rdv.create('my-room-123');
// Or connect by ID // Or connect to an existing connection
const conn = await rdv.connect('meeting-123'); const connection = await rdv.connect('my-room-123');
// Use the connection // Use data channels
conn.on('connect', () => { connection.on('connect', () => {
const channel = conn.dataChannel('chat'); const channel = connection.dataChannel('chat');
channel.send('Hello!'); channel.send('Hello!');
}); });
connection.on('datachannel', (channel) => {
if (channel.label === 'chat') {
channel.onmessage = (event) => {
console.log('Received:', event.data);
};
}
});
``` ```
#### Node.js #### Node.js
In Node.js, you need to provide a WebRTC polyfill since WebRTC APIs are not natively available:
```bash
npm install @roamhq/wrtc
# or
npm install wrtc
```
```typescript ```typescript
import { Rondevu } from '@xtr-dev/rondevu-client'; import { Rondevu } from '@xtr-dev/rondevu-client';
import wrtc from '@roamhq/wrtc'; import wrtc from '@roamhq/wrtc';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
const rdv = new Rondevu({ const rdv = new Rondevu({
baseUrl: 'https://server.com', baseUrl: 'https://api.ronde.vu',
fetch: fetch as any, fetch: fetch as any,
wrtc: { wrtc: {
RTCPeerConnection: wrtc.RTCPeerConnection, RTCPeerConnection: wrtc.RTCPeerConnection,
RTCSessionDescription: wrtc.RTCSessionDescription, RTCSessionDescription: wrtc.RTCSessionDescription,
RTCIceCandidate: wrtc.RTCIceCandidate, RTCIceCandidate: wrtc.RTCIceCandidate,
},
rtcConfig: {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
} }
}); });
// Rest is the same as browser usage const connection = await rdv.create('my-room-123');
const conn = await rdv.join('room');
conn.on('connect', () => { connection.on('connect', () => {
const channel = conn.dataChannel('chat'); const channel = connection.dataChannel('chat');
channel.send('Hello from Node.js!'); channel.send('Hello from Node.js!');
}); });
``` ```
@@ -99,23 +88,24 @@ conn.on('connect', () => {
### API ### API
**Main Methods:** **Main Methods:**
- `rdv.join(topic)` - Auto-connect to first peer in topic - `rdv.create(id)` - Create connection with custom ID
- `rdv.join(topic, {filter})` - Connect to specific peer by ID - `rdv.connect(id)` - Connect to existing connection by ID
- `rdv.create(id, topic)` - Create connection for others to join
- `rdv.connect(id)` - Join connection by ID
**Connection Events:** **Connection Events:**
- `connect` - Connection established - `connect` - Connection established
- `disconnect` - Connection closed - `disconnect` - Connection closed
- `datachannel` - Remote peer created data channel - `error` - Connection error
- `stream` - Remote media stream received - `datachannel` - New data channel received
- `error` - Error occurred - `stream` - Media stream received
**Connection Methods:** **Connection Methods:**
- `conn.dataChannel(label)` - Get or create data channel - `connection.dataChannel(label)` - Get or create data channel
- `conn.addStream(stream)` - Add media stream - `connection.addStream(stream)` - Add media stream
- `conn.getPeerConnection()` - Get underlying RTCPeerConnection - `connection.close()` - Close connection
- `conn.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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/rondevu-client", "name": "@xtr-dev/rondevu-client",
"version": "0.3.1", "version": "0.3.2",
"description": "TypeScript client for Rondevu peer signaling and discovery server", "description": "TypeScript client for Rondevu peer signaling and discovery server",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -1,7 +1,5 @@
import { import {
RondevuClientOptions, RondevuClientOptions,
ListTopicsResponse,
ListSessionsResponse,
CreateOfferRequest, CreateOfferRequest,
CreateOfferResponse, CreateOfferResponse,
AnswerRequest, AnswerRequest,
@@ -16,7 +14,7 @@ import {
} from './types.js'; } from './types.js';
/** /**
* HTTP API client for Rondevu peer signaling and discovery server * HTTP API client for Rondevu peer signaling server
*/ */
export class RondevuAPI { export class RondevuAPI {
private readonly baseUrl: string; private readonly baseUrl: string;
@@ -66,7 +64,7 @@ export class RondevuAPI {
/** /**
* Gets server version information * Gets server version information
* *
* @returns Server version (git commit hash) * @returns Server version
* *
* @example * @example
* ```typescript * ```typescript
@@ -82,82 +80,33 @@ export class RondevuAPI {
} }
/** /**
* Lists all topics with peer counts * Creates a new offer
* *
* @param page - Page number (starting from 1) * @param request - Offer details including peer ID, signaling data, and optional custom code
* @param limit - Results per page (max 1000) * @returns Unique offer code (UUID or custom code)
* @returns List of topics with pagination info
* *
* @example * @example
* ```typescript * ```typescript
* const api = new RondevuAPI({ baseUrl: 'https://example.com' }); * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
* const { topics, pagination } = await api.listTopics(); * const { code } = await api.createOffer({
* console.log(`Found ${topics.length} topics`);
* ```
*/
async listTopics(page = 1, limit = 100): Promise<ListTopicsResponse> {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
return this.request<ListTopicsResponse>(`/topics?${params}`, {
method: 'GET',
});
}
/**
* Discovers available peers for a given topic
*
* @param topic - Topic identifier
* @returns List of available sessions
*
* @example
* ```typescript
* const api = new RondevuAPI({ baseUrl: 'https://example.com' });
* const { sessions } = await api.listSessions('my-room');
* const otherPeers = sessions.filter(s => s.peerId !== myPeerId);
* ```
*/
async listSessions(topic: string): Promise<ListSessionsResponse> {
return this.request<ListSessionsResponse>(`/${encodeURIComponent(topic)}/sessions`, {
method: 'GET',
});
}
/**
* Announces peer availability and creates a new session
*
* @param topic - Topic identifier for grouping peers (max 1024 characters)
* @param request - Offer details including peer ID and signaling data
* @returns Unique session code (UUID)
*
* @example
* ```typescript
* const api = new RondevuAPI({ baseUrl: 'https://example.com' });
* const { code } = await api.createOffer('my-room', {
* peerId: 'peer-123', * peerId: 'peer-123',
* offer: signalingData * offer: signalingData,
* code: 'my-custom-code' // optional
* }); * });
* console.log('Session code:', code); * console.log('Offer code:', code);
* ``` * ```
*/ */
async createOffer( async createOffer(request: CreateOfferRequest): Promise<CreateOfferResponse> {
topic: string, return this.request<CreateOfferResponse>('/offer', {
request: CreateOfferRequest
): Promise<CreateOfferResponse> {
return this.request<CreateOfferResponse>(
`/${encodeURIComponent(topic)}/offer`,
{
method: 'POST', method: 'POST',
body: JSON.stringify(request), body: JSON.stringify(request),
} });
);
} }
/** /**
* Sends an answer or candidate to an existing session * Sends an answer or candidate to an existing offer
* *
* @param request - Answer details including session code and signaling data * @param request - Answer details including offer code and signaling data
* @returns Success confirmation * @returns Success confirmation
* *
* @example * @example
@@ -166,14 +115,14 @@ export class RondevuAPI {
* *
* // Send answer * // Send answer
* await api.sendAnswer({ * await api.sendAnswer({
* code: sessionCode, * code: offerCode,
* answer: answerData, * answer: answerData,
* side: 'answerer' * side: 'answerer'
* }); * });
* *
* // Send candidate * // Send candidate
* await api.sendAnswer({ * await api.sendAnswer({
* code: sessionCode, * code: offerCode,
* candidate: candidateData, * candidate: candidateData,
* side: 'offerer' * side: 'offerer'
* }); * });
@@ -187,24 +136,24 @@ export class RondevuAPI {
} }
/** /**
* Polls for session data from the other peer * Polls for offer data from the other peer
* *
* @param code - Session UUID * @param code - Offer code
* @param side - Which side is polling ('offerer' or 'answerer') * @param side - Which side is polling ('offerer' or 'answerer')
* @returns Session data including offers, answers, and candidates * @returns Offer data including offers, answers, and candidates
* *
* @example * @example
* ```typescript * ```typescript
* const api = new RondevuAPI({ baseUrl: 'https://example.com' }); * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
* *
* // Offerer polls for answer * // Offerer polls for answer
* const offererData = await api.poll(sessionCode, 'offerer'); * const offererData = await api.poll(offerCode, 'offerer');
* if (offererData.answer) { * if (offererData.answer) {
* console.log('Received answer:', offererData.answer); * console.log('Received answer:', offererData.answer);
* } * }
* *
* // Answerer polls for offer * // Answerer polls for offer
* const answererData = await api.poll(sessionCode, 'answerer'); * const answererData = await api.poll(offerCode, 'answerer');
* console.log('Received offer:', answererData.offer); * console.log('Received offer:', answererData.offer);
* ``` * ```
*/ */
@@ -220,15 +169,16 @@ export class RondevuAPI {
} }
/** /**
* Checks server health * Checks server health and version
* *
* @returns Health status and timestamp * @returns Health status, timestamp, and version
* *
* @example * @example
* ```typescript * ```typescript
* const api = new RondevuAPI({ baseUrl: 'https://example.com' }); * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
* const health = await api.health(); * const health = await api.health();
* console.log('Server status:', health.status); * console.log('Server status:', health.status);
* console.log('Server version:', health.version);
* ``` * ```
*/ */
async health(): Promise<HealthResponse> { async health(): Promise<HealthResponse> {

View File

@@ -7,7 +7,6 @@ import { RondevuConnectionParams, WebRTCPolyfill } from './types.js';
*/ */
export class RondevuConnection extends EventEmitter { export class RondevuConnection extends EventEmitter {
readonly id: string; readonly id: string;
readonly topic: string;
readonly role: 'offerer' | 'answerer'; readonly role: 'offerer' | 'answerer';
readonly remotePeerId: string; readonly remotePeerId: string;
@@ -27,7 +26,6 @@ export class RondevuConnection extends EventEmitter {
constructor(params: RondevuConnectionParams, client: RondevuAPI) { constructor(params: RondevuConnectionParams, client: RondevuAPI) {
super(); super();
this.id = params.id; this.id = params.id;
this.topic = params.topic;
this.role = params.role; this.role = params.role;
this.pc = params.pc; this.pc = params.pc;
this.localPeerId = params.localPeerId; this.localPeerId = params.localPeerId;

View File

@@ -16,18 +16,12 @@ export { RondevuAPI } from './client.js';
export type { export type {
// WebRTC types // WebRTC types
RondevuOptions, RondevuOptions,
JoinOptions,
ConnectionRole, ConnectionRole,
RondevuConnectionParams, RondevuConnectionParams,
RondevuConnectionEvents, RondevuConnectionEvents,
WebRTCPolyfill, WebRTCPolyfill,
// Signaling types // Signaling types
Side, Side,
Session,
TopicInfo,
Pagination,
ListTopicsResponse,
ListSessionsResponse,
CreateOfferRequest, CreateOfferRequest,
CreateOfferResponse, CreateOfferResponse,
AnswerRequest, AnswerRequest,

View File

@@ -1,6 +1,6 @@
import { RondevuAPI } from './client.js'; import { RondevuAPI } from './client.js';
import { RondevuConnection } from './connection.js'; import { RondevuConnection } from './connection.js';
import { RondevuOptions, JoinOptions, RondevuConnectionParams, WebRTCPolyfill } from './types.js'; import { RondevuOptions, RondevuConnectionParams, WebRTCPolyfill } from './types.js';
/** /**
* Main Rondevu WebRTC client with automatic connection management * Main Rondevu WebRTC client with automatic connection management
@@ -49,6 +49,43 @@ export class Rondevu {
'Install: npm install @roamhq/wrtc or npm install wrtc' 'Install: npm install @roamhq/wrtc or npm install wrtc'
); );
} }
// Check server version compatibility (async, don't block constructor)
this.checkServerVersion().catch(() => {
// Silently fail version check - connection will work even if version check fails
});
}
/**
* Check server version compatibility
*/
private async checkServerVersion(): Promise<void> {
try {
const { version: serverVersion } = await this.api.health();
const clientVersion = '0.3.2'; // Should match package.json
if (!this.isVersionCompatible(clientVersion, serverVersion)) {
console.warn(
`[Rondevu] Version mismatch: client v${clientVersion}, server v${serverVersion}. ` +
'This may cause compatibility issues.'
);
}
} catch (error) {
// Version check failed - server might not support /health endpoint
console.debug('[Rondevu] Could not check server version');
}
}
/**
* Check if client and server versions are compatible
* For now, just check major version compatibility
*/
private isVersionCompatible(clientVersion: string, serverVersion: string): boolean {
const clientMajor = parseInt(clientVersion.split('.')[0]);
const serverMajor = parseInt(serverVersion.split('.')[0]);
// Major versions must match
return clientMajor === serverMajor;
} }
/** /**
@@ -67,11 +104,10 @@ export class Rondevu {
/** /**
* Create a new connection (offerer role) * Create a new connection (offerer role)
* @param id - Connection identifier * @param id - Connection identifier (custom code)
* @param topic - Topic name for grouping connections
* @returns Promise that resolves to RondevuConnection * @returns Promise that resolves to RondevuConnection
*/ */
async create(id: string, topic: string): Promise<RondevuConnection> { async create(id: string): Promise<RondevuConnection> {
// Create peer connection // Create peer connection
const pc = new this.RTCPeerConnection(this.rtcConfig); const pc = new this.RTCPeerConnection(this.rtcConfig);
@@ -85,8 +121,8 @@ export class Rondevu {
// Wait for ICE gathering to complete // Wait for ICE gathering to complete
await this.waitForIceGathering(pc); await this.waitForIceGathering(pc);
// Create session on server with custom code // Create offer on server with custom code
await this.api.createOffer(topic, { await this.api.createOffer({
peerId: this.peerId, peerId: this.peerId,
offer: pc.localDescription!.sdp, offer: pc.localDescription!.sdp,
code: id, code: id,
@@ -95,7 +131,6 @@ export class Rondevu {
// Create connection object // Create connection object
const connectionParams: RondevuConnectionParams = { const connectionParams: RondevuConnectionParams = {
id, id,
topic,
role: 'offerer', role: 'offerer',
pc, pc,
localPeerId: this.peerId, localPeerId: this.peerId,
@@ -114,16 +149,16 @@ export class Rondevu {
} }
/** /**
* Connect to an existing connection by ID (answerer role) * Connect to an existing offer by ID (answerer role)
* @param id - Connection identifier * @param id - Offer code
* @returns Promise that resolves to RondevuConnection * @returns Promise that resolves to RondevuConnection
*/ */
async connect(id: string): Promise<RondevuConnection> { async connect(id: string): Promise<RondevuConnection> {
// Poll server to get session by ID // Poll server to get offer by ID
const sessionData = await this.findSessionByIdWithClient(id, this.api); const offerData = await this.findOfferById(id);
if (!sessionData) { if (!offerData) {
throw new Error(`Connection ${id} not found or expired`); throw new Error(`Offer ${id} not found or expired`);
} }
// Create peer connection // Create peer connection
@@ -132,7 +167,7 @@ export class Rondevu {
// Set remote offer // Set remote offer
await pc.setRemoteDescription({ await pc.setRemoteDescription({
type: 'offer', type: 'offer',
sdp: sessionData.offer, sdp: offerData.offer,
}); });
// Generate answer // Generate answer
@@ -152,11 +187,10 @@ export class Rondevu {
// Create connection object // Create connection object
const connectionParams: RondevuConnectionParams = { const connectionParams: RondevuConnectionParams = {
id, id,
topic: sessionData.topic || 'unknown',
role: 'answerer', role: 'answerer',
pc, pc,
localPeerId: this.peerId, localPeerId: this.peerId,
remotePeerId: sessionData.peerId, remotePeerId: '', // Will be determined from peerId in offer
pollingInterval: this.pollingInterval, pollingInterval: this.pollingInterval,
connectionTimeout: this.connectionTimeout, connectionTimeout: this.connectionTimeout,
wrtc: this.wrtc, wrtc: this.wrtc,
@@ -170,65 +204,6 @@ export class Rondevu {
return connection; return connection;
} }
/**
* Join a topic and discover available peers (answerer role)
* @param topic - Topic name
* @param options - Optional join options for filtering and selection
* @returns Promise that resolves to RondevuConnection
*/
async join(topic: string, options?: JoinOptions): Promise<RondevuConnection> {
// List sessions in topic
const { sessions } = await this.api.listSessions(topic);
// Filter out self (sessions with our peer ID)
let availableSessions = sessions.filter(
session => session.peerId !== this.peerId
);
// Apply custom filter if provided
if (options?.filter) {
availableSessions = availableSessions.filter(options.filter);
}
if (availableSessions.length === 0) {
throw new Error(`No available peers in topic: ${topic}`);
}
// Select session based on strategy
const selectedSession = this.selectSession(
availableSessions,
options?.select || 'first'
);
// Connect to selected session
return this.connect(selectedSession.code);
}
/**
* Select a session based on strategy
*/
private selectSession(
sessions: Array<{ code: string; peerId: string; createdAt: number }>,
strategy: 'first' | 'newest' | 'oldest' | 'random'
): { code: string; peerId: string; createdAt: number } {
switch (strategy) {
case 'first':
return sessions[0];
case 'newest':
return sessions.reduce((newest, session) =>
session.createdAt > newest.createdAt ? session : newest
);
case 'oldest':
return sessions.reduce((oldest, session) =>
session.createdAt < oldest.createdAt ? session : oldest
);
case 'random':
return sessions[Math.floor(Math.random() * sessions.length)];
default:
return sessions[0];
}
}
/** /**
* Wait for ICE gathering to complete * Wait for ICE gathering to complete
*/ */
@@ -256,36 +231,25 @@ export class Rondevu {
} }
/** /**
* Find a session by connection ID * Find an offer by code
* This requires polling since we don't know which topic it's in
*/ */
private async findSessionByIdWithClient( private async findOfferById(id: string): Promise<{
id: string,
client: RondevuAPI
): Promise<{
code: string;
peerId: string;
offer: string; offer: string;
topic?: string;
} | null> { } | null> {
try { try {
// Try to poll for the session directly // Poll for the offer directly
// The poll endpoint should return the session data const response = await this.api.poll(id, 'answerer');
const response = await client.poll(id, 'answerer');
const answererResponse = response as { offer: string; offerCandidates: string[] }; const answererResponse = response as { offer: string; offerCandidates: string[] };
if (answererResponse.offer) { if (answererResponse.offer) {
return { return {
code: id,
peerId: '', // Will be populated from session data
offer: answererResponse.offer, offer: answererResponse.offer,
topic: undefined,
}; };
} }
return null; return null;
} catch (err) { } catch (err) {
throw new Error(`Failed to find session ${id}: ${(err as Error).message}`); throw new Error(`Failed to find offer ${id}: ${(err as Error).message}`);
} }
} }
} }

View File

@@ -8,64 +8,7 @@
export type Side = 'offerer' | 'answerer'; export type Side = 'offerer' | 'answerer';
/** /**
* Session information returned from discovery endpoints * Request body for POST /offer
*/
export interface Session {
/** Unique session identifier (UUID) */
code: string;
/** Peer identifier/metadata */
peerId: string;
/** Signaling data for peer connection */
offer: string;
/** Additional signaling data from offerer */
offerCandidates: string[];
/** Unix timestamp when session was created */
createdAt: number;
/** Unix timestamp when session expires */
expiresAt: number;
}
/**
* Topic information with peer count
*/
export interface TopicInfo {
/** Topic identifier */
topic: string;
/** Number of available peers in this topic */
count: number;
}
/**
* Pagination information
*/
export interface Pagination {
/** Current page number */
page: number;
/** Results per page */
limit: number;
/** Total number of results */
total: number;
/** Whether there are more results available */
hasMore: boolean;
}
/**
* Response from GET / - list all topics
*/
export interface ListTopicsResponse {
topics: TopicInfo[];
pagination: Pagination;
}
/**
* Response from GET /:topic/sessions - list sessions in a topic
*/
export interface ListSessionsResponse {
sessions: Session[];
}
/**
* Request body for POST /:topic/offer
*/ */
export interface CreateOfferRequest { export interface CreateOfferRequest {
/** Peer identifier/metadata (max 1024 characters) */ /** Peer identifier/metadata (max 1024 characters) */
@@ -77,7 +20,7 @@ export interface CreateOfferRequest {
} }
/** /**
* Response from POST /:topic/offer * Response from POST /offer
*/ */
export interface CreateOfferResponse { export interface CreateOfferResponse {
/** Unique session identifier (UUID) */ /** Unique session identifier (UUID) */
@@ -154,6 +97,7 @@ export interface VersionResponse {
export interface HealthResponse { export interface HealthResponse {
status: 'ok'; status: 'ok';
timestamp: number; timestamp: number;
version: string;
} }
/** /**
@@ -206,16 +150,6 @@ export interface RondevuOptions {
wrtc?: WebRTCPolyfill; wrtc?: WebRTCPolyfill;
} }
/**
* Options for joining a topic
*/
export interface JoinOptions {
/** Filter function to select specific sessions */
filter?: (session: { code: string; peerId: string }) => boolean;
/** Selection strategy for choosing a session */
select?: 'first' | 'newest' | 'oldest' | 'random';
}
/** /**
* Connection role - whether this peer is creating or answering * Connection role - whether this peer is creating or answering
*/ */
@@ -226,7 +160,7 @@ export type ConnectionRole = 'offerer' | 'answerer';
*/ */
export interface RondevuConnectionParams { export interface RondevuConnectionParams {
id: string; id: string;
topic: string; topic?: string;
role: ConnectionRole; role: ConnectionRole;
pc: RTCPeerConnection; pc: RTCPeerConnection;
localPeerId: string; localPeerId: string;