commit abc553a3a5e7e3df6e3fb6eb944cdbba205f3716 Author: Bas van den Aakster Date: Sun Nov 2 14:32:53 2025 +0100 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e97095c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.log +.DS_Store +*.tsbuildinfo diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..59c3257 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +src/ +tsconfig.json +*.tsbuildinfo +node_modules/ +.DS_Store +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..7134de4 --- /dev/null +++ b/README.md @@ -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` + +##### `listSessions(topic)` + +Discovers available peers for a given topic. + +**Parameters:** +- `topic` (string): Topic identifier + +**Returns:** `Promise` + +##### `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` + +##### `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` + +##### `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` + +##### `health()` + +Checks server health. + +**Returns:** `Promise` + +## 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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..83e6037 --- /dev/null +++ b/package.json @@ -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" + ] +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..bd61004 --- /dev/null +++ b/src/client.ts @@ -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( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const headers: Record = { + 'Origin': this.origin, + ...(options.headers as Record), + }; + + 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 { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + return this.request(`/?${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 { + return this.request(`/${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 { + return this.request( + `/${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 { + return this.request('/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 { + const request: PollRequest = { code, side }; + return this.request('/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 { + return this.request('/health', { + method: 'GET', + }); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..36e6a38 --- /dev/null +++ b/src/index.ts @@ -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'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0e7fc40 --- /dev/null +++ b/src/types.ts @@ -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; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..010df2c --- /dev/null +++ b/tsconfig.json @@ -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"] +}