From 53206d306be9cf63fbbe07265ba53cdd38568448 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 16 Nov 2025 20:16:42 +0100 Subject: [PATCH] Add WebRTC polyfill support for Node.js environments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added optional polyfill parameters to RondevuOptions to support Node.js: - RTCPeerConnection: Custom peer connection implementation - RTCSessionDescription: Custom session description implementation - RTCIceCandidate: Custom ICE candidate implementation This allows users to plug in wrtc or node-webrtc packages for full WebRTC support in Node.js environments. Updated documentation with usage examples and environment compatibility matrix. Version bumped to 0.7.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 69 ++++++++++++++++++++++++++------ package.json | 2 +- src/peer/exchanging-ice-state.ts | 2 +- src/peer/index.ts | 32 ++++++++++++++- src/peer/state.ts | 2 +- src/rondevu.ts | 50 ++++++++++++++++++++++- 6 files changed, 139 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index aca2e36..6d903f4 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,43 @@ const client = new Rondevu({ }); ``` +### Node.js with WebRTC (wrtc) + +For WebRTC functionality in Node.js, you need to provide WebRTC polyfills since Node.js doesn't have native WebRTC support: + +```bash +npm install wrtc node-fetch +``` + +```typescript +import { Rondevu } from '@xtr-dev/rondevu-client'; +import fetch from 'node-fetch'; +import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'; + +const client = new Rondevu({ + baseUrl: 'https://api.ronde.vu', + fetch: fetch as any, + RTCPeerConnection, + RTCSessionDescription, + RTCIceCandidate +}); + +// Now you can use WebRTC features +await client.register(); +const peer = client.createPeer({ + iceServers: [ + { urls: 'stun:stun.l.google.com:19302' } + ] +}); + +// Create offers, answer, etc. +const offerId = await peer.createOffer({ + topics: ['my-topic'] +}); +``` + +**Note:** The `wrtc` package provides WebRTC bindings for Node.js. Alternative packages like `node-webrtc` can also be used - just pass their implementations to the Rondevu constructor. + ### Deno ```typescript @@ -500,28 +537,36 @@ import type { The client library is designed to work across different JavaScript runtimes: -| Environment | Native Fetch | Custom Fetch Needed | -|-------------|--------------|---------------------| -| Modern Browsers | ✅ Yes | ❌ No | -| Node.js 18+ | ✅ Yes | ❌ No | -| Node.js < 18 | ❌ No | ✅ Yes (node-fetch) | -| Deno | ✅ Yes | ❌ No | -| Bun | ✅ Yes | ❌ No | -| Cloudflare Workers | ✅ Yes | ❌ No | +| Environment | Native Fetch | Native WebRTC | Polyfills Needed | +|-------------|--------------|---------------|------------------| +| Modern Browsers | ✅ Yes | ✅ Yes | ❌ None | +| Node.js 18+ | ✅ Yes | ❌ No | ✅ WebRTC (wrtc) | +| Node.js < 18 | ❌ No | ❌ No | ✅ Fetch + WebRTC | +| Deno | ✅ Yes | ⚠️ Partial | ❌ None (signaling only) | +| Bun | ✅ Yes | ❌ No | ✅ WebRTC (wrtc) | +| Cloudflare Workers | ✅ Yes | ❌ No | ❌ None (signaling only) | -**If your environment doesn't have native fetch:** +**For signaling-only (no WebRTC peer connections):** + +Use the low-level API with `client.offers` - no WebRTC polyfills needed. + +**For full WebRTC support in Node.js:** ```bash -npm install node-fetch +npm install wrtc node-fetch ``` ```typescript import { Rondevu } from '@xtr-dev/rondevu-client'; import fetch from 'node-fetch'; +import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'; const client = new Rondevu({ - baseUrl: 'https://rondevu.xtrdev.workers.dev', - fetch: fetch as any + baseUrl: 'https://api.ronde.vu', + fetch: fetch as any, + RTCPeerConnection, + RTCSessionDescription, + RTCIceCandidate }); ``` diff --git a/package.json b/package.json index c8d17f8..0cc6f38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/rondevu-client", - "version": "0.7.3", + "version": "0.7.4", "description": "TypeScript client for Rondevu topic-based peer discovery and signaling server", "type": "module", "main": "dist/index.js", diff --git a/src/peer/exchanging-ice-state.ts b/src/peer/exchanging-ice-state.ts index 07fbf58..6b68680 100644 --- a/src/peer/exchanging-ice-state.ts +++ b/src/peer/exchanging-ice-state.ts @@ -43,7 +43,7 @@ export class ExchangingIceState extends PeerState { for (const cand of candidates) { if (cand.candidate && cand.candidate.candidate && cand.candidate.candidate !== '') { try { - await this.peer.pc.addIceCandidate(new RTCIceCandidate(cand.candidate)); + await this.peer.pc.addIceCandidate(new this.peer.RTCIceCandidate(cand.candidate)); this.lastIceTimestamp = cand.createdAt; } catch (err) { console.warn('Failed to add ICE candidate:', err); diff --git a/src/peer/index.ts b/src/peer/index.ts index 96122cb..d5d40e3 100644 --- a/src/peer/index.ts +++ b/src/peer/index.ts @@ -24,6 +24,11 @@ export default class RondevuPeer extends EventEmitter { offerId?: string; role?: 'offerer' | 'answerer'; + // WebRTC polyfills for Node.js compatibility + RTCPeerConnection: typeof RTCPeerConnection; + RTCSessionDescription: typeof RTCSessionDescription; + RTCIceCandidate: typeof RTCIceCandidate; + private _state: PeerState; // Event handler references for cleanup @@ -60,11 +65,34 @@ export default class RondevuPeer extends EventEmitter { { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] - } + }, + rtcPeerConnection?: typeof RTCPeerConnection, + rtcSessionDescription?: typeof RTCSessionDescription, + rtcIceCandidate?: typeof RTCIceCandidate ) { super(); this.offersApi = offersApi; - this.pc = new RTCPeerConnection(rtcConfig); + + // Use provided polyfills or fall back to globals + this.RTCPeerConnection = rtcPeerConnection || (typeof globalThis.RTCPeerConnection !== 'undefined' + ? globalThis.RTCPeerConnection + : (() => { + throw new Error('RTCPeerConnection is not available. Please provide it in the Rondevu constructor options for Node.js environments.'); + }) as any); + + this.RTCSessionDescription = rtcSessionDescription || (typeof globalThis.RTCSessionDescription !== 'undefined' + ? globalThis.RTCSessionDescription + : (() => { + throw new Error('RTCSessionDescription is not available. Please provide it in the Rondevu constructor options for Node.js environments.'); + }) as any); + + this.RTCIceCandidate = rtcIceCandidate || (typeof globalThis.RTCIceCandidate !== 'undefined' + ? globalThis.RTCIceCandidate + : (() => { + throw new Error('RTCIceCandidate is not available. Please provide it in the Rondevu constructor options for Node.js environments.'); + }) as any); + + this.pc = new this.RTCPeerConnection(rtcConfig); this._state = new IdleState(this); this.setupPeerConnection(); diff --git a/src/peer/state.ts b/src/peer/state.ts index 6ea8e68..a899b61 100644 --- a/src/peer/state.ts +++ b/src/peer/state.ts @@ -27,7 +27,7 @@ export abstract class PeerState { async handleIceCandidate(candidate: any): Promise { // ICE candidates can arrive in multiple states, so default is to add them if (this.peer.pc.remoteDescription) { - await this.peer.pc.addIceCandidate(new RTCIceCandidate(candidate)); + await this.peer.pc.addIceCandidate(new this.peer.RTCIceCandidate(candidate)); } } diff --git a/src/rondevu.ts b/src/rondevu.ts index c8ed92a..7fdefc9 100644 --- a/src/rondevu.ts +++ b/src/rondevu.ts @@ -25,6 +25,42 @@ export interface RondevuOptions { * ``` */ fetch?: FetchFunction; + + /** + * Custom RTCPeerConnection implementation for Node.js environments + * Required when using in Node.js with wrtc or similar polyfills + * + * @example Node.js with wrtc + * ```typescript + * import { RTCPeerConnection } from 'wrtc'; + * const client = new Rondevu({ RTCPeerConnection }); + * ``` + */ + RTCPeerConnection?: typeof RTCPeerConnection; + + /** + * Custom RTCSessionDescription implementation for Node.js environments + * Required when using in Node.js with wrtc or similar polyfills + * + * @example Node.js with wrtc + * ```typescript + * import { RTCSessionDescription } from 'wrtc'; + * const client = new Rondevu({ RTCSessionDescription }); + * ``` + */ + RTCSessionDescription?: typeof RTCSessionDescription; + + /** + * Custom RTCIceCandidate implementation for Node.js environments + * Required when using in Node.js with wrtc or similar polyfills + * + * @example Node.js with wrtc + * ```typescript + * import { RTCIceCandidate } from 'wrtc'; + * const client = new Rondevu({ RTCIceCandidate }); + * ``` + */ + RTCIceCandidate?: typeof RTCIceCandidate; } export class Rondevu { @@ -33,10 +69,16 @@ export class Rondevu { private credentials?: Credentials; private baseUrl: string; private fetchFn?: FetchFunction; + private rtcPeerConnection?: typeof RTCPeerConnection; + private rtcSessionDescription?: typeof RTCSessionDescription; + private rtcIceCandidate?: typeof RTCIceCandidate; constructor(options: RondevuOptions = {}) { this.baseUrl = options.baseUrl || 'https://api.ronde.vu'; this.fetchFn = options.fetch; + this.rtcPeerConnection = options.RTCPeerConnection; + this.rtcSessionDescription = options.RTCSessionDescription; + this.rtcIceCandidate = options.RTCIceCandidate; this.auth = new RondevuAuth(this.baseUrl, this.fetchFn); @@ -98,6 +140,12 @@ export class Rondevu { throw new Error('Not authenticated. Call register() first or provide credentials.'); } - return new RondevuPeer(this._offers, rtcConfig); + return new RondevuPeer( + this._offers, + rtcConfig, + this.rtcPeerConnection, + this.rtcSessionDescription, + this.rtcIceCandidate + ); } }