@xtr-dev/rondevu-client 0.8.3 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js DELETED
@@ -1,171 +0,0 @@
1
- /**
2
- * HTTP API client for Rondevu peer signaling server
3
- */
4
- export class RondevuAPI {
5
- /**
6
- * Creates a new Rondevu API client instance
7
- * @param options - Client configuration options
8
- */
9
- constructor(options) {
10
- this.baseUrl = options.baseUrl.replace(/\/$/, ''); // Remove trailing slash
11
- this.fetchImpl = options.fetch || globalThis.fetch.bind(globalThis);
12
- }
13
- /**
14
- * Makes an HTTP request to the Rondevu server
15
- */
16
- async request(endpoint, options = {}) {
17
- const url = `${this.baseUrl}${endpoint}`;
18
- const headers = {
19
- ...options.headers,
20
- };
21
- if (options.body) {
22
- headers['Content-Type'] = 'application/json';
23
- }
24
- const response = await this.fetchImpl(url, {
25
- ...options,
26
- headers,
27
- });
28
- const data = await response.json();
29
- if (!response.ok) {
30
- const error = data;
31
- throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`);
32
- }
33
- return data;
34
- }
35
- /**
36
- * Gets server version information
37
- *
38
- * @returns Server version
39
- *
40
- * @example
41
- * ```typescript
42
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
43
- * const { version } = await api.getVersion();
44
- * console.log('Server version:', version);
45
- * ```
46
- */
47
- async getVersion() {
48
- return this.request('/', {
49
- method: 'GET',
50
- });
51
- }
52
- /**
53
- * Creates a new offer
54
- *
55
- * @param request - Offer details including peer ID, signaling data, and optional custom code
56
- * @returns Unique offer code (UUID or custom code)
57
- *
58
- * @example
59
- * ```typescript
60
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
61
- * const { code } = await api.createOffer({
62
- * peerId: 'peer-123',
63
- * offer: signalingData,
64
- * code: 'my-custom-code' // optional
65
- * });
66
- * console.log('Offer code:', code);
67
- * ```
68
- */
69
- async createOffer(request) {
70
- return this.request('/offer', {
71
- method: 'POST',
72
- body: JSON.stringify(request),
73
- });
74
- }
75
- /**
76
- * Sends an answer or candidate to an existing offer
77
- *
78
- * @param request - Answer details including offer code and signaling data
79
- * @returns Success confirmation
80
- *
81
- * @example
82
- * ```typescript
83
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
84
- *
85
- * // Send answer
86
- * await api.sendAnswer({
87
- * code: offerCode,
88
- * answer: answerData,
89
- * side: 'answerer'
90
- * });
91
- *
92
- * // Send candidate
93
- * await api.sendAnswer({
94
- * code: offerCode,
95
- * candidate: candidateData,
96
- * side: 'offerer'
97
- * });
98
- * ```
99
- */
100
- async sendAnswer(request) {
101
- return this.request('/answer', {
102
- method: 'POST',
103
- body: JSON.stringify(request),
104
- });
105
- }
106
- /**
107
- * Polls for offer data from the other peer
108
- *
109
- * @param code - Offer code
110
- * @param side - Which side is polling ('offerer' or 'answerer')
111
- * @returns Offer data including offers, answers, and candidates
112
- *
113
- * @example
114
- * ```typescript
115
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
116
- *
117
- * // Offerer polls for answer
118
- * const offererData = await api.poll(offerCode, 'offerer');
119
- * if (offererData.answer) {
120
- * console.log('Received answer:', offererData.answer);
121
- * }
122
- *
123
- * // Answerer polls for offer
124
- * const answererData = await api.poll(offerCode, 'answerer');
125
- * console.log('Received offer:', answererData.offer);
126
- * ```
127
- */
128
- async poll(code, side) {
129
- const request = { code, side };
130
- return this.request('/poll', {
131
- method: 'POST',
132
- body: JSON.stringify(request),
133
- });
134
- }
135
- /**
136
- * Checks server health and version
137
- *
138
- * @returns Health status, timestamp, and version
139
- *
140
- * @example
141
- * ```typescript
142
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
143
- * const health = await api.health();
144
- * console.log('Server status:', health.status);
145
- * console.log('Server version:', health.version);
146
- * ```
147
- */
148
- async health() {
149
- return this.request('/health', {
150
- method: 'GET',
151
- });
152
- }
153
- /**
154
- * Ends a session by deleting the offer from the server
155
- *
156
- * @param code - The offer code
157
- * @returns Success confirmation
158
- *
159
- * @example
160
- * ```typescript
161
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
162
- * await api.leave('my-offer-code');
163
- * ```
164
- */
165
- async leave(code) {
166
- return this.request('/leave', {
167
- method: 'POST',
168
- body: JSON.stringify({ code }),
169
- });
170
- }
171
- }
@@ -1,127 +0,0 @@
1
- import { RondevuOffers } from './offers.js';
2
- /**
3
- * Events emitted by RondevuConnection
4
- */
5
- export interface RondevuConnectionEvents {
6
- 'connecting': () => void;
7
- 'connected': () => void;
8
- 'disconnected': () => void;
9
- 'error': (error: Error) => void;
10
- 'datachannel': (channel: RTCDataChannel) => void;
11
- 'track': (event: RTCTrackEvent) => void;
12
- }
13
- /**
14
- * Options for creating a WebRTC connection
15
- */
16
- export interface ConnectionOptions {
17
- /**
18
- * RTCConfiguration for the peer connection
19
- * @default { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }
20
- */
21
- rtcConfig?: RTCConfiguration;
22
- /**
23
- * Topics to advertise this connection under
24
- */
25
- topics: string[];
26
- /**
27
- * How long the offer should live (milliseconds)
28
- * @default 300000 (5 minutes)
29
- */
30
- ttl?: number;
31
- /**
32
- * Whether to create a data channel automatically (for offerer)
33
- * @default true
34
- */
35
- createDataChannel?: boolean;
36
- /**
37
- * Label for the automatically created data channel
38
- * @default 'data'
39
- */
40
- dataChannelLabel?: string;
41
- }
42
- /**
43
- * High-level WebRTC connection manager for Rondevu
44
- * Handles offer/answer exchange, ICE candidates, and connection lifecycle
45
- */
46
- export declare class RondevuConnection {
47
- private rtcConfig;
48
- private pc;
49
- private offersApi;
50
- private offerId?;
51
- private role?;
52
- private icePollingInterval?;
53
- private answerPollingInterval?;
54
- private lastIceTimestamp;
55
- private eventListeners;
56
- private dataChannel?;
57
- private pendingIceCandidates;
58
- /**
59
- * Current connection state
60
- */
61
- get connectionState(): RTCPeerConnectionState;
62
- /**
63
- * The offer ID for this connection
64
- */
65
- get id(): string | undefined;
66
- /**
67
- * Get the primary data channel (if created)
68
- */
69
- get channel(): RTCDataChannel | undefined;
70
- constructor(offersApi: RondevuOffers, rtcConfig?: RTCConfiguration);
71
- /**
72
- * Set up peer connection event handlers
73
- */
74
- private setupPeerConnection;
75
- /**
76
- * Flush buffered ICE candidates (trickle ICE support)
77
- */
78
- private flushPendingIceCandidates;
79
- /**
80
- * Create an offer and advertise on topics
81
- */
82
- createOffer(options: ConnectionOptions): Promise<string>;
83
- /**
84
- * Answer an existing offer
85
- */
86
- answer(offerId: string, offerSdp: string): Promise<void>;
87
- /**
88
- * Start polling for answers (offerer only)
89
- */
90
- private startAnswerPolling;
91
- /**
92
- * Start polling for ICE candidates
93
- */
94
- private startIcePolling;
95
- /**
96
- * Stop answer polling
97
- */
98
- private stopAnswerPolling;
99
- /**
100
- * Stop ICE polling
101
- */
102
- private stopIcePolling;
103
- /**
104
- * Stop all polling
105
- */
106
- private stopPolling;
107
- /**
108
- * Add event listener
109
- */
110
- on<K extends keyof RondevuConnectionEvents>(event: K, listener: RondevuConnectionEvents[K]): void;
111
- /**
112
- * Remove event listener
113
- */
114
- off<K extends keyof RondevuConnectionEvents>(event: K, listener: RondevuConnectionEvents[K]): void;
115
- /**
116
- * Emit event
117
- */
118
- private emit;
119
- /**
120
- * Add a media track to the connection
121
- */
122
- addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender;
123
- /**
124
- * Close the connection and clean up
125
- */
126
- close(): void;
127
- }
@@ -1,295 +0,0 @@
1
- /**
2
- * High-level WebRTC connection manager for Rondevu
3
- * Handles offer/answer exchange, ICE candidates, and connection lifecycle
4
- */
5
- export class RondevuConnection {
6
- /**
7
- * Current connection state
8
- */
9
- get connectionState() {
10
- return this.pc.connectionState;
11
- }
12
- /**
13
- * The offer ID for this connection
14
- */
15
- get id() {
16
- return this.offerId;
17
- }
18
- /**
19
- * Get the primary data channel (if created)
20
- */
21
- get channel() {
22
- return this.dataChannel;
23
- }
24
- constructor(offersApi, rtcConfig = {
25
- iceServers: [
26
- { urls: 'stun:stun.l.google.com:19302' },
27
- { urls: 'stun:stun1.l.google.com:19302' }
28
- ]
29
- }) {
30
- this.rtcConfig = rtcConfig;
31
- this.lastIceTimestamp = 0; // Start at 0 to get all candidates on first poll
32
- this.eventListeners = new Map();
33
- this.pendingIceCandidates = []; // Store candidates as plain objects
34
- this.offersApi = offersApi;
35
- this.pc = new RTCPeerConnection(rtcConfig);
36
- this.setupPeerConnection();
37
- }
38
- /**
39
- * Set up peer connection event handlers
40
- */
41
- setupPeerConnection() {
42
- this.pc.onicecandidate = async (event) => {
43
- if (event.candidate) {
44
- // Serialize the entire candidate object to plain JSON
45
- const candidateData = event.candidate.toJSON();
46
- // Skip end-of-candidates signal (empty candidate string)
47
- // Some browsers send this as a candidate object with empty/null properties
48
- if (!candidateData.candidate || candidateData.candidate === '') {
49
- return;
50
- }
51
- if (this.offerId) {
52
- // offerId is set, send immediately (trickle ICE)
53
- try {
54
- await this.offersApi.addIceCandidates(this.offerId, [candidateData]);
55
- }
56
- catch (err) {
57
- console.error('Error sending ICE candidate:', err);
58
- }
59
- }
60
- else {
61
- // offerId not set yet, buffer the candidate
62
- this.pendingIceCandidates.push(candidateData);
63
- }
64
- }
65
- };
66
- this.pc.onconnectionstatechange = () => {
67
- switch (this.pc.connectionState) {
68
- case 'connecting':
69
- this.emit('connecting');
70
- break;
71
- case 'connected':
72
- this.emit('connected');
73
- // Stop polling once connected - we have all the ICE candidates we need
74
- this.stopPolling();
75
- break;
76
- case 'disconnected':
77
- case 'failed':
78
- case 'closed':
79
- this.emit('disconnected');
80
- this.stopPolling();
81
- break;
82
- }
83
- };
84
- this.pc.ondatachannel = (event) => {
85
- this.dataChannel = event.channel;
86
- this.emit('datachannel', event.channel);
87
- };
88
- this.pc.ontrack = (event) => {
89
- this.emit('track', event);
90
- };
91
- this.pc.onicecandidateerror = (event) => {
92
- console.error('ICE candidate error:', event);
93
- };
94
- }
95
- /**
96
- * Flush buffered ICE candidates (trickle ICE support)
97
- */
98
- async flushPendingIceCandidates() {
99
- if (this.pendingIceCandidates.length > 0 && this.offerId) {
100
- try {
101
- await this.offersApi.addIceCandidates(this.offerId, this.pendingIceCandidates);
102
- this.pendingIceCandidates = [];
103
- }
104
- catch (err) {
105
- console.error('Error flushing pending ICE candidates:', err);
106
- }
107
- }
108
- }
109
- /**
110
- * Create an offer and advertise on topics
111
- */
112
- async createOffer(options) {
113
- this.role = 'offerer';
114
- // Create data channel if requested
115
- if (options.createDataChannel !== false) {
116
- this.dataChannel = this.pc.createDataChannel(options.dataChannelLabel || 'data');
117
- this.emit('datachannel', this.dataChannel);
118
- }
119
- // Create WebRTC offer
120
- const offer = await this.pc.createOffer();
121
- await this.pc.setLocalDescription(offer);
122
- // Create offer on Rondevu server
123
- const offers = await this.offersApi.create([{
124
- sdp: offer.sdp,
125
- topics: options.topics,
126
- ttl: options.ttl || 300000
127
- }]);
128
- this.offerId = offers[0].id;
129
- // Flush any ICE candidates that were generated during offer creation
130
- await this.flushPendingIceCandidates();
131
- // Start polling for answers
132
- this.startAnswerPolling();
133
- return this.offerId;
134
- }
135
- /**
136
- * Answer an existing offer
137
- */
138
- async answer(offerId, offerSdp) {
139
- this.role = 'answerer';
140
- // Set remote description
141
- await this.pc.setRemoteDescription({
142
- type: 'offer',
143
- sdp: offerSdp
144
- });
145
- // Create answer
146
- const answer = await this.pc.createAnswer();
147
- await this.pc.setLocalDescription(answer);
148
- // Send answer to server FIRST
149
- // This registers us as the answerer before ICE candidates arrive
150
- await this.offersApi.answer(offerId, answer.sdp);
151
- // Now set offerId to enable ICE candidate sending
152
- // This prevents a race condition where ICE candidates arrive before answer is registered
153
- this.offerId = offerId;
154
- // Flush any ICE candidates that were generated during answer creation
155
- await this.flushPendingIceCandidates();
156
- // Start polling for ICE candidates
157
- this.startIcePolling();
158
- }
159
- /**
160
- * Start polling for answers (offerer only)
161
- */
162
- startAnswerPolling() {
163
- if (this.role !== 'offerer' || !this.offerId)
164
- return;
165
- this.answerPollingInterval = setInterval(async () => {
166
- try {
167
- const answers = await this.offersApi.getAnswers();
168
- const myAnswer = answers.find(a => a.offerId === this.offerId);
169
- if (myAnswer) {
170
- // Set remote description
171
- await this.pc.setRemoteDescription({
172
- type: 'answer',
173
- sdp: myAnswer.sdp
174
- });
175
- // Stop answer polling, start ICE polling
176
- this.stopAnswerPolling();
177
- this.startIcePolling();
178
- }
179
- }
180
- catch (err) {
181
- console.error('Error polling for answers:', err);
182
- // Stop polling if offer expired/not found
183
- if (err instanceof Error && err.message.includes('not found')) {
184
- this.stopPolling();
185
- }
186
- }
187
- }, 2000);
188
- }
189
- /**
190
- * Start polling for ICE candidates
191
- */
192
- startIcePolling() {
193
- if (!this.offerId)
194
- return;
195
- this.icePollingInterval = setInterval(async () => {
196
- if (!this.offerId)
197
- return;
198
- try {
199
- const candidates = await this.offersApi.getIceCandidates(this.offerId, this.lastIceTimestamp);
200
- for (const cand of candidates) {
201
- // Server already filters candidates by role, so all candidates here are from remote peer
202
- try {
203
- // Skip invalid or empty candidates
204
- if (!cand.candidate || !cand.candidate.candidate || cand.candidate.candidate === '') {
205
- this.lastIceTimestamp = cand.createdAt;
206
- continue;
207
- }
208
- await this.pc.addIceCandidate(new RTCIceCandidate(cand.candidate));
209
- this.lastIceTimestamp = cand.createdAt;
210
- }
211
- catch (err) {
212
- // Log but don't fail on individual candidate errors
213
- console.warn('Failed to add ICE candidate:', err, cand);
214
- this.lastIceTimestamp = cand.createdAt;
215
- }
216
- }
217
- }
218
- catch (err) {
219
- console.error('Error polling for ICE candidates:', err);
220
- // Stop polling if offer expired/not found
221
- if (err instanceof Error && err.message.includes('not found')) {
222
- this.stopPolling();
223
- }
224
- }
225
- }, 1000);
226
- }
227
- /**
228
- * Stop answer polling
229
- */
230
- stopAnswerPolling() {
231
- if (this.answerPollingInterval) {
232
- clearInterval(this.answerPollingInterval);
233
- this.answerPollingInterval = undefined;
234
- }
235
- }
236
- /**
237
- * Stop ICE polling
238
- */
239
- stopIcePolling() {
240
- if (this.icePollingInterval) {
241
- clearInterval(this.icePollingInterval);
242
- this.icePollingInterval = undefined;
243
- }
244
- }
245
- /**
246
- * Stop all polling
247
- */
248
- stopPolling() {
249
- this.stopAnswerPolling();
250
- this.stopIcePolling();
251
- }
252
- /**
253
- * Add event listener
254
- */
255
- on(event, listener) {
256
- if (!this.eventListeners.has(event)) {
257
- this.eventListeners.set(event, new Set());
258
- }
259
- this.eventListeners.get(event).add(listener);
260
- }
261
- /**
262
- * Remove event listener
263
- */
264
- off(event, listener) {
265
- const listeners = this.eventListeners.get(event);
266
- if (listeners) {
267
- listeners.delete(listener);
268
- }
269
- }
270
- /**
271
- * Emit event
272
- */
273
- emit(event, ...args) {
274
- const listeners = this.eventListeners.get(event);
275
- if (listeners) {
276
- listeners.forEach(listener => {
277
- listener(...args);
278
- });
279
- }
280
- }
281
- /**
282
- * Add a media track to the connection
283
- */
284
- addTrack(track, ...streams) {
285
- return this.pc.addTrack(track, ...streams);
286
- }
287
- /**
288
- * Close the connection and clean up
289
- */
290
- close() {
291
- this.stopPolling();
292
- this.pc.close();
293
- this.eventListeners.clear();
294
- }
295
- }
@@ -1,93 +0,0 @@
1
- import RondevuPeer from './peer/index.js';
2
- /**
3
- * Service info from discovery
4
- */
5
- export interface ServiceInfo {
6
- uuid: string;
7
- isPublic: boolean;
8
- serviceFqn?: string;
9
- metadata?: Record<string, any>;
10
- }
11
- /**
12
- * Service list result
13
- */
14
- export interface ServiceListResult {
15
- username: string;
16
- services: ServiceInfo[];
17
- }
18
- /**
19
- * Service query result
20
- */
21
- export interface ServiceQueryResult {
22
- uuid: string;
23
- allowed: boolean;
24
- }
25
- /**
26
- * Service details
27
- */
28
- export interface ServiceDetails {
29
- serviceId: string;
30
- username: string;
31
- serviceFqn: string;
32
- offerId: string;
33
- sdp: string;
34
- isPublic: boolean;
35
- metadata?: Record<string, any>;
36
- createdAt: number;
37
- expiresAt: number;
38
- }
39
- /**
40
- * Connect result
41
- */
42
- export interface ConnectResult {
43
- peer: RondevuPeer;
44
- channel: RTCDataChannel;
45
- }
46
- /**
47
- * Rondevu Discovery API
48
- * Handles service discovery and connections
49
- */
50
- export declare class RondevuDiscovery {
51
- private baseUrl;
52
- private credentials;
53
- private offersApi;
54
- constructor(baseUrl: string, credentials: {
55
- peerId: string;
56
- secret: string;
57
- });
58
- /**
59
- * Lists all services for a username
60
- * Returns UUIDs only for private services, full details for public
61
- */
62
- listServices(username: string): Promise<ServiceListResult>;
63
- /**
64
- * Queries a service by FQN
65
- * Returns UUID if service exists and is allowed
66
- */
67
- queryService(username: string, serviceFqn: string): Promise<ServiceQueryResult>;
68
- /**
69
- * Gets service details by UUID
70
- */
71
- getServiceDetails(uuid: string): Promise<ServiceDetails>;
72
- /**
73
- * Connects to a service by UUID
74
- */
75
- connectToService(uuid: string, options?: {
76
- rtcConfig?: RTCConfiguration;
77
- onConnected?: () => void;
78
- onData?: (data: any) => void;
79
- }): Promise<RondevuPeer>;
80
- /**
81
- * Convenience method: Query and connect in one call
82
- * Returns both peer and data channel
83
- */
84
- connect(username: string, serviceFqn: string, options?: {
85
- rtcConfig?: RTCConfiguration;
86
- }): Promise<ConnectResult>;
87
- /**
88
- * Convenience method: Connect to service by UUID with channel
89
- */
90
- connectByUuid(uuid: string, options?: {
91
- rtcConfig?: RTCConfiguration;
92
- }): Promise<ConnectResult>;
93
- }