Initial commit: Rondevu TypeScript client

TypeScript client library for Rondevu peer signaling and discovery server.

Features:
- Fully typed API with TypeScript definitions
- Support for all Rondevu server endpoints
- Configurable base URL for any server instance
- Browser and Node.js compatible
- Comprehensive documentation and examples
- Type-safe request/response handling

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-02 14:32:53 +01:00
commit abc553a3a5
8 changed files with 707 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
dist/
*.log
.DS_Store
*.tsbuildinfo

6
.npmignore Normal file
View File

@@ -0,0 +1,6 @@
src/
tsconfig.json
*.tsbuildinfo
node_modules/
.DS_Store
*.log

234
README.md Normal file
View File

@@ -0,0 +1,234 @@
# @rondevu/client
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.
## Installation
```bash
npm install @rondevu/client
```
## Usage
### Basic Setup
```typescript
import { RondevuClient } from '@rondevu/client';
const client = new RondevuClient({
baseUrl: 'https://rondevu.example.com',
// Optional: custom origin for session isolation
origin: 'https://myapp.com'
});
```
### Peer Discovery Flow
#### 1. List Available Topics
```typescript
// Get all topics with peer counts
const { topics, pagination } = await client.listTopics();
topics.forEach(topic => {
console.log(`${topic.topic}: ${topic.count} peers available`);
});
```
#### 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 '@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 '@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
MIT

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "@rondevu/client",
"version": "1.0.0",
"description": "TypeScript client for Rondevu peer signaling and discovery server",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build"
},
"keywords": [
"webrtc",
"p2p",
"signaling",
"peer-discovery",
"rondevu"
],
"author": "",
"license": "MIT",
"devDependencies": {
"typescript": "^5.9.3"
},
"files": [
"dist",
"README.md"
]
}

223
src/client.ts Normal file
View File

@@ -0,0 +1,223 @@
import {
RondevuClientOptions,
ListTopicsResponse,
ListSessionsResponse,
CreateOfferRequest,
CreateOfferResponse,
AnswerRequest,
AnswerResponse,
PollRequest,
PollOffererResponse,
PollAnswererResponse,
HealthResponse,
ErrorResponse,
Side,
} from './types';
/**
* HTTP client for Rondevu peer signaling and discovery server
*/
export class RondevuClient {
private readonly baseUrl: string;
private readonly origin: string;
private readonly fetchImpl: typeof fetch;
/**
* Creates a new Rondevu client instance
* @param options - Client configuration options
*/
constructor(options: RondevuClientOptions) {
this.baseUrl = options.baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.origin = options.origin || new URL(this.baseUrl).origin;
this.fetchImpl = options.fetch || fetch;
}
/**
* Makes an HTTP request to the Rondevu server
*/
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
'Origin': this.origin,
...(options.headers as Record<string, string>),
};
if (options.body) {
headers['Content-Type'] = 'application/json';
}
const response = await this.fetchImpl(url, {
...options,
headers,
});
const data = await response.json();
if (!response.ok) {
const error = data as ErrorResponse;
throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`);
}
return data as T;
}
/**
* Lists all topics with peer counts
*
* @param page - Page number (starting from 1)
* @param limit - Results per page (max 1000)
* @returns List of topics with pagination info
*
* @example
* ```typescript
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
* const { topics, pagination } = await client.listTopics();
* 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>(`/?${params}`, {
method: 'GET',
});
}
/**
* Discovers available peers for a given topic
*
* @param topic - Topic identifier
* @returns List of available sessions
*
* @example
* ```typescript
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
* const { sessions } = await client.listSessions('my-room');
* const otherPeers = sessions.filter(s => s.info !== 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 256 characters)
* @param request - Offer details including peer info 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',
* offer: signalingData
* });
* console.log('Session code:', code);
* ```
*/
async createOffer(
topic: string,
request: CreateOfferRequest
): Promise<CreateOfferResponse> {
return this.request<CreateOfferResponse>(
`/${encodeURIComponent(topic)}/offer`,
{
method: 'POST',
body: JSON.stringify(request),
}
);
}
/**
* Sends an answer or candidate to an existing session
*
* @param request - Answer details including session code and signaling data
* @returns Success confirmation
*
* @example
* ```typescript
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
*
* // Send answer
* await client.sendAnswer({
* code: sessionCode,
* answer: answerData,
* side: 'answerer'
* });
*
* // Send candidate
* await client.sendAnswer({
* code: sessionCode,
* candidate: candidateData,
* side: 'offerer'
* });
* ```
*/
async sendAnswer(request: AnswerRequest): Promise<AnswerResponse> {
return this.request<AnswerResponse>('/answer', {
method: 'POST',
body: JSON.stringify(request),
});
}
/**
* Polls for session data from the other peer
*
* @param code - Session UUID
* @param side - Which side is polling ('offerer' or 'answerer')
* @returns Session data including offers, answers, and candidates
*
* @example
* ```typescript
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
*
* // Offerer polls for answer
* const offererData = await client.poll(sessionCode, 'offerer');
* if (offererData.answer) {
* console.log('Received answer:', offererData.answer);
* }
*
* // Answerer polls for offer
* const answererData = await client.poll(sessionCode, 'answerer');
* console.log('Received offer:', answererData.offer);
* ```
*/
async poll(
code: string,
side: Side
): Promise<PollOffererResponse | PollAnswererResponse> {
const request: PollRequest = { code, side };
return this.request<PollOffererResponse | PollAnswererResponse>('/poll', {
method: 'POST',
body: JSON.stringify(request),
});
}
/**
* Checks server health
*
* @returns Health status and timestamp
*
* @example
* ```typescript
* const client = new RondevuClient({ baseUrl: 'https://example.com' });
* const health = await client.health();
* console.log('Server status:', health.status);
* ```
*/
async health(): Promise<HealthResponse> {
return this.request<HealthResponse>('/health', {
method: 'GET',
});
}
}

31
src/index.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* @rondevu/client - TypeScript client for Rondevu peer signaling server
*
* @example
* ```typescript
* import { RondevuClient } from '@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'
* });
* ```
*/
export { RondevuClient } from './client';
export * from './types';

162
src/types.ts Normal file
View File

@@ -0,0 +1,162 @@
/**
* Session side - identifies which peer in a connection
*/
export type Side = 'offerer' | 'answerer';
/**
* Session information returned from discovery endpoints
*/
export interface Session {
/** Unique session identifier (UUID) */
code: string;
/** Peer identifier/metadata */
info: 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 {
/** Peer identifier/metadata (max 1024 characters) */
info: string;
/** Signaling data for peer connection */
offer: string;
}
/**
* Response from POST /:topic/offer
*/
export interface CreateOfferResponse {
/** Unique session identifier (UUID) */
code: string;
}
/**
* Request body for POST /answer
*/
export interface AnswerRequest {
/** Session UUID from the offer */
code: string;
/** Response signaling data (required if candidate not provided) */
answer?: string;
/** Additional signaling data (required if answer not provided) */
candidate?: string;
/** Which peer is sending the data */
side: Side;
}
/**
* Response from POST /answer
*/
export interface AnswerResponse {
success: boolean;
}
/**
* Request body for POST /poll
*/
export interface PollRequest {
/** Session UUID */
code: string;
/** Which side is polling */
side: Side;
}
/**
* Response from POST /poll when side=offerer
*/
export interface PollOffererResponse {
/** Answer from answerer (null if not yet received) */
answer: string | null;
/** Additional signaling data from answerer */
answerCandidates: string[];
}
/**
* Response from POST /poll when side=answerer
*/
export interface PollAnswererResponse {
/** Offer from offerer */
offer: string;
/** Additional signaling data from offerer */
offerCandidates: string[];
}
/**
* Response from POST /poll (union type)
*/
export type PollResponse = PollOffererResponse | PollAnswererResponse;
/**
* Response from GET /health
*/
export interface HealthResponse {
status: 'ok';
timestamp: number;
}
/**
* Error response structure
*/
export interface ErrorResponse {
error: string;
}
/**
* Client configuration options
*/
export interface RondevuClientOptions {
/** Base URL of the Rondevu server (e.g., 'https://example.com') */
baseUrl: string;
/** Origin header value for session isolation (defaults to baseUrl origin) */
origin?: string;
/** Optional fetch implementation (for Node.js environments) */
fetch?: typeof fetch;
}

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}