@xtr-dev/rondevu-client 0.18.9 → 0.18.10
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/index.d.ts +0 -9
- package/dist/index.js +0 -7
- package/dist/rondevu.d.ts +64 -77
- package/dist/rondevu.js +290 -186
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -5,18 +5,9 @@
|
|
|
5
5
|
export { Rondevu, RondevuError, NetworkError, ValidationError, ConnectionError } from './rondevu.js';
|
|
6
6
|
export { RondevuAPI } from './api.js';
|
|
7
7
|
export { RpcBatcher } from './rpc-batcher.js';
|
|
8
|
-
export { RondevuConnection } from './connection.js';
|
|
9
|
-
export { OffererConnection } from './offerer-connection.js';
|
|
10
|
-
export { AnswererConnection } from './answerer-connection.js';
|
|
11
|
-
export { ExponentialBackoff } from './exponential-backoff.js';
|
|
12
|
-
export { MessageBuffer } from './message-buffer.js';
|
|
13
8
|
export { WebCryptoAdapter } from './web-crypto-adapter.js';
|
|
14
9
|
export { NodeCryptoAdapter } from './node-crypto-adapter.js';
|
|
15
10
|
export type { Signaler, Binnable, } from './types.js';
|
|
16
11
|
export type { Keypair, OfferRequest, ServiceRequest, Service, ServiceOffer, IceCandidate, } from './api.js';
|
|
17
12
|
export type { RondevuOptions, PublishServiceOptions, ConnectToServiceOptions, ConnectionContext, OfferContext, OfferFactory, ActiveOffer, FindServiceOptions, ServiceResult, PaginatedServiceResult } from './rondevu.js';
|
|
18
13
|
export type { CryptoAdapter } from './crypto-adapter.js';
|
|
19
|
-
export type { ConnectionConfig, } from './connection-config.js';
|
|
20
|
-
export type { ConnectionState, BufferedMessage, ReconnectInfo, StateChangeInfo, ConnectionEventMap, ConnectionEventName, ConnectionEventArgs, } from './connection-events.js';
|
|
21
|
-
export type { OffererOptions, } from './offerer-connection.js';
|
|
22
|
-
export type { AnswererOptions, } from './answerer-connection.js';
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,6 @@
|
|
|
5
5
|
export { Rondevu, RondevuError, NetworkError, ValidationError, ConnectionError } from './rondevu.js';
|
|
6
6
|
export { RondevuAPI } from './api.js';
|
|
7
7
|
export { RpcBatcher } from './rpc-batcher.js';
|
|
8
|
-
// Export connection classes
|
|
9
|
-
export { RondevuConnection } from './connection.js';
|
|
10
|
-
export { OffererConnection } from './offerer-connection.js';
|
|
11
|
-
export { AnswererConnection } from './answerer-connection.js';
|
|
12
|
-
// Export utilities
|
|
13
|
-
export { ExponentialBackoff } from './exponential-backoff.js';
|
|
14
|
-
export { MessageBuffer } from './message-buffer.js';
|
|
15
8
|
// Export crypto adapters
|
|
16
9
|
export { WebCryptoAdapter } from './web-crypto-adapter.js';
|
|
17
10
|
export { NodeCryptoAdapter } from './node-crypto-adapter.js';
|
package/dist/rondevu.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { RondevuAPI, Keypair, IceCandidate, BatcherOptions } from './api.js';
|
|
2
2
|
import { CryptoAdapter } from './crypto-adapter.js';
|
|
3
3
|
import { EventEmitter } from 'eventemitter3';
|
|
4
|
-
import { OffererConnection } from './offerer-connection.js';
|
|
5
|
-
import { AnswererConnection } from './answerer-connection.js';
|
|
6
|
-
import { ConnectionConfig } from './connection-config.js';
|
|
7
4
|
export type IceServerPreset = 'ipv4-turn' | 'hostname-turns' | 'google-stun' | 'relay-only';
|
|
8
5
|
export declare const ICE_SERVER_PRESETS: Record<IceServerPreset, RTCIceServer[]>;
|
|
9
6
|
export interface RondevuOptions {
|
|
@@ -35,7 +32,6 @@ export interface PublishServiceOptions {
|
|
|
35
32
|
maxOffers: number;
|
|
36
33
|
offerFactory?: OfferFactory;
|
|
37
34
|
ttl?: number;
|
|
38
|
-
connectionConfig?: Partial<ConnectionConfig>;
|
|
39
35
|
}
|
|
40
36
|
export interface ConnectionContext {
|
|
41
37
|
pc: RTCPeerConnection;
|
|
@@ -48,8 +44,8 @@ export interface ConnectToServiceOptions {
|
|
|
48
44
|
serviceFqn?: string;
|
|
49
45
|
service?: string;
|
|
50
46
|
username?: string;
|
|
47
|
+
onConnection?: (context: ConnectionContext) => void | Promise<void>;
|
|
51
48
|
rtcConfig?: RTCConfiguration;
|
|
52
|
-
connectionConfig?: Partial<ConnectionConfig>;
|
|
53
49
|
}
|
|
54
50
|
export interface ActiveOffer {
|
|
55
51
|
offerId: string;
|
|
@@ -105,13 +101,14 @@ export declare class ConnectionError extends RondevuError {
|
|
|
105
101
|
constructor(message: string, context?: Record<string, any>);
|
|
106
102
|
}
|
|
107
103
|
/**
|
|
108
|
-
* Rondevu - Complete WebRTC signaling client
|
|
104
|
+
* Rondevu - Complete WebRTC signaling client
|
|
109
105
|
*
|
|
110
|
-
*
|
|
111
|
-
* -
|
|
112
|
-
* -
|
|
113
|
-
* -
|
|
114
|
-
* -
|
|
106
|
+
* Provides a unified API for:
|
|
107
|
+
* - Implicit username claiming (auto-claimed on first authenticated request)
|
|
108
|
+
* - Service publishing with automatic signature generation
|
|
109
|
+
* - Service discovery (direct, random, paginated)
|
|
110
|
+
* - WebRTC signaling (offer/answer exchange, ICE relay)
|
|
111
|
+
* - Keypair management
|
|
115
112
|
*
|
|
116
113
|
* @example
|
|
117
114
|
* ```typescript
|
|
@@ -122,39 +119,39 @@ export declare class ConnectionError extends RondevuError {
|
|
|
122
119
|
* iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
|
|
123
120
|
* })
|
|
124
121
|
*
|
|
122
|
+
* // Or use custom ICE servers
|
|
123
|
+
* const rondevu2 = await Rondevu.connect({
|
|
124
|
+
* apiUrl: 'https://signal.example.com',
|
|
125
|
+
* username: 'bob',
|
|
126
|
+
* iceServers: [
|
|
127
|
+
* { urls: 'stun:stun.l.google.com:19302' },
|
|
128
|
+
* { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' }
|
|
129
|
+
* ]
|
|
130
|
+
* })
|
|
131
|
+
*
|
|
125
132
|
* // Publish a service with automatic offer management
|
|
126
133
|
* await rondevu.publishService({
|
|
127
134
|
* service: 'chat:2.0.0',
|
|
128
|
-
* maxOffers: 5 // Maintain up to 5 concurrent offers
|
|
135
|
+
* maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
136
|
+
* offerFactory: async (pc) => {
|
|
137
|
+
* // pc is created by Rondevu with ICE handlers already attached
|
|
138
|
+
* const dc = pc.createDataChannel('chat')
|
|
139
|
+
* const offer = await pc.createOffer()
|
|
140
|
+
* await pc.setLocalDescription(offer)
|
|
141
|
+
* return { dc, offer }
|
|
142
|
+
* }
|
|
129
143
|
* })
|
|
130
144
|
*
|
|
131
145
|
* // Start accepting connections (auto-fills offers and polls)
|
|
132
146
|
* await rondevu.startFilling()
|
|
133
147
|
*
|
|
134
|
-
* //
|
|
135
|
-
* rondevu.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* connection.send('Hello!')
|
|
139
|
-
* })
|
|
140
|
-
*
|
|
141
|
-
* // Connect to a service (v1.0.0 - returns AnswererConnection)
|
|
142
|
-
* const connection = await rondevu.connectToService({
|
|
143
|
-
* serviceFqn: 'chat:2.0.0@bob'
|
|
144
|
-
* })
|
|
145
|
-
*
|
|
146
|
-
* connection.on('connected', () => {
|
|
147
|
-
* console.log('Connected!')
|
|
148
|
-
* connection.send('Hello!')
|
|
149
|
-
* })
|
|
150
|
-
*
|
|
151
|
-
* connection.on('message', (data) => {
|
|
152
|
-
* console.log('Received:', data)
|
|
153
|
-
* })
|
|
148
|
+
* // Access active connections
|
|
149
|
+
* for (const offer of rondevu.getActiveOffers()) {
|
|
150
|
+
* offer.dc?.addEventListener('message', (e) => console.log(e.data))
|
|
151
|
+
* }
|
|
154
152
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* })
|
|
153
|
+
* // Stop when done
|
|
154
|
+
* rondevu.stopFilling()
|
|
158
155
|
* ```
|
|
159
156
|
*/
|
|
160
157
|
export declare class Rondevu extends EventEmitter {
|
|
@@ -175,12 +172,11 @@ export declare class Rondevu extends EventEmitter {
|
|
|
175
172
|
private maxOffers;
|
|
176
173
|
private offerFactory;
|
|
177
174
|
private ttl;
|
|
178
|
-
private
|
|
179
|
-
private connectionConfig?;
|
|
175
|
+
private activeOffers;
|
|
180
176
|
private filling;
|
|
181
|
-
private fillingSemaphore;
|
|
182
177
|
private pollingInterval;
|
|
183
178
|
private lastPollTimestamp;
|
|
179
|
+
private isPolling;
|
|
184
180
|
private constructor();
|
|
185
181
|
/**
|
|
186
182
|
* Internal debug logging - only logs if debug mode is enabled
|
|
@@ -219,22 +215,26 @@ export declare class Rondevu extends EventEmitter {
|
|
|
219
215
|
* ```typescript
|
|
220
216
|
* await rondevu.publishService({
|
|
221
217
|
* service: 'chat:2.0.0',
|
|
222
|
-
* maxOffers: 5
|
|
223
|
-
* connectionConfig: {
|
|
224
|
-
* reconnectEnabled: true,
|
|
225
|
-
* bufferEnabled: true
|
|
226
|
-
* }
|
|
218
|
+
* maxOffers: 5
|
|
227
219
|
* })
|
|
228
220
|
* await rondevu.startFilling()
|
|
229
221
|
* ```
|
|
230
222
|
*/
|
|
231
223
|
publishService(options: PublishServiceOptions): Promise<void>;
|
|
232
224
|
/**
|
|
233
|
-
*
|
|
225
|
+
* Set up ICE candidate handler to send candidates to the server
|
|
226
|
+
*
|
|
227
|
+
* Note: This is used by connectToService() where the offerId is already known.
|
|
228
|
+
* For createOffer(), we use inline ICE handling with early candidate queuing
|
|
229
|
+
* since the offerId isn't available until after the factory completes.
|
|
230
|
+
*/
|
|
231
|
+
private setupIceCandidateHandler;
|
|
232
|
+
/**
|
|
233
|
+
* Create a single offer and publish it to the server
|
|
234
234
|
*/
|
|
235
235
|
private createOffer;
|
|
236
236
|
/**
|
|
237
|
-
* Fill offers to reach maxOffers count
|
|
237
|
+
* Fill offers to reach maxOffers count
|
|
238
238
|
*/
|
|
239
239
|
private fillOffers;
|
|
240
240
|
/**
|
|
@@ -259,7 +259,7 @@ export declare class Rondevu extends EventEmitter {
|
|
|
259
259
|
/**
|
|
260
260
|
* Check if an offer is currently connected
|
|
261
261
|
* @param offerId - The offer ID to check
|
|
262
|
-
* @returns True if the offer exists and
|
|
262
|
+
* @returns True if the offer exists and has been answered
|
|
263
263
|
*/
|
|
264
264
|
isConnected(offerId: string): boolean;
|
|
265
265
|
/**
|
|
@@ -283,45 +283,41 @@ export declare class Rondevu extends EventEmitter {
|
|
|
283
283
|
*/
|
|
284
284
|
private resolveServiceFqn;
|
|
285
285
|
/**
|
|
286
|
-
*
|
|
287
|
-
* Returns
|
|
288
|
-
|
|
289
|
-
|
|
286
|
+
* Start polling for remote ICE candidates
|
|
287
|
+
* Returns the polling interval ID
|
|
288
|
+
*/
|
|
289
|
+
private startIcePolling;
|
|
290
|
+
/**
|
|
291
|
+
* Automatically connect to a service (answerer side)
|
|
292
|
+
* Handles the entire connection flow: discovery, WebRTC setup, answer exchange, ICE candidates
|
|
290
293
|
*
|
|
291
294
|
* @example
|
|
292
295
|
* ```typescript
|
|
293
296
|
* // Connect to specific user
|
|
294
297
|
* const connection = await rondevu.connectToService({
|
|
295
298
|
* serviceFqn: 'chat:2.0.0@alice',
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
+
* onConnection: ({ dc, peerUsername }) => {
|
|
300
|
+
* console.log('Connected to', peerUsername)
|
|
301
|
+
* dc.addEventListener('message', (e) => console.log(e.data))
|
|
302
|
+
* dc.addEventListener('open', () => dc.send('Hello!'))
|
|
299
303
|
* }
|
|
300
304
|
* })
|
|
301
305
|
*
|
|
302
|
-
* connection.on('connected', () => {
|
|
303
|
-
* console.log('Connected!')
|
|
304
|
-
* connection.send('Hello!')
|
|
305
|
-
* })
|
|
306
|
-
*
|
|
307
|
-
* connection.on('message', (data) => {
|
|
308
|
-
* console.log('Received:', data)
|
|
309
|
-
* })
|
|
310
|
-
*
|
|
311
|
-
* connection.on('reconnecting', (attempt) => {
|
|
312
|
-
* console.log(`Reconnecting, attempt ${attempt}`)
|
|
313
|
-
* })
|
|
314
|
-
*
|
|
315
306
|
* // Discover random service
|
|
316
307
|
* const connection = await rondevu.connectToService({
|
|
317
|
-
* service: 'chat:2.0.0'
|
|
308
|
+
* service: 'chat:2.0.0',
|
|
309
|
+
* onConnection: ({ dc, peerUsername }) => {
|
|
310
|
+
* console.log('Connected to', peerUsername)
|
|
311
|
+
* }
|
|
318
312
|
* })
|
|
319
313
|
* ```
|
|
320
314
|
*/
|
|
321
|
-
connectToService(options: ConnectToServiceOptions): Promise<
|
|
315
|
+
connectToService(options: ConnectToServiceOptions): Promise<ConnectionContext>;
|
|
322
316
|
/**
|
|
323
317
|
* Find a service - unified discovery method
|
|
324
318
|
*
|
|
319
|
+
* Replaces getService(), discoverService(), and discoverServices() with a single method.
|
|
320
|
+
*
|
|
325
321
|
* @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
|
|
326
322
|
* @param options - Discovery options
|
|
327
323
|
*
|
|
@@ -403,15 +399,6 @@ export declare class Rondevu extends EventEmitter {
|
|
|
403
399
|
* Get the public key
|
|
404
400
|
*/
|
|
405
401
|
getPublicKey(): string;
|
|
406
|
-
/**
|
|
407
|
-
* Get active connections (for offerer side)
|
|
408
|
-
*/
|
|
409
|
-
getActiveConnections(): Map<string, OffererConnection>;
|
|
410
|
-
/**
|
|
411
|
-
* Get all active offers (legacy compatibility)
|
|
412
|
-
* @deprecated Use getActiveConnections() instead
|
|
413
|
-
*/
|
|
414
|
-
getActiveOffers(): ActiveOffer[];
|
|
415
402
|
/**
|
|
416
403
|
* Access to underlying API for advanced operations
|
|
417
404
|
* @deprecated Use direct methods on Rondevu instance instead
|
package/dist/rondevu.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { RondevuAPI } from './api.js';
|
|
2
2
|
import { EventEmitter } from 'eventemitter3';
|
|
3
|
-
import { OffererConnection } from './offerer-connection.js';
|
|
4
|
-
import { AnswererConnection } from './answerer-connection.js';
|
|
5
3
|
// ICE server presets
|
|
6
4
|
export const ICE_SERVER_PRESETS = {
|
|
7
5
|
'ipv4-turn': [
|
|
@@ -88,13 +86,14 @@ export class ConnectionError extends RondevuError {
|
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
/**
|
|
91
|
-
* Rondevu - Complete WebRTC signaling client
|
|
89
|
+
* Rondevu - Complete WebRTC signaling client
|
|
92
90
|
*
|
|
93
|
-
*
|
|
94
|
-
* -
|
|
95
|
-
* -
|
|
96
|
-
* -
|
|
97
|
-
* -
|
|
91
|
+
* Provides a unified API for:
|
|
92
|
+
* - Implicit username claiming (auto-claimed on first authenticated request)
|
|
93
|
+
* - Service publishing with automatic signature generation
|
|
94
|
+
* - Service discovery (direct, random, paginated)
|
|
95
|
+
* - WebRTC signaling (offer/answer exchange, ICE relay)
|
|
96
|
+
* - Keypair management
|
|
98
97
|
*
|
|
99
98
|
* @example
|
|
100
99
|
* ```typescript
|
|
@@ -105,39 +104,39 @@ export class ConnectionError extends RondevuError {
|
|
|
105
104
|
* iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
|
|
106
105
|
* })
|
|
107
106
|
*
|
|
107
|
+
* // Or use custom ICE servers
|
|
108
|
+
* const rondevu2 = await Rondevu.connect({
|
|
109
|
+
* apiUrl: 'https://signal.example.com',
|
|
110
|
+
* username: 'bob',
|
|
111
|
+
* iceServers: [
|
|
112
|
+
* { urls: 'stun:stun.l.google.com:19302' },
|
|
113
|
+
* { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' }
|
|
114
|
+
* ]
|
|
115
|
+
* })
|
|
116
|
+
*
|
|
108
117
|
* // Publish a service with automatic offer management
|
|
109
118
|
* await rondevu.publishService({
|
|
110
119
|
* service: 'chat:2.0.0',
|
|
111
|
-
* maxOffers: 5 // Maintain up to 5 concurrent offers
|
|
120
|
+
* maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
121
|
+
* offerFactory: async (pc) => {
|
|
122
|
+
* // pc is created by Rondevu with ICE handlers already attached
|
|
123
|
+
* const dc = pc.createDataChannel('chat')
|
|
124
|
+
* const offer = await pc.createOffer()
|
|
125
|
+
* await pc.setLocalDescription(offer)
|
|
126
|
+
* return { dc, offer }
|
|
127
|
+
* }
|
|
112
128
|
* })
|
|
113
129
|
*
|
|
114
130
|
* // Start accepting connections (auto-fills offers and polls)
|
|
115
131
|
* await rondevu.startFilling()
|
|
116
132
|
*
|
|
117
|
-
* //
|
|
118
|
-
* rondevu.
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* connection.send('Hello!')
|
|
122
|
-
* })
|
|
133
|
+
* // Access active connections
|
|
134
|
+
* for (const offer of rondevu.getActiveOffers()) {
|
|
135
|
+
* offer.dc?.addEventListener('message', (e) => console.log(e.data))
|
|
136
|
+
* }
|
|
123
137
|
*
|
|
124
|
-
* //
|
|
125
|
-
*
|
|
126
|
-
* serviceFqn: 'chat:2.0.0@bob'
|
|
127
|
-
* })
|
|
128
|
-
*
|
|
129
|
-
* connection.on('connected', () => {
|
|
130
|
-
* console.log('Connected!')
|
|
131
|
-
* connection.send('Hello!')
|
|
132
|
-
* })
|
|
133
|
-
*
|
|
134
|
-
* connection.on('message', (data) => {
|
|
135
|
-
* console.log('Received:', data)
|
|
136
|
-
* })
|
|
137
|
-
*
|
|
138
|
-
* connection.on('reconnecting', (attempt) => {
|
|
139
|
-
* console.log(`Reconnecting, attempt ${attempt}`)
|
|
140
|
-
* })
|
|
138
|
+
* // Stop when done
|
|
139
|
+
* rondevu.stopFilling()
|
|
141
140
|
* ```
|
|
142
141
|
*/
|
|
143
142
|
export class Rondevu extends EventEmitter {
|
|
@@ -149,12 +148,12 @@ export class Rondevu extends EventEmitter {
|
|
|
149
148
|
this.maxOffers = 0;
|
|
150
149
|
this.offerFactory = null;
|
|
151
150
|
this.ttl = Rondevu.DEFAULT_TTL_MS;
|
|
152
|
-
this.
|
|
151
|
+
this.activeOffers = new Map();
|
|
153
152
|
// Polling
|
|
154
153
|
this.filling = false;
|
|
155
|
-
this.fillingSemaphore = false; // Semaphore to prevent concurrent fillOffers calls
|
|
156
154
|
this.pollingInterval = null;
|
|
157
155
|
this.lastPollTimestamp = 0;
|
|
156
|
+
this.isPolling = false; // Guard against concurrent poll execution
|
|
158
157
|
this.apiUrl = apiUrl;
|
|
159
158
|
this.username = username;
|
|
160
159
|
this.keypair = keypair;
|
|
@@ -285,27 +284,49 @@ export class Rondevu extends EventEmitter {
|
|
|
285
284
|
* ```typescript
|
|
286
285
|
* await rondevu.publishService({
|
|
287
286
|
* service: 'chat:2.0.0',
|
|
288
|
-
* maxOffers: 5
|
|
289
|
-
* connectionConfig: {
|
|
290
|
-
* reconnectEnabled: true,
|
|
291
|
-
* bufferEnabled: true
|
|
292
|
-
* }
|
|
287
|
+
* maxOffers: 5
|
|
293
288
|
* })
|
|
294
289
|
* await rondevu.startFilling()
|
|
295
290
|
* ```
|
|
296
291
|
*/
|
|
297
292
|
async publishService(options) {
|
|
298
|
-
const { service, maxOffers, offerFactory, ttl
|
|
293
|
+
const { service, maxOffers, offerFactory, ttl } = options;
|
|
299
294
|
this.currentService = service;
|
|
300
295
|
this.maxOffers = maxOffers;
|
|
301
296
|
this.offerFactory = offerFactory || this.defaultOfferFactory.bind(this);
|
|
302
297
|
this.ttl = ttl || Rondevu.DEFAULT_TTL_MS;
|
|
303
|
-
this.connectionConfig = connectionConfig;
|
|
304
298
|
this.debug(`Publishing service: ${service} with maxOffers: ${maxOffers}`);
|
|
305
299
|
this.usernameClaimed = true;
|
|
306
300
|
}
|
|
307
301
|
/**
|
|
308
|
-
*
|
|
302
|
+
* Set up ICE candidate handler to send candidates to the server
|
|
303
|
+
*
|
|
304
|
+
* Note: This is used by connectToService() where the offerId is already known.
|
|
305
|
+
* For createOffer(), we use inline ICE handling with early candidate queuing
|
|
306
|
+
* since the offerId isn't available until after the factory completes.
|
|
307
|
+
*/
|
|
308
|
+
setupIceCandidateHandler(pc, serviceFqn, offerId) {
|
|
309
|
+
pc.onicecandidate = async (event) => {
|
|
310
|
+
if (event.candidate) {
|
|
311
|
+
try {
|
|
312
|
+
// Handle both browser and Node.js (wrtc) environments
|
|
313
|
+
// Browser: candidate.toJSON() exists
|
|
314
|
+
// Node.js wrtc: candidate is already a plain object
|
|
315
|
+
const candidateData = typeof event.candidate.toJSON === 'function'
|
|
316
|
+
? event.candidate.toJSON()
|
|
317
|
+
: event.candidate;
|
|
318
|
+
// Emit local ICE candidate event
|
|
319
|
+
this.emit('ice:candidate:local', offerId, candidateData);
|
|
320
|
+
await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
console.error('[Rondevu] Failed to send ICE candidate:', err);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Create a single offer and publish it to the server
|
|
309
330
|
*/
|
|
310
331
|
async createOffer() {
|
|
311
332
|
if (!this.currentService || !this.offerFactory) {
|
|
@@ -317,9 +338,42 @@ export class Rondevu extends EventEmitter {
|
|
|
317
338
|
// Auto-append username to service
|
|
318
339
|
const serviceFqn = `${this.currentService}@${this.username}`;
|
|
319
340
|
this.debug('Creating new offer...');
|
|
320
|
-
// 1. Create RTCPeerConnection
|
|
341
|
+
// 1. Create the RTCPeerConnection - Rondevu controls this to set up handlers early
|
|
321
342
|
const pc = new RTCPeerConnection(rtcConfig);
|
|
322
|
-
// 2.
|
|
343
|
+
// 2. Set up ICE candidate handler with queuing BEFORE the factory runs
|
|
344
|
+
// This ensures we capture all candidates, even those generated immediately
|
|
345
|
+
// when setLocalDescription() is called in the factory
|
|
346
|
+
const earlyIceCandidates = [];
|
|
347
|
+
let offerId;
|
|
348
|
+
pc.onicecandidate = async (event) => {
|
|
349
|
+
if (event.candidate) {
|
|
350
|
+
// Handle both browser and Node.js (wrtc) environments
|
|
351
|
+
const candidateData = typeof event.candidate.toJSON === 'function'
|
|
352
|
+
? event.candidate.toJSON()
|
|
353
|
+
: event.candidate;
|
|
354
|
+
// Emit local ICE candidate event
|
|
355
|
+
if (offerId) {
|
|
356
|
+
this.emit('ice:candidate:local', offerId, candidateData);
|
|
357
|
+
}
|
|
358
|
+
if (offerId) {
|
|
359
|
+
// We have the offerId, send directly
|
|
360
|
+
try {
|
|
361
|
+
await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]);
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
console.error('[Rondevu] Failed to send ICE candidate:', err);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// Queue for later - we don't have the offerId yet
|
|
369
|
+
this.debug('Queuing early ICE candidate');
|
|
370
|
+
earlyIceCandidates.push(candidateData);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
// 3. Call the factory with the pc - factory creates data channel and offer
|
|
375
|
+
// When factory calls setLocalDescription(), ICE gathering starts and
|
|
376
|
+
// candidates are captured by the handler we set up above
|
|
323
377
|
let dc;
|
|
324
378
|
let offer;
|
|
325
379
|
try {
|
|
@@ -328,10 +382,11 @@ export class Rondevu extends EventEmitter {
|
|
|
328
382
|
offer = factoryResult.offer;
|
|
329
383
|
}
|
|
330
384
|
catch (err) {
|
|
385
|
+
// Clean up the connection if factory fails
|
|
331
386
|
pc.close();
|
|
332
387
|
throw err;
|
|
333
388
|
}
|
|
334
|
-
//
|
|
389
|
+
// 4. Publish to server to get offerId
|
|
335
390
|
const result = await this.api.publishService({
|
|
336
391
|
serviceFqn,
|
|
337
392
|
offers: [{ sdp: offer.sdp }],
|
|
@@ -339,68 +394,61 @@ export class Rondevu extends EventEmitter {
|
|
|
339
394
|
signature: '',
|
|
340
395
|
message: '',
|
|
341
396
|
});
|
|
342
|
-
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
api: this.api,
|
|
346
|
-
serviceFqn,
|
|
397
|
+
offerId = result.offers[0].offerId;
|
|
398
|
+
// 5. Store active offer
|
|
399
|
+
this.activeOffers.set(offerId, {
|
|
347
400
|
offerId,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
// Setup connection event handlers
|
|
356
|
-
connection.on('connected', () => {
|
|
357
|
-
this.debug(`Connection established for offer ${offerId}`);
|
|
358
|
-
this.emit('connection:opened', offerId, connection);
|
|
359
|
-
});
|
|
360
|
-
connection.on('failed', (error) => {
|
|
361
|
-
this.debug(`Connection failed for offer ${offerId}:`, error);
|
|
362
|
-
this.activeConnections.delete(offerId);
|
|
363
|
-
this.fillOffers(); // Replace failed offer
|
|
364
|
-
});
|
|
365
|
-
connection.on('closed', () => {
|
|
366
|
-
this.debug(`Connection closed for offer ${offerId}`);
|
|
367
|
-
this.activeConnections.delete(offerId);
|
|
368
|
-
this.fillOffers(); // Replace closed offer
|
|
401
|
+
serviceFqn,
|
|
402
|
+
pc,
|
|
403
|
+
dc,
|
|
404
|
+
answered: false,
|
|
405
|
+
createdAt: Date.now()
|
|
369
406
|
});
|
|
370
|
-
// Store active connection
|
|
371
|
-
this.activeConnections.set(offerId, connection);
|
|
372
|
-
// Initialize the connection
|
|
373
|
-
await connection.initialize();
|
|
374
407
|
this.debug(`Offer created: ${offerId}`);
|
|
375
408
|
this.emit('offer:created', offerId, serviceFqn);
|
|
409
|
+
// Set up data channel open handler (offerer side)
|
|
410
|
+
if (dc) {
|
|
411
|
+
dc.onopen = () => {
|
|
412
|
+
this.debug(`Data channel opened for offer ${offerId}`);
|
|
413
|
+
this.emit('connection:opened', offerId, dc);
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
// 6. Send any queued early ICE candidates
|
|
417
|
+
if (earlyIceCandidates.length > 0) {
|
|
418
|
+
this.debug(`Sending ${earlyIceCandidates.length} early ICE candidates`);
|
|
419
|
+
try {
|
|
420
|
+
await this.api.addOfferIceCandidates(serviceFqn, offerId, earlyIceCandidates);
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
console.error('[Rondevu] Failed to send early ICE candidates:', err);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// 7. Monitor connection state
|
|
427
|
+
pc.onconnectionstatechange = () => {
|
|
428
|
+
this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`);
|
|
429
|
+
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
|
430
|
+
this.emit('connection:closed', offerId);
|
|
431
|
+
this.activeOffers.delete(offerId);
|
|
432
|
+
this.fillOffers(); // Try to replace failed offer
|
|
433
|
+
}
|
|
434
|
+
};
|
|
376
435
|
}
|
|
377
436
|
/**
|
|
378
|
-
* Fill offers to reach maxOffers count
|
|
437
|
+
* Fill offers to reach maxOffers count
|
|
379
438
|
*/
|
|
380
439
|
async fillOffers() {
|
|
381
440
|
if (!this.filling || !this.currentService)
|
|
382
441
|
return;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.debug(`Filling offers: current=${currentCount}, needed=${needed}`);
|
|
393
|
-
for (let i = 0; i < needed; i++) {
|
|
394
|
-
try {
|
|
395
|
-
await this.createOffer();
|
|
396
|
-
}
|
|
397
|
-
catch (err) {
|
|
398
|
-
console.error('[Rondevu] Failed to create offer:', err);
|
|
399
|
-
}
|
|
442
|
+
const currentCount = this.activeOffers.size;
|
|
443
|
+
const needed = this.maxOffers - currentCount;
|
|
444
|
+
this.debug(`Filling offers: current=${currentCount}, needed=${needed}`);
|
|
445
|
+
for (let i = 0; i < needed; i++) {
|
|
446
|
+
try {
|
|
447
|
+
await this.createOffer();
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
console.error('[Rondevu] Failed to create offer:', err);
|
|
400
451
|
}
|
|
401
|
-
}
|
|
402
|
-
finally {
|
|
403
|
-
this.fillingSemaphore = false;
|
|
404
452
|
}
|
|
405
453
|
}
|
|
406
454
|
/**
|
|
@@ -409,20 +457,41 @@ export class Rondevu extends EventEmitter {
|
|
|
409
457
|
async pollInternal() {
|
|
410
458
|
if (!this.filling)
|
|
411
459
|
return;
|
|
460
|
+
// Prevent concurrent poll execution to avoid duplicate answer processing
|
|
461
|
+
if (this.isPolling) {
|
|
462
|
+
this.debug('Poll already in progress, skipping');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.isPolling = true;
|
|
412
466
|
try {
|
|
413
467
|
const result = await this.api.poll(this.lastPollTimestamp);
|
|
414
|
-
// Process answers
|
|
468
|
+
// Process answers
|
|
415
469
|
for (const answer of result.answers) {
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
470
|
+
const activeOffer = this.activeOffers.get(answer.offerId);
|
|
471
|
+
if (activeOffer && !activeOffer.answered) {
|
|
472
|
+
this.debug(`Received answer for offer ${answer.offerId}`);
|
|
473
|
+
await activeOffer.pc.setRemoteDescription({
|
|
474
|
+
type: 'answer',
|
|
475
|
+
sdp: answer.sdp
|
|
476
|
+
});
|
|
477
|
+
activeOffer.answered = true;
|
|
478
|
+
this.lastPollTimestamp = answer.answeredAt;
|
|
479
|
+
this.emit('offer:answered', answer.offerId, answer.answererId);
|
|
480
|
+
// Create replacement offer
|
|
481
|
+
this.fillOffers();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Process ICE candidates
|
|
485
|
+
for (const [offerId, candidates] of Object.entries(result.iceCandidates)) {
|
|
486
|
+
const activeOffer = this.activeOffers.get(offerId);
|
|
487
|
+
if (activeOffer) {
|
|
488
|
+
const answererCandidates = candidates.filter(c => c.role === 'answerer');
|
|
489
|
+
for (const item of answererCandidates) {
|
|
490
|
+
if (item.candidate) {
|
|
491
|
+
this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
|
|
492
|
+
await activeOffer.pc.addIceCandidate(new RTCIceCandidate(item.candidate));
|
|
493
|
+
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt);
|
|
494
|
+
}
|
|
426
495
|
}
|
|
427
496
|
}
|
|
428
497
|
}
|
|
@@ -430,6 +499,9 @@ export class Rondevu extends EventEmitter {
|
|
|
430
499
|
catch (err) {
|
|
431
500
|
console.error('[Rondevu] Polling error:', err);
|
|
432
501
|
}
|
|
502
|
+
finally {
|
|
503
|
+
this.isPolling = false;
|
|
504
|
+
}
|
|
433
505
|
}
|
|
434
506
|
/**
|
|
435
507
|
* Start filling offers and polling for answers/ICE
|
|
@@ -459,34 +531,35 @@ export class Rondevu extends EventEmitter {
|
|
|
459
531
|
stopFilling() {
|
|
460
532
|
this.debug('Stopping offer filling and polling');
|
|
461
533
|
this.filling = false;
|
|
462
|
-
this.
|
|
534
|
+
this.isPolling = false; // Reset polling guard
|
|
463
535
|
// Stop polling
|
|
464
536
|
if (this.pollingInterval) {
|
|
465
537
|
clearInterval(this.pollingInterval);
|
|
466
538
|
this.pollingInterval = null;
|
|
467
539
|
}
|
|
468
540
|
// Close all active connections
|
|
469
|
-
for (const [offerId,
|
|
470
|
-
this.debug(`Closing
|
|
471
|
-
|
|
541
|
+
for (const [offerId, offer] of this.activeOffers.entries()) {
|
|
542
|
+
this.debug(`Closing offer ${offerId}`);
|
|
543
|
+
offer.dc?.close();
|
|
544
|
+
offer.pc.close();
|
|
472
545
|
}
|
|
473
|
-
this.
|
|
546
|
+
this.activeOffers.clear();
|
|
474
547
|
}
|
|
475
548
|
/**
|
|
476
549
|
* Get the count of active offers
|
|
477
550
|
* @returns Number of active offers
|
|
478
551
|
*/
|
|
479
552
|
getOfferCount() {
|
|
480
|
-
return this.
|
|
553
|
+
return this.activeOffers.size;
|
|
481
554
|
}
|
|
482
555
|
/**
|
|
483
556
|
* Check if an offer is currently connected
|
|
484
557
|
* @param offerId - The offer ID to check
|
|
485
|
-
* @returns True if the offer exists and
|
|
558
|
+
* @returns True if the offer exists and has been answered
|
|
486
559
|
*/
|
|
487
560
|
isConnected(offerId) {
|
|
488
|
-
const
|
|
489
|
-
return
|
|
561
|
+
const offer = this.activeOffers.get(offerId);
|
|
562
|
+
return offer ? offer.answered : false;
|
|
490
563
|
}
|
|
491
564
|
/**
|
|
492
565
|
* Disconnect all active offers
|
|
@@ -494,11 +567,12 @@ export class Rondevu extends EventEmitter {
|
|
|
494
567
|
*/
|
|
495
568
|
async disconnectAll() {
|
|
496
569
|
this.debug('Disconnecting all offers');
|
|
497
|
-
for (const [offerId,
|
|
498
|
-
this.debug(`Closing
|
|
499
|
-
|
|
570
|
+
for (const [offerId, offer] of this.activeOffers.entries()) {
|
|
571
|
+
this.debug(`Closing offer ${offerId}`);
|
|
572
|
+
offer.dc?.close();
|
|
573
|
+
offer.pc.close();
|
|
500
574
|
}
|
|
501
|
-
this.
|
|
575
|
+
this.activeOffers.clear();
|
|
502
576
|
}
|
|
503
577
|
/**
|
|
504
578
|
* Get the current service status
|
|
@@ -507,7 +581,7 @@ export class Rondevu extends EventEmitter {
|
|
|
507
581
|
getServiceStatus() {
|
|
508
582
|
return {
|
|
509
583
|
active: this.currentService !== null,
|
|
510
|
-
offerCount: this.
|
|
584
|
+
offerCount: this.activeOffers.size,
|
|
511
585
|
maxOffers: this.maxOffers,
|
|
512
586
|
filling: this.filling
|
|
513
587
|
};
|
|
@@ -535,43 +609,54 @@ export class Rondevu extends EventEmitter {
|
|
|
535
609
|
}
|
|
536
610
|
}
|
|
537
611
|
/**
|
|
538
|
-
*
|
|
539
|
-
* Returns
|
|
540
|
-
|
|
541
|
-
|
|
612
|
+
* Start polling for remote ICE candidates
|
|
613
|
+
* Returns the polling interval ID
|
|
614
|
+
*/
|
|
615
|
+
startIcePolling(pc, serviceFqn, offerId) {
|
|
616
|
+
let lastIceTimestamp = 0;
|
|
617
|
+
return setInterval(async () => {
|
|
618
|
+
try {
|
|
619
|
+
const result = await this.api.getOfferIceCandidates(serviceFqn, offerId, lastIceTimestamp);
|
|
620
|
+
for (const item of result.candidates) {
|
|
621
|
+
if (item.candidate) {
|
|
622
|
+
this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
|
|
623
|
+
await pc.addIceCandidate(new RTCIceCandidate(item.candidate));
|
|
624
|
+
lastIceTimestamp = item.createdAt;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
console.error('[Rondevu] Failed to poll ICE candidates:', err);
|
|
630
|
+
}
|
|
631
|
+
}, Rondevu.POLLING_INTERVAL_MS);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Automatically connect to a service (answerer side)
|
|
635
|
+
* Handles the entire connection flow: discovery, WebRTC setup, answer exchange, ICE candidates
|
|
542
636
|
*
|
|
543
637
|
* @example
|
|
544
638
|
* ```typescript
|
|
545
639
|
* // Connect to specific user
|
|
546
640
|
* const connection = await rondevu.connectToService({
|
|
547
641
|
* serviceFqn: 'chat:2.0.0@alice',
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
642
|
+
* onConnection: ({ dc, peerUsername }) => {
|
|
643
|
+
* console.log('Connected to', peerUsername)
|
|
644
|
+
* dc.addEventListener('message', (e) => console.log(e.data))
|
|
645
|
+
* dc.addEventListener('open', () => dc.send('Hello!'))
|
|
551
646
|
* }
|
|
552
647
|
* })
|
|
553
648
|
*
|
|
554
|
-
* connection.on('connected', () => {
|
|
555
|
-
* console.log('Connected!')
|
|
556
|
-
* connection.send('Hello!')
|
|
557
|
-
* })
|
|
558
|
-
*
|
|
559
|
-
* connection.on('message', (data) => {
|
|
560
|
-
* console.log('Received:', data)
|
|
561
|
-
* })
|
|
562
|
-
*
|
|
563
|
-
* connection.on('reconnecting', (attempt) => {
|
|
564
|
-
* console.log(`Reconnecting, attempt ${attempt}`)
|
|
565
|
-
* })
|
|
566
|
-
*
|
|
567
649
|
* // Discover random service
|
|
568
650
|
* const connection = await rondevu.connectToService({
|
|
569
|
-
* service: 'chat:2.0.0'
|
|
651
|
+
* service: 'chat:2.0.0',
|
|
652
|
+
* onConnection: ({ dc, peerUsername }) => {
|
|
653
|
+
* console.log('Connected to', peerUsername)
|
|
654
|
+
* }
|
|
570
655
|
* })
|
|
571
656
|
* ```
|
|
572
657
|
*/
|
|
573
658
|
async connectToService(options) {
|
|
574
|
-
const {
|
|
659
|
+
const { onConnection, rtcConfig } = options;
|
|
575
660
|
// Validate inputs
|
|
576
661
|
if (options.serviceFqn !== undefined && typeof options.serviceFqn === 'string' && !options.serviceFqn.trim()) {
|
|
577
662
|
throw new Error('serviceFqn cannot be empty');
|
|
@@ -585,28 +670,73 @@ export class Rondevu extends EventEmitter {
|
|
|
585
670
|
// Determine the full service FQN
|
|
586
671
|
const fqn = await this.resolveServiceFqn(options);
|
|
587
672
|
this.debug(`Connecting to service: ${fqn}`);
|
|
588
|
-
// Get service offer
|
|
673
|
+
// 1. Get service offer
|
|
589
674
|
const serviceData = await this.api.getService(fqn);
|
|
590
675
|
this.debug(`Found service from @${serviceData.username}`);
|
|
591
|
-
// Create
|
|
676
|
+
// 2. Create RTCPeerConnection
|
|
592
677
|
const rtcConfiguration = rtcConfig || {
|
|
593
678
|
iceServers: this.iceServers
|
|
594
679
|
};
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
680
|
+
const pc = new RTCPeerConnection(rtcConfiguration);
|
|
681
|
+
// 3. Set up data channel handler (answerer receives it from offerer)
|
|
682
|
+
let dc = null;
|
|
683
|
+
const dataChannelPromise = new Promise((resolve) => {
|
|
684
|
+
pc.ondatachannel = (event) => {
|
|
685
|
+
this.debug('Data channel received from offerer');
|
|
686
|
+
dc = event.channel;
|
|
687
|
+
this.emit('connection:opened', serviceData.offerId, dc);
|
|
688
|
+
resolve(dc);
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
// 4. Set up ICE candidate exchange
|
|
692
|
+
this.setupIceCandidateHandler(pc, serviceData.serviceFqn, serviceData.offerId);
|
|
693
|
+
// 5. Poll for remote ICE candidates
|
|
694
|
+
const icePollInterval = this.startIcePolling(pc, serviceData.serviceFqn, serviceData.offerId);
|
|
695
|
+
// 6. Set remote description
|
|
696
|
+
await pc.setRemoteDescription({
|
|
697
|
+
type: 'offer',
|
|
698
|
+
sdp: serviceData.sdp
|
|
699
|
+
});
|
|
700
|
+
// 7. Create and send answer
|
|
701
|
+
const answer = await pc.createAnswer();
|
|
702
|
+
await pc.setLocalDescription(answer);
|
|
703
|
+
await this.api.answerOffer(serviceData.serviceFqn, serviceData.offerId, answer.sdp);
|
|
704
|
+
// 8. Wait for data channel to be established
|
|
705
|
+
dc = await dataChannelPromise;
|
|
706
|
+
// Create connection context
|
|
707
|
+
const context = {
|
|
708
|
+
pc,
|
|
709
|
+
dc,
|
|
598
710
|
serviceFqn: serviceData.serviceFqn,
|
|
599
711
|
offerId: serviceData.offerId,
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
712
|
+
peerUsername: serviceData.username
|
|
713
|
+
};
|
|
714
|
+
// 9. Set up connection state monitoring
|
|
715
|
+
pc.onconnectionstatechange = () => {
|
|
716
|
+
this.debug(`Connection state: ${pc.connectionState}`);
|
|
717
|
+
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
|
718
|
+
clearInterval(icePollInterval);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
// 10. Wait for data channel to open and call onConnection
|
|
722
|
+
if (dc.readyState === 'open') {
|
|
723
|
+
this.debug('Data channel already open');
|
|
724
|
+
if (onConnection) {
|
|
725
|
+
await onConnection(context);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
await new Promise((resolve) => {
|
|
730
|
+
dc.addEventListener('open', async () => {
|
|
731
|
+
this.debug('Data channel opened');
|
|
732
|
+
if (onConnection) {
|
|
733
|
+
await onConnection(context);
|
|
734
|
+
}
|
|
735
|
+
resolve();
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return context;
|
|
610
740
|
}
|
|
611
741
|
// ============================================
|
|
612
742
|
// Service Discovery
|
|
@@ -614,6 +744,8 @@ export class Rondevu extends EventEmitter {
|
|
|
614
744
|
/**
|
|
615
745
|
* Find a service - unified discovery method
|
|
616
746
|
*
|
|
747
|
+
* Replaces getService(), discoverService(), and discoverServices() with a single method.
|
|
748
|
+
*
|
|
617
749
|
* @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
|
|
618
750
|
* @param options - Discovery options
|
|
619
751
|
*
|
|
@@ -702,34 +834,6 @@ export class Rondevu extends EventEmitter {
|
|
|
702
834
|
getPublicKey() {
|
|
703
835
|
return this.keypair.publicKey;
|
|
704
836
|
}
|
|
705
|
-
/**
|
|
706
|
-
* Get active connections (for offerer side)
|
|
707
|
-
*/
|
|
708
|
-
getActiveConnections() {
|
|
709
|
-
return this.activeConnections;
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Get all active offers (legacy compatibility)
|
|
713
|
-
* @deprecated Use getActiveConnections() instead
|
|
714
|
-
*/
|
|
715
|
-
getActiveOffers() {
|
|
716
|
-
const offers = [];
|
|
717
|
-
for (const [offerId, connection] of this.activeConnections.entries()) {
|
|
718
|
-
const pc = connection.getPeerConnection();
|
|
719
|
-
const dc = connection.getDataChannel();
|
|
720
|
-
if (pc) {
|
|
721
|
-
offers.push({
|
|
722
|
-
offerId,
|
|
723
|
-
serviceFqn: this.currentService ? `${this.currentService}@${this.username}` : '',
|
|
724
|
-
pc,
|
|
725
|
-
dc: dc || undefined,
|
|
726
|
-
answered: connection.getState() === 'connected',
|
|
727
|
-
createdAt: Date.now(),
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return offers;
|
|
732
|
-
}
|
|
733
837
|
/**
|
|
734
838
|
* Access to underlying API for advanced operations
|
|
735
839
|
* @deprecated Use direct methods on Rondevu instance instead
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtr-dev/rondevu-client",
|
|
3
|
-
"version": "0.18.
|
|
4
|
-
"description": "TypeScript client for Rondevu
|
|
3
|
+
"version": "0.18.10",
|
|
4
|
+
"description": "TypeScript client for Rondevu WebRTC signaling with username-based discovery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|