@xtr-dev/rondevu-client 0.8.3 → 0.9.1

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/discovery.js DELETED
@@ -1,164 +0,0 @@
1
- import RondevuPeer from './peer/index.js';
2
- import { RondevuOffers } from './offers.js';
3
- /**
4
- * Rondevu Discovery API
5
- * Handles service discovery and connections
6
- */
7
- export class RondevuDiscovery {
8
- constructor(baseUrl, credentials) {
9
- this.baseUrl = baseUrl;
10
- this.credentials = credentials;
11
- this.offersApi = new RondevuOffers(baseUrl, credentials);
12
- }
13
- /**
14
- * Lists all services for a username
15
- * Returns UUIDs only for private services, full details for public
16
- */
17
- async listServices(username) {
18
- const response = await fetch(`${this.baseUrl}/usernames/${username}/services`);
19
- if (!response.ok) {
20
- throw new Error('Failed to list services');
21
- }
22
- const data = await response.json();
23
- return {
24
- username: data.username,
25
- services: data.services
26
- };
27
- }
28
- /**
29
- * Queries a service by FQN
30
- * Returns UUID if service exists and is allowed
31
- */
32
- async queryService(username, serviceFqn) {
33
- const response = await fetch(`${this.baseUrl}/index/${username}/query`, {
34
- method: 'POST',
35
- headers: { 'Content-Type': 'application/json' },
36
- body: JSON.stringify({ serviceFqn })
37
- });
38
- if (!response.ok) {
39
- const error = await response.json();
40
- throw new Error(error.error || 'Service not found');
41
- }
42
- const data = await response.json();
43
- return {
44
- uuid: data.uuid,
45
- allowed: data.allowed
46
- };
47
- }
48
- /**
49
- * Gets service details by UUID
50
- */
51
- async getServiceDetails(uuid) {
52
- const response = await fetch(`${this.baseUrl}/services/${uuid}`);
53
- if (!response.ok) {
54
- const error = await response.json();
55
- throw new Error(error.error || 'Service not found');
56
- }
57
- const data = await response.json();
58
- return {
59
- serviceId: data.serviceId,
60
- username: data.username,
61
- serviceFqn: data.serviceFqn,
62
- offerId: data.offerId,
63
- sdp: data.sdp,
64
- isPublic: data.isPublic,
65
- metadata: data.metadata,
66
- createdAt: data.createdAt,
67
- expiresAt: data.expiresAt
68
- };
69
- }
70
- /**
71
- * Connects to a service by UUID
72
- */
73
- async connectToService(uuid, options) {
74
- // Get service details
75
- const service = await this.getServiceDetails(uuid);
76
- // Create peer with the offer
77
- const peer = new RondevuPeer(this.offersApi, options?.rtcConfig || {
78
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
79
- });
80
- // Set up event handlers
81
- if (options?.onConnected) {
82
- peer.on('connected', options.onConnected);
83
- }
84
- if (options?.onData) {
85
- peer.on('datachannel', (channel) => {
86
- channel.onmessage = (e) => options.onData(e.data);
87
- });
88
- }
89
- // Answer the offer
90
- await peer.answer(service.offerId, service.sdp, {
91
- topics: [], // V2 doesn't use topics
92
- rtcConfig: options?.rtcConfig
93
- });
94
- return peer;
95
- }
96
- /**
97
- * Convenience method: Query and connect in one call
98
- * Returns both peer and data channel
99
- */
100
- async connect(username, serviceFqn, options) {
101
- // Query service
102
- const query = await this.queryService(username, serviceFqn);
103
- if (!query.allowed) {
104
- throw new Error('Service access denied');
105
- }
106
- // Get service details
107
- const service = await this.getServiceDetails(query.uuid);
108
- // Create peer
109
- const peer = new RondevuPeer(this.offersApi, options?.rtcConfig || {
110
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
111
- });
112
- // Answer the offer
113
- await peer.answer(service.offerId, service.sdp, {
114
- topics: [], // V2 doesn't use topics
115
- rtcConfig: options?.rtcConfig
116
- });
117
- // Wait for data channel
118
- const channel = await new Promise((resolve, reject) => {
119
- const timeout = setTimeout(() => {
120
- reject(new Error('Timeout waiting for data channel'));
121
- }, 30000);
122
- peer.on('datachannel', (ch) => {
123
- clearTimeout(timeout);
124
- resolve(ch);
125
- });
126
- peer.on('failed', (error) => {
127
- clearTimeout(timeout);
128
- reject(error);
129
- });
130
- });
131
- return { peer, channel };
132
- }
133
- /**
134
- * Convenience method: Connect to service by UUID with channel
135
- */
136
- async connectByUuid(uuid, options) {
137
- // Get service details
138
- const service = await this.getServiceDetails(uuid);
139
- // Create peer
140
- const peer = new RondevuPeer(this.offersApi, options?.rtcConfig || {
141
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
142
- });
143
- // Answer the offer
144
- await peer.answer(service.offerId, service.sdp, {
145
- topics: [], // V2 doesn't use topics
146
- rtcConfig: options?.rtcConfig
147
- });
148
- // Wait for data channel
149
- const channel = await new Promise((resolve, reject) => {
150
- const timeout = setTimeout(() => {
151
- reject(new Error('Timeout waiting for data channel'));
152
- }, 30000);
153
- peer.on('datachannel', (ch) => {
154
- clearTimeout(timeout);
155
- resolve(ch);
156
- });
157
- peer.on('failed', (error) => {
158
- clearTimeout(timeout);
159
- reject(error);
160
- });
161
- });
162
- return { peer, channel };
163
- }
164
- }
package/dist/peer.d.ts DELETED
@@ -1,111 +0,0 @@
1
- import { RondevuOffers } from './offers.js';
2
- import { EventEmitter } from './event-emitter.js';
3
- /**
4
- * Timeout configurations for different connection phases
5
- */
6
- export interface PeerTimeouts {
7
- /** Timeout for ICE gathering (default: 10000ms) */
8
- iceGathering?: number;
9
- /** Timeout for waiting for answer (default: 30000ms) */
10
- waitingForAnswer?: number;
11
- /** Timeout for creating answer (default: 10000ms) */
12
- creatingAnswer?: number;
13
- /** Timeout for ICE connection (default: 30000ms) */
14
- iceConnection?: number;
15
- }
16
- /**
17
- * Options for creating a peer connection
18
- */
19
- export interface PeerOptions {
20
- /** RTCConfiguration for the peer connection */
21
- rtcConfig?: RTCConfiguration;
22
- /** Topics to advertise this connection under */
23
- topics: string[];
24
- /** How long the offer should live (milliseconds) */
25
- ttl?: number;
26
- /** Whether to create a data channel automatically (for offerer) */
27
- createDataChannel?: boolean;
28
- /** Label for the automatically created data channel */
29
- dataChannelLabel?: string;
30
- /** Timeout configurations */
31
- timeouts?: PeerTimeouts;
32
- }
33
- /**
34
- * Events emitted by RondevuPeer
35
- */
36
- export interface PeerEvents extends Record<string, (...args: any[]) => void> {
37
- 'state': (state: string) => void;
38
- 'connected': () => void;
39
- 'disconnected': () => void;
40
- 'failed': (error: Error) => void;
41
- 'datachannel': (channel: RTCDataChannel) => void;
42
- 'track': (event: RTCTrackEvent) => void;
43
- }
44
- /**
45
- * Base class for peer connection states
46
- */
47
- declare abstract class PeerState {
48
- protected peer: RondevuPeer;
49
- constructor(peer: RondevuPeer);
50
- abstract get name(): string;
51
- createOffer(options: PeerOptions): Promise<string>;
52
- answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
53
- handleAnswer(sdp: string): Promise<void>;
54
- handleIceCandidate(candidate: any): Promise<void>;
55
- cleanup(): void;
56
- close(): void;
57
- }
58
- /**
59
- * High-level WebRTC peer connection manager with state-based lifecycle
60
- * Handles offer/answer exchange, ICE candidates, timeouts, and error recovery
61
- */
62
- export default class RondevuPeer extends EventEmitter<PeerEvents> {
63
- pc: RTCPeerConnection;
64
- offersApi: RondevuOffers;
65
- offerId?: string;
66
- role?: 'offerer' | 'answerer';
67
- private _state;
68
- /**
69
- * Current connection state name
70
- */
71
- get stateName(): string;
72
- /**
73
- * Current state object (internal use)
74
- */
75
- get state(): PeerState;
76
- /**
77
- * RTCPeerConnection state
78
- */
79
- get connectionState(): RTCPeerConnectionState;
80
- constructor(offersApi: RondevuOffers, rtcConfig?: RTCConfiguration);
81
- /**
82
- * Set up peer connection event handlers
83
- */
84
- private setupPeerConnection;
85
- /**
86
- * Set new state and emit state change event
87
- */
88
- setState(newState: PeerState): void;
89
- /**
90
- * Emit event (exposed for PeerState classes)
91
- * @internal
92
- */
93
- emitEvent<K extends keyof PeerEvents>(event: K, ...args: Parameters<PeerEvents[K]>): void;
94
- /**
95
- * Create an offer and advertise on topics
96
- */
97
- createOffer(options: PeerOptions): Promise<string>;
98
- /**
99
- * Answer an existing offer
100
- */
101
- answer(offerId: string, offerSdp: string, options: PeerOptions): Promise<void>;
102
- /**
103
- * Add a media track to the connection
104
- */
105
- addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender;
106
- /**
107
- * Close the connection and clean up
108
- */
109
- close(): void;
110
- }
111
- export {};
package/dist/peer.js DELETED
@@ -1,392 +0,0 @@
1
- import { EventEmitter } from './event-emitter.js';
2
- /**
3
- * Base class for peer connection states
4
- */
5
- class PeerState {
6
- constructor(peer) {
7
- this.peer = peer;
8
- }
9
- async createOffer(options) {
10
- throw new Error(`Cannot create offer in ${this.name} state`);
11
- }
12
- async answer(offerId, offerSdp, options) {
13
- throw new Error(`Cannot answer in ${this.name} state`);
14
- }
15
- async handleAnswer(sdp) {
16
- throw new Error(`Cannot handle answer in ${this.name} state`);
17
- }
18
- async handleIceCandidate(candidate) {
19
- // ICE candidates can arrive in multiple states, so default is to add them
20
- if (this.peer.pc.remoteDescription) {
21
- await this.peer.pc.addIceCandidate(new RTCIceCandidate(candidate));
22
- }
23
- }
24
- cleanup() {
25
- // Override in states that need cleanup
26
- }
27
- close() {
28
- this.cleanup();
29
- this.peer.setState(new ClosedState(this.peer));
30
- }
31
- }
32
- /**
33
- * Initial idle state
34
- */
35
- class IdleState extends PeerState {
36
- get name() { return 'idle'; }
37
- async createOffer(options) {
38
- this.peer.setState(new CreatingOfferState(this.peer, options));
39
- return this.peer.state.createOffer(options);
40
- }
41
- async answer(offerId, offerSdp, options) {
42
- this.peer.setState(new AnsweringState(this.peer));
43
- return this.peer.state.answer(offerId, offerSdp, options);
44
- }
45
- }
46
- /**
47
- * Creating offer and sending to server
48
- */
49
- class CreatingOfferState extends PeerState {
50
- constructor(peer, options) {
51
- super(peer);
52
- this.options = options;
53
- }
54
- get name() { return 'creating-offer'; }
55
- async createOffer(options) {
56
- try {
57
- this.peer.role = 'offerer';
58
- // Create data channel if requested
59
- if (options.createDataChannel !== false) {
60
- const channel = this.peer.pc.createDataChannel(options.dataChannelLabel || 'data');
61
- this.peer.emitEvent('datachannel', channel);
62
- }
63
- // Create WebRTC offer
64
- const offer = await this.peer.pc.createOffer();
65
- await this.peer.pc.setLocalDescription(offer);
66
- // Send offer to server immediately (don't wait for ICE)
67
- const offers = await this.peer.offersApi.create([{
68
- sdp: offer.sdp,
69
- topics: options.topics,
70
- ttl: options.ttl || 300000
71
- }]);
72
- const offerId = offers[0].id;
73
- this.peer.offerId = offerId;
74
- // Enable trickle ICE - send candidates as they arrive
75
- this.peer.pc.onicecandidate = async (event) => {
76
- if (event.candidate && offerId) {
77
- const candidateData = event.candidate.toJSON();
78
- if (candidateData.candidate && candidateData.candidate !== '') {
79
- try {
80
- await this.peer.offersApi.addIceCandidates(offerId, [candidateData]);
81
- }
82
- catch (err) {
83
- console.error('Error sending ICE candidate:', err);
84
- }
85
- }
86
- }
87
- };
88
- // Transition to waiting for answer
89
- this.peer.setState(new WaitingForAnswerState(this.peer, offerId, options));
90
- return offerId;
91
- }
92
- catch (error) {
93
- this.peer.setState(new FailedState(this.peer, error));
94
- throw error;
95
- }
96
- }
97
- }
98
- /**
99
- * Waiting for answer from another peer
100
- */
101
- class WaitingForAnswerState extends PeerState {
102
- constructor(peer, offerId, options) {
103
- super(peer);
104
- this.offerId = offerId;
105
- this.options = options;
106
- this.startPolling();
107
- }
108
- get name() { return 'waiting-for-answer'; }
109
- startPolling() {
110
- const answerTimeout = this.options.timeouts?.waitingForAnswer || 30000;
111
- this.timeout = setTimeout(() => {
112
- this.cleanup();
113
- this.peer.setState(new FailedState(this.peer, new Error('Timeout waiting for answer')));
114
- }, answerTimeout);
115
- this.pollingInterval = setInterval(async () => {
116
- try {
117
- const answers = await this.peer.offersApi.getAnswers();
118
- const myAnswer = answers.find(a => a.offerId === this.offerId);
119
- if (myAnswer) {
120
- this.cleanup();
121
- await this.handleAnswer(myAnswer.sdp);
122
- }
123
- }
124
- catch (err) {
125
- console.error('Error polling for answers:', err);
126
- if (err instanceof Error && err.message.includes('not found')) {
127
- this.cleanup();
128
- this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
129
- }
130
- }
131
- }, 2000);
132
- }
133
- async handleAnswer(sdp) {
134
- try {
135
- await this.peer.pc.setRemoteDescription({
136
- type: 'answer',
137
- sdp
138
- });
139
- // Transition to exchanging ICE
140
- this.peer.setState(new ExchangingIceState(this.peer, this.offerId, this.options));
141
- }
142
- catch (error) {
143
- this.peer.setState(new FailedState(this.peer, error));
144
- }
145
- }
146
- cleanup() {
147
- if (this.pollingInterval)
148
- clearInterval(this.pollingInterval);
149
- if (this.timeout)
150
- clearTimeout(this.timeout);
151
- }
152
- }
153
- /**
154
- * Answering an offer and sending to server
155
- */
156
- class AnsweringState extends PeerState {
157
- constructor(peer) {
158
- super(peer);
159
- }
160
- get name() { return 'answering'; }
161
- async answer(offerId, offerSdp, options) {
162
- try {
163
- this.peer.role = 'answerer';
164
- this.peer.offerId = offerId;
165
- // Set remote description
166
- await this.peer.pc.setRemoteDescription({
167
- type: 'offer',
168
- sdp: offerSdp
169
- });
170
- // Create answer
171
- const answer = await this.peer.pc.createAnswer();
172
- await this.peer.pc.setLocalDescription(answer);
173
- // Send answer to server immediately (don't wait for ICE)
174
- await this.peer.offersApi.answer(offerId, answer.sdp);
175
- // Enable trickle ICE - send candidates as they arrive
176
- this.peer.pc.onicecandidate = async (event) => {
177
- if (event.candidate && offerId) {
178
- const candidateData = event.candidate.toJSON();
179
- if (candidateData.candidate && candidateData.candidate !== '') {
180
- try {
181
- await this.peer.offersApi.addIceCandidates(offerId, [candidateData]);
182
- }
183
- catch (err) {
184
- console.error('Error sending ICE candidate:', err);
185
- }
186
- }
187
- }
188
- };
189
- // Transition to exchanging ICE
190
- this.peer.setState(new ExchangingIceState(this.peer, offerId, options));
191
- }
192
- catch (error) {
193
- this.peer.setState(new FailedState(this.peer, error));
194
- throw error;
195
- }
196
- }
197
- }
198
- /**
199
- * Exchanging ICE candidates and waiting for connection
200
- */
201
- class ExchangingIceState extends PeerState {
202
- constructor(peer, offerId, options) {
203
- super(peer);
204
- this.offerId = offerId;
205
- this.options = options;
206
- this.lastIceTimestamp = 0;
207
- this.startPolling();
208
- }
209
- get name() { return 'exchanging-ice'; }
210
- startPolling() {
211
- const connectionTimeout = this.options.timeouts?.iceConnection || 30000;
212
- this.timeout = setTimeout(() => {
213
- this.cleanup();
214
- this.peer.setState(new FailedState(this.peer, new Error('ICE connection timeout')));
215
- }, connectionTimeout);
216
- this.pollingInterval = setInterval(async () => {
217
- try {
218
- const candidates = await this.peer.offersApi.getIceCandidates(this.offerId, this.lastIceTimestamp);
219
- for (const cand of candidates) {
220
- if (cand.candidate && cand.candidate.candidate && cand.candidate.candidate !== '') {
221
- try {
222
- await this.peer.pc.addIceCandidate(new RTCIceCandidate(cand.candidate));
223
- this.lastIceTimestamp = cand.createdAt;
224
- }
225
- catch (err) {
226
- console.warn('Failed to add ICE candidate:', err);
227
- this.lastIceTimestamp = cand.createdAt;
228
- }
229
- }
230
- else {
231
- this.lastIceTimestamp = cand.createdAt;
232
- }
233
- }
234
- }
235
- catch (err) {
236
- console.error('Error polling for ICE candidates:', err);
237
- if (err instanceof Error && err.message.includes('not found')) {
238
- this.cleanup();
239
- this.peer.setState(new FailedState(this.peer, new Error('Offer expired or not found')));
240
- }
241
- }
242
- }, 1000);
243
- }
244
- cleanup() {
245
- if (this.pollingInterval)
246
- clearInterval(this.pollingInterval);
247
- if (this.timeout)
248
- clearTimeout(this.timeout);
249
- }
250
- }
251
- /**
252
- * Successfully connected state
253
- */
254
- class ConnectedState extends PeerState {
255
- get name() { return 'connected'; }
256
- cleanup() {
257
- // Keep connection alive, but stop any polling
258
- // The peer connection will handle disconnects via onconnectionstatechange
259
- }
260
- }
261
- /**
262
- * Failed state
263
- */
264
- class FailedState extends PeerState {
265
- constructor(peer, error) {
266
- super(peer);
267
- this.error = error;
268
- peer.emitEvent('failed', error);
269
- }
270
- get name() { return 'failed'; }
271
- cleanup() {
272
- // Connection is failed, clean up resources
273
- this.peer.pc.close();
274
- }
275
- }
276
- /**
277
- * Closed state
278
- */
279
- class ClosedState extends PeerState {
280
- get name() { return 'closed'; }
281
- cleanup() {
282
- this.peer.pc.close();
283
- }
284
- }
285
- /**
286
- * High-level WebRTC peer connection manager with state-based lifecycle
287
- * Handles offer/answer exchange, ICE candidates, timeouts, and error recovery
288
- */
289
- export default class RondevuPeer extends EventEmitter {
290
- /**
291
- * Current connection state name
292
- */
293
- get stateName() {
294
- return this._state.name;
295
- }
296
- /**
297
- * Current state object (internal use)
298
- */
299
- get state() {
300
- return this._state;
301
- }
302
- /**
303
- * RTCPeerConnection state
304
- */
305
- get connectionState() {
306
- return this.pc.connectionState;
307
- }
308
- constructor(offersApi, rtcConfig = {
309
- iceServers: [
310
- { urls: 'stun:stun.l.google.com:19302' },
311
- { urls: 'stun:stun1.l.google.com:19302' }
312
- ]
313
- }) {
314
- super();
315
- this.offersApi = offersApi;
316
- this.pc = new RTCPeerConnection(rtcConfig);
317
- this._state = new IdleState(this);
318
- this.setupPeerConnection();
319
- }
320
- /**
321
- * Set up peer connection event handlers
322
- */
323
- setupPeerConnection() {
324
- this.pc.onconnectionstatechange = () => {
325
- switch (this.pc.connectionState) {
326
- case 'connected':
327
- this.setState(new ConnectedState(this));
328
- this.emitEvent('connected');
329
- break;
330
- case 'disconnected':
331
- this.emitEvent('disconnected');
332
- break;
333
- case 'failed':
334
- this.setState(new FailedState(this, new Error('Connection failed')));
335
- break;
336
- case 'closed':
337
- this.setState(new ClosedState(this));
338
- this.emitEvent('disconnected');
339
- break;
340
- }
341
- };
342
- this.pc.ondatachannel = (event) => {
343
- this.emitEvent('datachannel', event.channel);
344
- };
345
- this.pc.ontrack = (event) => {
346
- this.emitEvent('track', event);
347
- };
348
- this.pc.onicecandidateerror = (event) => {
349
- console.error('ICE candidate error:', event);
350
- };
351
- }
352
- /**
353
- * Set new state and emit state change event
354
- */
355
- setState(newState) {
356
- this._state.cleanup();
357
- this._state = newState;
358
- this.emitEvent('state', newState.name);
359
- }
360
- /**
361
- * Emit event (exposed for PeerState classes)
362
- * @internal
363
- */
364
- emitEvent(event, ...args) {
365
- this.emit(event, ...args);
366
- }
367
- /**
368
- * Create an offer and advertise on topics
369
- */
370
- async createOffer(options) {
371
- return this._state.createOffer(options);
372
- }
373
- /**
374
- * Answer an existing offer
375
- */
376
- async answer(offerId, offerSdp, options) {
377
- return this._state.answer(offerId, offerSdp, options);
378
- }
379
- /**
380
- * Add a media track to the connection
381
- */
382
- addTrack(track, ...streams) {
383
- return this.pc.addTrack(track, ...streams);
384
- }
385
- /**
386
- * Close the connection and clean up
387
- */
388
- close() {
389
- this._state.close();
390
- this.removeAllListeners();
391
- }
392
- }