Add high-level Rondevu client with three connection methods

- Add Rondevu class with join(), connect(), and create() methods
- Add RondevuConnection wrapper for WebRTC connections
- Add event emitter for connection events
- Update to ES modules (ESNext) for Vite compatibility
- Simplify README to be more concise
- Update package.json to specify type: module

Three ways to connect:
- join(topic) for auto-discovery
- join(topic, {filter}) for peer ID filtering
- create(id)/connect(id) for direct connections

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-07 21:20:27 +01:00
parent bfb2ec2172
commit 06fa957ccc
9 changed files with 818 additions and 251 deletions

254
README.md
View File

@@ -1,234 +1,58 @@
# @xtr-dev/rondevu-client
# Rondevu
TypeScript client for interacting with the Rondevu peer signaling and discovery server. Provides a simple, type-safe API for WebRTC peer discovery and connection establishment.
🎯 Meet WebRTC peers by topic, by peer ID, or by connection ID.
## Installation
## @xtr-dev/rondevu-client
Rondevu HTTP and WebRTC client, for simple peer discovery and connection.
### Install
```bash
npm install @xtr-dev/rondevu-client
```
## Usage
### Basic Setup
### Usage
```typescript
import { RondevuClient } from '@xtr-dev/rondevu-client';
import { Rondevu } from '@xtr-dev/rondevu-client';
const client = new RondevuClient({
baseUrl: 'https://rondevu.example.com',
// Optional: custom origin for session isolation
origin: 'https://myapp.com'
const rdv = new Rondevu({ baseUrl: 'https://server.com' });
// Connect by topic
const conn = await rdv.join('room');
// Or connect by ID
const conn = await rdv.connect('meeting-123');
// Use the connection
conn.on('connect', () => {
const channel = conn.dataChannel('chat');
channel.send('Hello!');
});
```
### Peer Discovery Flow
### API
#### 1. List Available Topics
**Main Methods:**
- `rdv.join(topic)` - Auto-connect to first peer in topic
- `rdv.join(topic, {filter})` - Connect to specific peer by ID
- `rdv.create(id, topic)` - Create connection for others to join
- `rdv.connect(id)` - Join connection by ID
```typescript
// Get all topics with peer counts
const { topics, pagination } = await client.listTopics();
**Connection Events:**
- `connect` - Connection established
- `disconnect` - Connection closed
- `datachannel` - Remote peer created data channel
- `stream` - Remote media stream received
- `error` - Error occurred
topics.forEach(topic => {
console.log(`${topic.topic}: ${topic.count} peers available`);
});
```
**Connection Methods:**
- `conn.dataChannel(label)` - Get or create data channel
- `conn.addStream(stream)` - Add media stream
- `conn.getPeerConnection()` - Get underlying RTCPeerConnection
- `conn.close()` - Close connection
#### 2. Create an Offer (Peer A)
```typescript
// Announce availability in a topic
const { code } = await client.createOffer('my-room', {
info: 'peer-A-unique-id',
offer: webrtcOfferData
});
console.log('Session code:', code);
```
#### 3. Discover Peers (Peer B)
```typescript
// Find available peers in a topic
const { sessions } = await client.listSessions('my-room');
// Filter out your own sessions
const otherPeers = sessions.filter(s => s.info !== 'my-peer-id');
if (otherPeers.length > 0) {
const peer = otherPeers[0];
console.log('Found peer:', peer.info);
}
```
#### 4. Send Answer (Peer B)
```typescript
// Connect to a peer by answering their offer
await client.sendAnswer({
code: peer.code,
answer: webrtcAnswerData,
side: 'answerer'
});
```
#### 5. Poll for Data (Both Peers)
```typescript
// Offerer polls for answer
const offererData = await client.poll(code, 'offerer');
if (offererData.answer) {
console.log('Received answer from peer');
}
// Answerer polls for offer details
const answererData = await client.poll(code, 'answerer');
console.log('Offer candidates:', answererData.offerCandidates);
```
#### 6. Exchange ICE Candidates
```typescript
// Send additional signaling data
await client.sendAnswer({
code: sessionCode,
candidate: iceCandidate,
side: 'offerer' // or 'answerer'
});
```
### Health Check
```typescript
const health = await client.health();
console.log('Server status:', health.status);
console.log('Timestamp:', health.timestamp);
```
## API Reference
### `RondevuClient`
#### Constructor
```typescript
new RondevuClient(options: RondevuClientOptions)
```
**Options:**
- `baseUrl` (string, required): Base URL of the Rondevu server
- `origin` (string, optional): Origin header for session isolation (defaults to baseUrl origin)
- `fetch` (function, optional): Custom fetch implementation (for Node.js)
#### Methods
##### `listTopics(page?, limit?)`
Lists all topics with peer counts.
**Parameters:**
- `page` (number, optional): Page number, default 1
- `limit` (number, optional): Results per page, default 100, max 1000
**Returns:** `Promise<ListTopicsResponse>`
##### `listSessions(topic)`
Discovers available peers for a given topic.
**Parameters:**
- `topic` (string): Topic identifier
**Returns:** `Promise<ListSessionsResponse>`
##### `createOffer(topic, request)`
Announces peer availability and creates a new session.
**Parameters:**
- `topic` (string): Topic identifier (max 256 characters)
- `request` (CreateOfferRequest):
- `info` (string): Peer identifier/metadata (max 1024 characters)
- `offer` (string): WebRTC signaling data
**Returns:** `Promise<CreateOfferResponse>`
##### `sendAnswer(request)`
Sends an answer or candidate to an existing session.
**Parameters:**
- `request` (AnswerRequest):
- `code` (string): Session UUID
- `answer` (string, optional): Answer signaling data
- `candidate` (string, optional): ICE candidate data
- `side` ('offerer' | 'answerer'): Which peer is sending
**Returns:** `Promise<AnswerResponse>`
##### `poll(code, side)`
Polls for session data from the other peer.
**Parameters:**
- `code` (string): Session UUID
- `side` ('offerer' | 'answerer'): Which side is polling
**Returns:** `Promise<PollOffererResponse | PollAnswererResponse>`
##### `health()`
Checks server health.
**Returns:** `Promise<HealthResponse>`
## TypeScript Types
All types are exported from the main package:
```typescript
import {
RondevuClient,
Session,
TopicInfo,
CreateOfferRequest,
AnswerRequest,
PollRequest,
Side,
// ... and more
} from '@xtr-dev/rondevu-client';
```
## Node.js Usage
For Node.js environments (v18+), the built-in fetch is used automatically. For older Node.js versions, provide a fetch implementation:
```typescript
import fetch from 'node-fetch';
import { RondevuClient } from '@xtr-dev/rondevu-client';
const client = new RondevuClient({
baseUrl: 'https://rondevu.example.com',
fetch: fetch as any
});
```
## Error Handling
All API methods throw errors with descriptive messages:
```typescript
try {
await client.createOffer('my-room', {
info: 'peer-id',
offer: data
});
} catch (error) {
console.error('Failed to create offer:', error.message);
}
```
## License
### License
MIT

View File

@@ -1,7 +1,8 @@
{
"name": "@xtr-dev/rondevu-client",
"version": "0.0.2",
"version": "0.0.3",
"description": "TypeScript client for Rondevu peer signaling and discovery server",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {

View File

@@ -99,7 +99,7 @@ export class RondevuClient {
* ```typescript
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
* const { sessions } = await client.listSessions('my-room');
* const otherPeers = sessions.filter(s => s.info !== myPeerId);
* const otherPeers = sessions.filter(s => s.peerId !== myPeerId);
* ```
*/
async listSessions(topic: string): Promise<ListSessionsResponse> {
@@ -111,15 +111,15 @@ export class RondevuClient {
/**
* Announces peer availability and creates a new session
*
* @param topic - Topic identifier for grouping peers (max 256 characters)
* @param request - Offer details including peer info and signaling data
* @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 client = new RondevuClient({ baseUrl: 'https://example.com' });
* const { code } = await client.createOffer('my-room', {
* info: 'peer-123',
* peerId: 'peer-123',
* offer: signalingData
* });
* console.log('Session code:', code);

310
src/connection.ts Normal file
View File

@@ -0,0 +1,310 @@
import { EventEmitter } from './event-emitter';
import { RondevuClient } from './client';
import { RondevuConnectionParams } from './types';
/**
* Represents a WebRTC connection with automatic signaling and ICE exchange
*/
export class RondevuConnection extends EventEmitter {
readonly id: string;
readonly topic: string;
readonly role: 'offerer' | 'answerer';
readonly remotePeerId: string;
private pc: RTCPeerConnection;
private client: RondevuClient;
private localPeerId: string;
private dataChannels: Map<string, RTCDataChannel>;
private pollingInterval?: ReturnType<typeof setInterval>;
private pollingIntervalMs: number;
private connectionTimeoutMs: number;
private connectionTimer?: ReturnType<typeof setTimeout>;
private isPolling: boolean = false;
private isClosed: boolean = false;
constructor(params: RondevuConnectionParams, client: RondevuClient) {
super();
this.id = params.id;
this.topic = params.topic;
this.role = params.role;
this.pc = params.pc;
this.localPeerId = params.localPeerId;
this.remotePeerId = params.remotePeerId;
this.client = client;
this.dataChannels = new Map();
this.pollingIntervalMs = params.pollingInterval;
this.connectionTimeoutMs = params.connectionTimeout;
this.setupEventHandlers();
this.startConnectionTimeout();
}
/**
* Setup RTCPeerConnection event handlers
*/
private setupEventHandlers(): void {
// ICE candidate gathering
this.pc.onicecandidate = (event) => {
if (event.candidate && !this.isClosed) {
this.sendIceCandidate(event.candidate).catch((err) => {
this.emit('error', new Error(`Failed to send ICE candidate: ${err.message}`));
});
}
};
// Connection state changes
this.pc.onconnectionstatechange = () => {
this.handleConnectionStateChange();
};
// Remote data channels
this.pc.ondatachannel = (event) => {
this.handleRemoteDataChannel(event.channel);
};
// Remote media streams
this.pc.ontrack = (event) => {
if (event.streams && event.streams[0]) {
this.emit('stream', event.streams[0]);
}
};
// ICE connection state changes
this.pc.oniceconnectionstatechange = () => {
const state = this.pc.iceConnectionState;
if (state === 'failed' || state === 'closed') {
this.emit('error', new Error(`ICE connection ${state}`));
if (state === 'failed') {
this.close();
}
}
};
}
/**
* Handle RTCPeerConnection state changes
*/
private handleConnectionStateChange(): void {
const state = this.pc.connectionState;
switch (state) {
case 'connected':
this.clearConnectionTimeout();
this.stopPolling();
this.emit('connect');
break;
case 'disconnected':
this.emit('disconnect');
break;
case 'failed':
this.emit('error', new Error('Connection failed'));
this.close();
break;
case 'closed':
this.emit('disconnect');
break;
}
}
/**
* Send an ICE candidate to the remote peer via signaling server
*/
private async sendIceCandidate(candidate: RTCIceCandidate): Promise<void> {
try {
await this.client.sendAnswer({
code: this.id,
candidate: JSON.stringify(candidate.toJSON()),
side: this.role,
});
} catch (err: any) {
throw new Error(`Failed to send ICE candidate: ${err.message}`);
}
}
/**
* Start polling for remote session data (answer/candidates)
*/
startPolling(): void {
if (this.isPolling || this.isClosed) {
return;
}
this.isPolling = true;
// Poll immediately
this.poll().catch((err) => {
this.emit('error', new Error(`Poll error: ${err.message}`));
});
// Set up interval polling
this.pollingInterval = setInterval(() => {
this.poll().catch((err) => {
this.emit('error', new Error(`Poll error: ${err.message}`));
});
}, this.pollingIntervalMs);
}
/**
* Stop polling
*/
private stopPolling(): void {
this.isPolling = false;
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = undefined;
}
}
/**
* Poll the signaling server for remote data
*/
private async poll(): Promise<void> {
if (this.isClosed) {
this.stopPolling();
return;
}
try {
const response = await this.client.poll(this.id, this.role);
if (this.role === 'offerer') {
const offererResponse = response as { answer: string | null; answerCandidates: string[] };
// Apply answer if received and not yet applied
if (offererResponse.answer && !this.pc.currentRemoteDescription) {
await this.pc.setRemoteDescription({
type: 'answer',
sdp: offererResponse.answer,
});
}
// Apply ICE candidates
if (offererResponse.answerCandidates && offererResponse.answerCandidates.length > 0) {
for (const candidateStr of offererResponse.answerCandidates) {
try {
const candidate = JSON.parse(candidateStr);
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
} catch (err) {
console.warn('Failed to add ICE candidate:', err);
}
}
}
} else {
// Answerer role
const answererResponse = response as { offer: string; offerCandidates: string[] };
// Apply ICE candidates from offerer
if (answererResponse.offerCandidates && answererResponse.offerCandidates.length > 0) {
for (const candidateStr of answererResponse.offerCandidates) {
try {
const candidate = JSON.parse(candidateStr);
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
} catch (err) {
console.warn('Failed to add ICE candidate:', err);
}
}
}
}
} catch (err: any) {
// Session not found or expired
if (err.message.includes('404') || err.message.includes('not found')) {
this.emit('error', new Error('Session not found or expired'));
this.close();
}
throw err;
}
}
/**
* Handle remotely created data channel
*/
private handleRemoteDataChannel(channel: RTCDataChannel): void {
this.dataChannels.set(channel.label, channel);
this.emit('datachannel', channel);
}
/**
* Get or create a data channel
*/
dataChannel(label: string, options?: RTCDataChannelInit): RTCDataChannel {
let channel = this.dataChannels.get(label);
if (!channel) {
channel = this.pc.createDataChannel(label, options);
this.dataChannels.set(label, channel);
}
return channel;
}
/**
* Add a local media stream to the connection
*/
addStream(stream: MediaStream): void {
stream.getTracks().forEach(track => {
this.pc.addTrack(track, stream);
});
}
/**
* Get the underlying RTCPeerConnection for advanced usage
*/
getPeerConnection(): RTCPeerConnection {
return this.pc;
}
/**
* Start connection timeout
*/
private startConnectionTimeout(): void {
this.connectionTimer = setTimeout(() => {
if (this.pc.connectionState !== 'connected') {
this.emit('error', new Error('Connection timeout'));
this.close();
}
}, this.connectionTimeoutMs);
}
/**
* Clear connection timeout
*/
private clearConnectionTimeout(): void {
if (this.connectionTimer) {
clearTimeout(this.connectionTimer);
this.connectionTimer = undefined;
}
}
/**
* Close the connection and cleanup resources
*/
close(): void {
if (this.isClosed) {
return;
}
this.isClosed = true;
this.stopPolling();
this.clearConnectionTimeout();
// Close all data channels
this.dataChannels.forEach(dc => {
if (dc.readyState === 'open' || dc.readyState === 'connecting') {
dc.close();
}
});
this.dataChannels.clear();
// Close peer connection
if (this.pc.connectionState !== 'closed') {
this.pc.close();
}
this.emit('disconnect');
}
}

86
src/event-emitter.ts Normal file
View File

@@ -0,0 +1,86 @@
/**
* Simple EventEmitter implementation for browser and Node.js compatibility
*/
export class EventEmitter {
private events: Map<string, Set<Function>>;
constructor() {
this.events = new Map();
}
/**
* Register an event listener
*/
on(event: string, listener: Function): this {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(listener);
return this;
}
/**
* Register a one-time event listener
*/
once(event: string, listener: Function): this {
const onceWrapper = (...args: any[]) => {
this.off(event, onceWrapper);
listener.apply(this, args);
};
return this.on(event, onceWrapper);
}
/**
* Remove an event listener
*/
off(event: string, listener: Function): this {
const listeners = this.events.get(event);
if (listeners) {
listeners.delete(listener);
if (listeners.size === 0) {
this.events.delete(event);
}
}
return this;
}
/**
* Emit an event
*/
emit(event: string, ...args: any[]): boolean {
const listeners = this.events.get(event);
if (!listeners || listeners.size === 0) {
return false;
}
listeners.forEach(listener => {
try {
listener.apply(this, args);
} catch (err) {
console.error(`Error in ${event} event listener:`, err);
}
});
return true;
}
/**
* Remove all listeners for an event (or all events if not specified)
*/
removeAllListeners(event?: string): this {
if (event) {
this.events.delete(event);
} else {
this.events.clear();
}
return this;
}
/**
* Get listener count for an event
*/
listenerCount(event: string): number {
const listeners = this.events.get(event);
return listeners ? listeners.size : 0;
}
}

View File

@@ -1,31 +1,41 @@
/**
* TypeScript client for Rondevu peer signaling server
*
* @example
* ```typescript
* import { RondevuClient } from '@xtr-dev/rondevu-client';
*
* const client = new RondevuClient({
* baseUrl: 'https://rondevu.example.com'
* });
*
* // Create an offer
* const { code } = await client.createOffer('my-room', {
* info: 'peer-123',
* offer: signalingData
* });
*
* // Discover peers
* const { sessions } = await client.listSessions('my-room');
*
* // Send answer
* await client.sendAnswer({
* code: sessions[0].code,
* answer: answerData,
* side: 'answerer'
* });
* ```
* @xtr-dev/rondevu-client
* WebRTC peer signaling and discovery client
*/
// Export main WebRTC client class
export { Rondevu } from './rondevu';
// Export connection class
export { RondevuConnection } from './connection';
// Export low-level signaling client (for advanced usage)
export { RondevuClient } from './client';
export * from './types';
// Export all types
export type {
// WebRTC types
RondevuOptions,
JoinOptions,
ConnectionRole,
RondevuConnectionParams,
RondevuConnectionEvents,
// Signaling types
Side,
Session,
TopicInfo,
Pagination,
ListTopicsResponse,
ListSessionsResponse,
CreateOfferRequest,
CreateOfferResponse,
AnswerRequest,
AnswerResponse,
PollRequest,
PollOffererResponse,
PollAnswererResponse,
PollResponse,
HealthResponse,
ErrorResponse,
RondevuClientOptions,
} from './types';

266
src/rondevu.ts Normal file
View File

@@ -0,0 +1,266 @@
import { RondevuClient } from './client';
import { RondevuConnection } from './connection';
import { RondevuOptions, JoinOptions, RondevuConnectionParams } from './types';
/**
* Main Rondevu WebRTC client with automatic connection management
*/
export class Rondevu {
readonly peerId: string;
private client: RondevuClient;
private rtcConfig?: RTCConfiguration;
private pollingInterval: number;
private connectionTimeout: number;
/**
* Creates a new Rondevu client instance
* @param options - Client configuration options
*/
constructor(options: RondevuOptions) {
this.client = new RondevuClient({
baseUrl: options.baseUrl,
origin: options.origin,
fetch: options.fetch,
});
// Auto-generate peer ID if not provided
this.peerId = options.peerId || this.generatePeerId();
this.rtcConfig = options.rtcConfig;
this.pollingInterval = options.pollingInterval || 1000;
this.connectionTimeout = options.connectionTimeout || 30000;
}
/**
* Generate a unique peer ID
*/
private generatePeerId(): string {
return `rdv_${Math.random().toString(36).substring(2, 14)}`;
}
/**
* Update the peer ID (useful when user identity changes)
*/
updatePeerId(newPeerId: string): void {
(this as any).peerId = newPeerId;
}
/**
* Create a new connection (offerer role)
* @param id - Connection identifier
* @param topic - Topic name for grouping connections
* @returns Promise that resolves to RondevuConnection
*/
async create(id: string, topic: string): Promise<RondevuConnection> {
// Create peer connection
const pc = new RTCPeerConnection(this.rtcConfig);
// Create initial data channel for negotiation (required for offer creation)
pc.createDataChannel('_negotiation');
// Generate offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Wait for ICE gathering to complete
await this.waitForIceGathering(pc);
// Create session on server with custom code
await this.client.createOffer(topic, {
peerId: this.peerId,
offer: pc.localDescription!.sdp,
code: id,
});
// Create connection object
const connectionParams: RondevuConnectionParams = {
id,
topic,
role: 'offerer',
pc,
localPeerId: this.peerId,
remotePeerId: '', // Will be populated when answer is received
pollingInterval: this.pollingInterval,
connectionTimeout: this.connectionTimeout,
};
const connection = new RondevuConnection(connectionParams, this.client);
// Start polling for answer
connection.startPolling();
return connection;
}
/**
* Connect to an existing connection by ID (answerer role)
* @param id - Connection identifier
* @returns Promise that resolves to RondevuConnection
*/
async connect(id: string): Promise<RondevuConnection> {
// Poll server to get session by ID
const sessionData = await this.findSessionById(id);
if (!sessionData) {
throw new Error(`Connection ${id} not found or expired`);
}
// Create peer connection
const pc = new RTCPeerConnection(this.rtcConfig);
// Set remote offer
await pc.setRemoteDescription({
type: 'offer',
sdp: sessionData.offer,
});
// Generate answer
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// Wait for ICE gathering
await this.waitForIceGathering(pc);
// Send answer to server
await this.client.sendAnswer({
code: id,
answer: pc.localDescription!.sdp,
side: 'answerer',
});
// Create connection object
const connectionParams: RondevuConnectionParams = {
id,
topic: sessionData.topic || 'unknown',
role: 'answerer',
pc,
localPeerId: this.peerId,
remotePeerId: sessionData.peerId,
pollingInterval: this.pollingInterval,
connectionTimeout: this.connectionTimeout,
};
const connection = new RondevuConnection(connectionParams, this.client);
// Start polling for ICE candidates
connection.startPolling();
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.client.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
*/
private async waitForIceGathering(pc: RTCPeerConnection): Promise<void> {
if (pc.iceGatheringState === 'complete') {
return;
}
return new Promise((resolve) => {
const checkState = () => {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}
};
pc.addEventListener('icegatheringstatechange', checkState);
// Also set a timeout in case gathering takes too long
setTimeout(() => {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}, 5000);
});
}
/**
* Find a session by connection ID
* This requires polling since we don't know which topic it's in
*/
private async findSessionById(id: string): Promise<{
code: string;
peerId: string;
offer: string;
topic?: string;
} | null> {
try {
// Try to poll for the session directly
// The poll endpoint should return the session data
const response = await this.client.poll(id, 'answerer');
const answererResponse = response as { offer: string; offerCandidates: string[] };
if (answererResponse.offer) {
return {
code: id,
peerId: '', // Will be populated from session data
offer: answererResponse.offer,
topic: undefined,
};
}
return null;
} catch (err) {
throw new Error(`Failed to find session ${id}: ${(err as Error).message}`);
}
}
}

View File

@@ -1,3 +1,7 @@
// ============================================================================
// Signaling Types
// ============================================================================
/**
* Session side - identifies which peer in a connection
*/
@@ -10,7 +14,7 @@ export interface Session {
/** Unique session identifier (UUID) */
code: string;
/** Peer identifier/metadata */
info: string;
peerId: string;
/** Signaling data for peer connection */
offer: string;
/** Additional signaling data from offerer */
@@ -65,9 +69,11 @@ export interface ListSessionsResponse {
*/
export interface CreateOfferRequest {
/** Peer identifier/metadata (max 1024 characters) */
info: string;
peerId: string;
/** Signaling data for peer connection */
offer: string;
/** Optional custom connection code (if not provided, server generates UUID) */
code?: string;
}
/**
@@ -160,3 +166,67 @@ export interface RondevuClientOptions {
/** Optional fetch implementation (for Node.js environments) */
fetch?: typeof fetch;
}
// ============================================================================
// WebRTC Types
// ============================================================================
/**
* Configuration options for Rondevu WebRTC client
*/
export interface RondevuOptions {
/** Base URL of the Rondevu server (e.g., 'https://example.com') */
baseUrl: string;
/** Peer identifier (optional, auto-generated if not provided) */
peerId?: string;
/** Origin header value for session isolation (defaults to baseUrl origin) */
origin?: string;
/** Optional fetch implementation (for Node.js environments) */
fetch?: typeof fetch;
/** WebRTC configuration (ICE servers, etc.) */
rtcConfig?: RTCConfiguration;
/** Polling interval in milliseconds (default: 1000) */
pollingInterval?: number;
/** Connection timeout in milliseconds (default: 30000) */
connectionTimeout?: number;
}
/**
* 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
*/
export type ConnectionRole = 'offerer' | 'answerer';
/**
* Parameters for creating a RondevuConnection
*/
export interface RondevuConnectionParams {
id: string;
topic: string;
role: ConnectionRole;
pc: RTCPeerConnection;
localPeerId: string;
remotePeerId: string;
pollingInterval: number;
connectionTimeout: number;
}
/**
* Event map for RondevuConnection events
*/
export interface RondevuConnectionEvents {
connect: () => void;
disconnect: () => void;
error: (error: Error) => void;
datachannel: (channel: RTCDataChannel) => void;
stream: (stream: MediaStream) => void;
}

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"declaration": true,
"outDir": "./dist",
@@ -10,7 +10,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true
},
"include": ["src/**/*"],