@xtr-dev/rondevu-client 0.20.1 → 0.21.3
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/README.md +83 -385
- package/dist/api/batcher.d.ts +60 -38
- package/dist/api/batcher.js +121 -77
- package/dist/api/client.d.ts +104 -61
- package/dist/api/client.js +273 -185
- package/dist/connections/answerer.d.ts +15 -6
- package/dist/connections/answerer.js +56 -19
- package/dist/connections/base.d.ts +6 -4
- package/dist/connections/base.js +26 -16
- package/dist/connections/config.d.ts +30 -0
- package/dist/connections/config.js +20 -0
- package/dist/connections/events.d.ts +6 -6
- package/dist/connections/offerer.d.ts +37 -8
- package/dist/connections/offerer.js +92 -24
- package/dist/core/ice-config.d.ts +35 -0
- package/dist/core/ice-config.js +111 -0
- package/dist/core/index.d.ts +18 -18
- package/dist/core/index.js +18 -13
- package/dist/core/offer-pool.d.ts +30 -11
- package/dist/core/offer-pool.js +90 -76
- package/dist/core/peer.d.ts +158 -0
- package/dist/core/peer.js +254 -0
- package/dist/core/polling-manager.d.ts +71 -0
- package/dist/core/polling-manager.js +122 -0
- package/dist/core/rondevu-errors.d.ts +59 -0
- package/dist/core/rondevu-errors.js +75 -0
- package/dist/core/rondevu-types.d.ts +125 -0
- package/dist/core/rondevu-types.js +6 -0
- package/dist/core/rondevu.d.ts +106 -209
- package/dist/core/rondevu.js +222 -349
- package/dist/crypto/adapter.d.ts +25 -9
- package/dist/crypto/node.d.ts +27 -5
- package/dist/crypto/node.js +96 -25
- package/dist/crypto/web.d.ts +26 -4
- package/dist/crypto/web.js +102 -25
- package/dist/utils/message-buffer.js +4 -4
- package/dist/webrtc/adapter.d.ts +22 -0
- package/dist/webrtc/adapter.js +5 -0
- package/dist/webrtc/browser.d.ts +12 -0
- package/dist/webrtc/browser.js +15 -0
- package/dist/webrtc/node.d.ts +32 -0
- package/dist/webrtc/node.js +32 -0
- package/package.json +17 -6
package/dist/core/rondevu.js
CHANGED
|
@@ -1,129 +1,44 @@
|
|
|
1
1
|
import { RondevuAPI } from '../api/client.js';
|
|
2
|
+
import { BrowserWebRTCAdapter } from '../webrtc/browser.js';
|
|
2
3
|
import { EventEmitter } from 'eventemitter3';
|
|
3
|
-
import { AnswererConnection } from '../connections/answerer.js';
|
|
4
4
|
import { OfferPool } from './offer-pool.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
urls: [
|
|
11
|
-
'turn:57.129.61.67:3478?transport=tcp',
|
|
12
|
-
'turn:57.129.61.67:3478?transport=udp',
|
|
13
|
-
],
|
|
14
|
-
username: 'webrtcuser',
|
|
15
|
-
credential: 'supersecretpassword'
|
|
16
|
-
}
|
|
17
|
-
],
|
|
18
|
-
'hostname-turns': [
|
|
19
|
-
{ urls: 'stun:turn.share.fish:3478' },
|
|
20
|
-
{
|
|
21
|
-
urls: [
|
|
22
|
-
'turns:turn.share.fish:5349?transport=tcp',
|
|
23
|
-
'turns:turn.share.fish:5349?transport=udp',
|
|
24
|
-
'turn:turn.share.fish:3478?transport=tcp',
|
|
25
|
-
'turn:turn.share.fish:3478?transport=udp',
|
|
26
|
-
],
|
|
27
|
-
username: 'webrtcuser',
|
|
28
|
-
credential: 'supersecretpassword'
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
'google-stun': [
|
|
32
|
-
{ urls: 'stun:stun.l.google.com:19302' },
|
|
33
|
-
{ urls: 'stun:stun1.l.google.com:19302' }
|
|
34
|
-
],
|
|
35
|
-
'relay-only': [
|
|
36
|
-
{ urls: 'stun:57.129.61.67:3478' },
|
|
37
|
-
{
|
|
38
|
-
urls: [
|
|
39
|
-
'turn:57.129.61.67:3478?transport=tcp',
|
|
40
|
-
'turn:57.129.61.67:3478?transport=udp',
|
|
41
|
-
],
|
|
42
|
-
username: 'webrtcuser',
|
|
43
|
-
credential: 'supersecretpassword',
|
|
44
|
-
// @ts-expect-error - iceTransportPolicy is valid but not in RTCIceServer type
|
|
45
|
-
iceTransportPolicy: 'relay'
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Base error class for Rondevu errors
|
|
51
|
-
*/
|
|
52
|
-
export class RondevuError extends Error {
|
|
53
|
-
constructor(message, context) {
|
|
54
|
-
super(message);
|
|
55
|
-
this.context = context;
|
|
56
|
-
this.name = 'RondevuError';
|
|
57
|
-
Object.setPrototypeOf(this, RondevuError.prototype);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Network-related errors (API calls, connectivity)
|
|
62
|
-
*/
|
|
63
|
-
export class NetworkError extends RondevuError {
|
|
64
|
-
constructor(message, context) {
|
|
65
|
-
super(message, context);
|
|
66
|
-
this.name = 'NetworkError';
|
|
67
|
-
Object.setPrototypeOf(this, NetworkError.prototype);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Validation errors (invalid input, malformed data)
|
|
72
|
-
*/
|
|
73
|
-
export class ValidationError extends RondevuError {
|
|
74
|
-
constructor(message, context) {
|
|
75
|
-
super(message, context);
|
|
76
|
-
this.name = 'ValidationError';
|
|
77
|
-
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* WebRTC connection errors (peer connection failures, ICE issues)
|
|
82
|
-
*/
|
|
83
|
-
export class ConnectionError extends RondevuError {
|
|
84
|
-
constructor(message, context) {
|
|
85
|
-
super(message, context);
|
|
86
|
-
this.name = 'ConnectionError';
|
|
87
|
-
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
5
|
+
import { Peer } from './peer.js';
|
|
6
|
+
import { getIceConfiguration } from './ice-config.js';
|
|
7
|
+
import { PollingManager } from './polling-manager.js';
|
|
8
|
+
// Re-export ICE config for backward compatibility
|
|
9
|
+
export { ICE_SERVER_PRESETS } from './ice-config.js';
|
|
90
10
|
/**
|
|
91
11
|
* Rondevu - Complete WebRTC signaling client with durable connections
|
|
92
12
|
*
|
|
93
|
-
*
|
|
94
|
-
* - connectToService() now returns AnswererConnection instead of ConnectionContext
|
|
95
|
-
* - Automatic reconnection and message buffering built-in
|
|
96
|
-
* - Connection objects expose .send() method instead of raw DataChannel
|
|
97
|
-
* - Rich event system for connection lifecycle (connected, disconnected, reconnecting, etc.)
|
|
13
|
+
* Uses a tags-based discovery system where offers have 1+ tags for matching.
|
|
98
14
|
*
|
|
99
15
|
* @example
|
|
100
16
|
* ```typescript
|
|
101
17
|
* // Create and initialize Rondevu instance with preset ICE servers
|
|
102
18
|
* const rondevu = await Rondevu.connect({
|
|
103
19
|
* apiUrl: 'https://signal.example.com',
|
|
104
|
-
* username: 'alice',
|
|
105
20
|
* iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
|
|
106
21
|
* })
|
|
107
22
|
*
|
|
108
|
-
* //
|
|
109
|
-
* await rondevu.
|
|
110
|
-
*
|
|
23
|
+
* // Create offers with tags for discovery
|
|
24
|
+
* await rondevu.offer({
|
|
25
|
+
* tags: ['chat', 'video'],
|
|
111
26
|
* maxOffers: 5 // Maintain up to 5 concurrent offers
|
|
112
27
|
* })
|
|
113
28
|
*
|
|
114
29
|
* // Start accepting connections (auto-fills offers and polls)
|
|
115
30
|
* await rondevu.startFilling()
|
|
116
31
|
*
|
|
117
|
-
* // Listen for connections
|
|
32
|
+
* // Listen for connections
|
|
118
33
|
* rondevu.on('connection:opened', (offerId, connection) => {
|
|
119
34
|
* connection.on('connected', () => console.log('Connected!'))
|
|
120
35
|
* connection.on('message', (data) => console.log('Received:', data))
|
|
121
36
|
* connection.send('Hello!')
|
|
122
37
|
* })
|
|
123
38
|
*
|
|
124
|
-
* // Connect
|
|
125
|
-
* const connection = await rondevu.
|
|
126
|
-
*
|
|
39
|
+
* // Connect by discovering offers with matching tags
|
|
40
|
+
* const connection = await rondevu.connect({
|
|
41
|
+
* tags: ['chat']
|
|
127
42
|
* })
|
|
128
43
|
*
|
|
129
44
|
* connection.on('connected', () => {
|
|
@@ -141,27 +56,35 @@ export class ConnectionError extends RondevuError {
|
|
|
141
56
|
* ```
|
|
142
57
|
*/
|
|
143
58
|
export class Rondevu extends EventEmitter {
|
|
144
|
-
constructor(apiUrl,
|
|
59
|
+
constructor(apiUrl, credential, api, iceServers, iceTransportPolicy, webrtcAdapter, cryptoAdapter, debugEnabled = false) {
|
|
145
60
|
super();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.currentService = null;
|
|
61
|
+
// Publishing state
|
|
62
|
+
this.currentTags = null;
|
|
149
63
|
this.offerPool = null;
|
|
150
64
|
this.apiUrl = apiUrl;
|
|
151
|
-
this.
|
|
152
|
-
this.keypair = keypair;
|
|
65
|
+
this.credential = credential;
|
|
153
66
|
this.api = api;
|
|
154
67
|
this.iceServers = iceServers;
|
|
68
|
+
this.iceTransportPolicy = iceTransportPolicy;
|
|
69
|
+
this.webrtcAdapter = webrtcAdapter;
|
|
155
70
|
this.cryptoAdapter = cryptoAdapter;
|
|
156
|
-
this.batchingOptions = batchingOptions;
|
|
157
71
|
this.debugEnabled = debugEnabled;
|
|
158
|
-
|
|
159
|
-
this.
|
|
72
|
+
// Initialize centralized polling manager
|
|
73
|
+
this.pollingManager = new PollingManager({
|
|
74
|
+
api: this.api,
|
|
75
|
+
debugEnabled: this.debugEnabled,
|
|
76
|
+
});
|
|
77
|
+
// Forward polling events to Rondevu instance
|
|
78
|
+
this.pollingManager.on('poll:answer', data => {
|
|
79
|
+
this.emit('poll:answer', data);
|
|
80
|
+
});
|
|
81
|
+
this.pollingManager.on('poll:ice', data => {
|
|
82
|
+
this.emit('poll:ice', data);
|
|
83
|
+
});
|
|
160
84
|
this.debug('Instance created:', {
|
|
161
|
-
|
|
162
|
-
publicKey: this.keypair.publicKey,
|
|
85
|
+
name: this.credential.name,
|
|
163
86
|
hasIceServers: iceServers.length > 0,
|
|
164
|
-
|
|
87
|
+
iceTransportPolicy: iceTransportPolicy || 'all',
|
|
165
88
|
});
|
|
166
89
|
}
|
|
167
90
|
/**
|
|
@@ -177,84 +100,76 @@ export class Rondevu extends EventEmitter {
|
|
|
177
100
|
*
|
|
178
101
|
* @example
|
|
179
102
|
* ```typescript
|
|
103
|
+
* const rondevu = await Rondevu.connect({}) // Uses default API URL
|
|
104
|
+
* // or
|
|
180
105
|
* const rondevu = await Rondevu.connect({
|
|
181
|
-
* apiUrl: 'https://api.
|
|
182
|
-
* username: 'alice'
|
|
106
|
+
* apiUrl: 'https://custom.api.com'
|
|
183
107
|
* })
|
|
184
108
|
* ```
|
|
185
109
|
*/
|
|
186
|
-
static async connect(options) {
|
|
187
|
-
const
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (options.rtcIceCandidate) {
|
|
193
|
-
globalThis.RTCIceCandidate = options.rtcIceCandidate;
|
|
194
|
-
}
|
|
195
|
-
// Handle preset string or custom array
|
|
196
|
-
let iceServers;
|
|
197
|
-
if (typeof options.iceServers === 'string') {
|
|
198
|
-
iceServers = ICE_SERVER_PRESETS[options.iceServers];
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
iceServers = options.iceServers || [
|
|
202
|
-
{ urls: 'stun:stun.l.google.com:19302' }
|
|
203
|
-
];
|
|
204
|
-
}
|
|
110
|
+
static async connect(options = {}) {
|
|
111
|
+
const apiUrl = options.apiUrl || Rondevu.DEFAULT_API_URL;
|
|
112
|
+
// Use provided WebRTC adapter or default to browser adapter
|
|
113
|
+
const webrtcAdapter = options.webrtcAdapter || new BrowserWebRTCAdapter();
|
|
114
|
+
// Handle preset string or custom array, extracting iceTransportPolicy if present
|
|
115
|
+
const iceConfig = getIceConfiguration(options.iceServers);
|
|
205
116
|
if (options.debug) {
|
|
206
117
|
console.log('[Rondevu] Connecting:', {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
iceServers: iceServers
|
|
210
|
-
|
|
118
|
+
apiUrl,
|
|
119
|
+
hasCredential: !!options.credential,
|
|
120
|
+
iceServers: iceConfig.iceServers?.length ?? 0,
|
|
121
|
+
iceTransportPolicy: iceConfig.iceTransportPolicy || 'all',
|
|
211
122
|
});
|
|
212
123
|
}
|
|
213
|
-
// Generate
|
|
214
|
-
let
|
|
215
|
-
if (!
|
|
124
|
+
// Generate credential if not provided
|
|
125
|
+
let credential = options.credential;
|
|
126
|
+
if (!credential) {
|
|
216
127
|
if (options.debug)
|
|
217
|
-
console.log('[Rondevu] Generating new
|
|
218
|
-
|
|
128
|
+
console.log('[Rondevu] Generating new credentials...');
|
|
129
|
+
credential = await RondevuAPI.generateCredentials(apiUrl, {
|
|
130
|
+
name: options.username, // Will claim this username if provided
|
|
131
|
+
});
|
|
219
132
|
if (options.debug)
|
|
220
|
-
console.log('[Rondevu] Generated
|
|
133
|
+
console.log('[Rondevu] Generated credentials, name:', credential.name);
|
|
221
134
|
}
|
|
222
135
|
else {
|
|
223
136
|
if (options.debug)
|
|
224
|
-
console.log('[Rondevu] Using existing
|
|
137
|
+
console.log('[Rondevu] Using existing credential, name:', credential.name);
|
|
225
138
|
}
|
|
226
139
|
// Create API instance
|
|
227
|
-
const api = new RondevuAPI(
|
|
140
|
+
const api = new RondevuAPI(apiUrl, credential, options.cryptoAdapter);
|
|
228
141
|
if (options.debug)
|
|
229
142
|
console.log('[Rondevu] Created API instance');
|
|
230
|
-
return new Rondevu(
|
|
143
|
+
return new Rondevu(apiUrl, credential, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, options.cryptoAdapter, options.debug || false);
|
|
231
144
|
}
|
|
145
|
+
// ============================================
|
|
146
|
+
// Credential Access
|
|
147
|
+
// ============================================
|
|
232
148
|
/**
|
|
233
|
-
*
|
|
149
|
+
* Get the current credential name
|
|
234
150
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const random = Array.from(crypto.getRandomValues(new Uint8Array(3)))
|
|
238
|
-
.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
239
|
-
return `anon-${timestamp}-${random}`;
|
|
151
|
+
getName() {
|
|
152
|
+
return this.credential.name;
|
|
240
153
|
}
|
|
241
|
-
// ============================================
|
|
242
|
-
// Username Management
|
|
243
|
-
// ============================================
|
|
244
154
|
/**
|
|
245
|
-
*
|
|
155
|
+
* Get the full credential (name + secret)
|
|
156
|
+
* Use this to persist credentials for future sessions
|
|
157
|
+
*
|
|
158
|
+
* ⚠️ SECURITY WARNING:
|
|
159
|
+
* - The secret grants full access to this identity
|
|
160
|
+
* - Store credentials securely (encrypted storage, never in logs)
|
|
161
|
+
* - Never expose credentials in URLs, console output, or error messages
|
|
162
|
+
* - Treat the secret like a password or API key
|
|
246
163
|
*/
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
164
|
+
getCredential() {
|
|
165
|
+
return { ...this.credential };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get the WebRTC adapter for creating peer connections
|
|
169
|
+
* Used internally by offer pool and connections
|
|
170
|
+
*/
|
|
171
|
+
getWebRTCAdapter() {
|
|
172
|
+
return this.webrtcAdapter;
|
|
258
173
|
}
|
|
259
174
|
// ============================================
|
|
260
175
|
// Service Publishing
|
|
@@ -270,37 +185,39 @@ export class Rondevu extends EventEmitter {
|
|
|
270
185
|
return { dc, offer };
|
|
271
186
|
}
|
|
272
187
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
188
|
+
* Create offers with tags for discovery (offerer/host side)
|
|
189
|
+
* Auto-starts filling by default. Use the returned object to cancel.
|
|
275
190
|
*
|
|
276
191
|
* @example
|
|
277
192
|
* ```typescript
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* reconnectEnabled: true,
|
|
283
|
-
* bufferEnabled: true
|
|
284
|
-
* }
|
|
193
|
+
* // Auto-start (default)
|
|
194
|
+
* const offer = await rondevu.offer({
|
|
195
|
+
* tags: ['chat', 'video'],
|
|
196
|
+
* maxOffers: 5
|
|
285
197
|
* })
|
|
198
|
+
* // Later: offer.cancel() to stop
|
|
199
|
+
*
|
|
200
|
+
* // Manual start
|
|
201
|
+
* await rondevu.offer({ tags: ['chat'], maxOffers: 5, autoStart: false })
|
|
286
202
|
* await rondevu.startFilling()
|
|
287
203
|
* ```
|
|
288
204
|
*/
|
|
289
|
-
async
|
|
290
|
-
const {
|
|
291
|
-
this.
|
|
205
|
+
async offer(options) {
|
|
206
|
+
const { tags, maxOffers, offerFactory, ttl, connectionConfig, autoStart = true } = options;
|
|
207
|
+
this.currentTags = tags;
|
|
292
208
|
this.connectionConfig = connectionConfig;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
this.debug(`Publishing service: ${service} with maxOffers: ${maxOffers}`);
|
|
296
|
-
// Create OfferPool (but don't start it yet - call startFilling() to begin)
|
|
209
|
+
this.debug(`Creating offers with tags: ${tags.join(', ')} with maxOffers: ${maxOffers}`);
|
|
210
|
+
// Create OfferPool
|
|
297
211
|
this.offerPool = new OfferPool({
|
|
298
212
|
api: this.api,
|
|
299
|
-
|
|
213
|
+
tags,
|
|
214
|
+
ownerUsername: this.credential.name,
|
|
300
215
|
maxOffers,
|
|
301
216
|
offerFactory: offerFactory || this.defaultOfferFactory.bind(this),
|
|
302
217
|
ttl: ttl || Rondevu.DEFAULT_TTL_MS,
|
|
303
218
|
iceServers: this.iceServers,
|
|
219
|
+
iceTransportPolicy: this.iceTransportPolicy,
|
|
220
|
+
webrtcAdapter: this.webrtcAdapter,
|
|
304
221
|
connectionConfig,
|
|
305
222
|
debugEnabled: this.debugEnabled,
|
|
306
223
|
});
|
|
@@ -308,23 +225,39 @@ export class Rondevu extends EventEmitter {
|
|
|
308
225
|
this.offerPool.on('connection:opened', (offerId, connection) => {
|
|
309
226
|
this.emit('connection:opened', offerId, connection);
|
|
310
227
|
});
|
|
311
|
-
this.offerPool.on('offer:created', (offerId,
|
|
312
|
-
this.emit('offer:created', offerId,
|
|
228
|
+
this.offerPool.on('offer:created', (offerId, tags) => {
|
|
229
|
+
this.emit('offer:created', offerId, tags);
|
|
313
230
|
});
|
|
314
231
|
this.offerPool.on('connection:rotated', (oldOfferId, newOfferId, connection) => {
|
|
315
232
|
this.emit('connection:rotated', oldOfferId, newOfferId, connection);
|
|
316
233
|
});
|
|
317
|
-
|
|
234
|
+
// Subscribe to polling events and forward to OfferPool
|
|
235
|
+
this.on('poll:answer', data => {
|
|
236
|
+
this.offerPool?.handlePollAnswer(data);
|
|
237
|
+
});
|
|
238
|
+
this.on('poll:ice', data => {
|
|
239
|
+
this.offerPool?.handlePollIce(data);
|
|
240
|
+
});
|
|
241
|
+
// Auto-start if enabled (default)
|
|
242
|
+
if (autoStart) {
|
|
243
|
+
await this.startFilling();
|
|
244
|
+
}
|
|
245
|
+
// Return handle for cancellation
|
|
246
|
+
return {
|
|
247
|
+
cancel: () => this.stopFilling(),
|
|
248
|
+
};
|
|
318
249
|
}
|
|
319
250
|
/**
|
|
320
251
|
* Start filling offers and polling for answers/ICE
|
|
321
|
-
* Call this after
|
|
252
|
+
* Call this after offer() to begin accepting connections
|
|
322
253
|
*/
|
|
323
254
|
async startFilling() {
|
|
324
255
|
if (!this.offerPool) {
|
|
325
|
-
throw new Error('No
|
|
256
|
+
throw new Error('No offers created. Call offer() first.');
|
|
326
257
|
}
|
|
327
258
|
this.debug('Starting offer filling and polling');
|
|
259
|
+
// Start the centralized polling manager
|
|
260
|
+
this.pollingManager.start();
|
|
328
261
|
await this.offerPool.start();
|
|
329
262
|
}
|
|
330
263
|
/**
|
|
@@ -333,8 +266,31 @@ export class Rondevu extends EventEmitter {
|
|
|
333
266
|
*/
|
|
334
267
|
stopFilling() {
|
|
335
268
|
this.debug('Stopping offer filling and polling');
|
|
269
|
+
// Stop the centralized polling manager
|
|
270
|
+
this.pollingManager.stop();
|
|
336
271
|
this.offerPool?.stop();
|
|
337
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Start the centralized polling manager
|
|
275
|
+
* Use this when you need polling without offers (e.g., answerer connections)
|
|
276
|
+
*/
|
|
277
|
+
startPolling() {
|
|
278
|
+
this.debug('Starting polling manager');
|
|
279
|
+
this.pollingManager.start();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Stop the centralized polling manager
|
|
283
|
+
*/
|
|
284
|
+
stopPolling() {
|
|
285
|
+
this.debug('Stopping polling manager');
|
|
286
|
+
this.pollingManager.stop();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Check if polling is active
|
|
290
|
+
*/
|
|
291
|
+
isPolling() {
|
|
292
|
+
return this.pollingManager.isRunning();
|
|
293
|
+
}
|
|
338
294
|
/**
|
|
339
295
|
* Get the count of active offers
|
|
340
296
|
* @returns Number of active offers
|
|
@@ -359,148 +315,112 @@ export class Rondevu extends EventEmitter {
|
|
|
359
315
|
this.offerPool?.disconnectAll();
|
|
360
316
|
}
|
|
361
317
|
/**
|
|
362
|
-
* Get the current
|
|
363
|
-
* @returns Object with
|
|
318
|
+
* Get the current publishing status
|
|
319
|
+
* @returns Object with publishing state information
|
|
364
320
|
*/
|
|
365
|
-
|
|
321
|
+
getPublishStatus() {
|
|
366
322
|
return {
|
|
367
|
-
active: this.
|
|
368
|
-
offerCount: this.offerPool?.getOfferCount() ?? 0
|
|
323
|
+
active: this.currentTags !== null,
|
|
324
|
+
offerCount: this.offerPool?.getOfferCount() ?? 0,
|
|
325
|
+
tags: this.currentTags,
|
|
369
326
|
};
|
|
370
327
|
}
|
|
371
328
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*/
|
|
375
|
-
async resolveServiceFqn(options) {
|
|
376
|
-
const { serviceFqn, service, username } = options;
|
|
377
|
-
if (serviceFqn) {
|
|
378
|
-
return serviceFqn;
|
|
379
|
-
}
|
|
380
|
-
else if (service && username) {
|
|
381
|
-
return `${service}@${username}`;
|
|
382
|
-
}
|
|
383
|
-
else if (service) {
|
|
384
|
-
// Discovery mode - get random service
|
|
385
|
-
this.debug(`Discovering service: ${service}`);
|
|
386
|
-
const discovered = await this.findService(service);
|
|
387
|
-
return discovered.serviceFqn;
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
throw new Error('Either serviceFqn or service must be provided');
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Connect to a service (answerer side) - v1.0.0 API
|
|
395
|
-
* Returns an AnswererConnection with automatic reconnection and buffering
|
|
396
|
-
*
|
|
397
|
-
* BREAKING CHANGE: This now returns AnswererConnection instead of ConnectionContext
|
|
329
|
+
* Create a peer connection with simplified DX
|
|
330
|
+
* Returns a Peer object with clean state management and events
|
|
398
331
|
*
|
|
399
332
|
* @example
|
|
400
333
|
* ```typescript
|
|
334
|
+
* // Connect to any peer matching tags
|
|
335
|
+
* const peer = await rondevu.peer({ tags: ['chat'] })
|
|
336
|
+
*
|
|
401
337
|
* // Connect to specific user
|
|
402
|
-
* const
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* reconnectEnabled: true,
|
|
406
|
-
* bufferEnabled: true
|
|
407
|
-
* }
|
|
338
|
+
* const peer = await rondevu.peer({
|
|
339
|
+
* username: 'alice',
|
|
340
|
+
* tags: ['chat']
|
|
408
341
|
* })
|
|
409
342
|
*
|
|
410
|
-
*
|
|
411
|
-
* console.log('Connected
|
|
412
|
-
*
|
|
343
|
+
* peer.on('open', () => {
|
|
344
|
+
* console.log('Connected to', peer.peerUsername)
|
|
345
|
+
* peer.send('Hello!')
|
|
413
346
|
* })
|
|
414
347
|
*
|
|
415
|
-
*
|
|
348
|
+
* peer.on('message', (data) => {
|
|
416
349
|
* console.log('Received:', data)
|
|
417
350
|
* })
|
|
418
351
|
*
|
|
419
|
-
*
|
|
420
|
-
* console.log(`
|
|
352
|
+
* peer.on('state', (state, prevState) => {
|
|
353
|
+
* console.log(`State: ${prevState} → ${state}`)
|
|
421
354
|
* })
|
|
422
355
|
*
|
|
423
|
-
* //
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
* }
|
|
356
|
+
* // Access underlying RTCPeerConnection
|
|
357
|
+
* if (peer.peerConnection) {
|
|
358
|
+
* console.log('ICE state:', peer.peerConnection.iceConnectionState)
|
|
359
|
+
* }
|
|
427
360
|
* ```
|
|
428
361
|
*/
|
|
429
|
-
async
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
if (options.serviceFqn !== undefined && typeof options.serviceFqn === 'string' && !options.serviceFqn.trim()) {
|
|
433
|
-
throw new Error('serviceFqn cannot be empty');
|
|
434
|
-
}
|
|
435
|
-
if (options.service !== undefined && typeof options.service === 'string' && !options.service.trim()) {
|
|
436
|
-
throw new Error('service cannot be empty');
|
|
437
|
-
}
|
|
438
|
-
if (options.username !== undefined && typeof options.username === 'string' && !options.username.trim()) {
|
|
439
|
-
throw new Error('username cannot be empty');
|
|
440
|
-
}
|
|
441
|
-
// Determine the full service FQN
|
|
442
|
-
const fqn = await this.resolveServiceFqn(options);
|
|
443
|
-
this.debug(`Connecting to service: ${fqn}`);
|
|
444
|
-
// Get service offer
|
|
445
|
-
const serviceData = await this.api.getService(fqn);
|
|
446
|
-
this.debug(`Found service from @${serviceData.username}`);
|
|
447
|
-
// Create RTCConfiguration
|
|
448
|
-
const rtcConfiguration = rtcConfig || {
|
|
449
|
-
iceServers: this.iceServers
|
|
450
|
-
};
|
|
451
|
-
// Create AnswererConnection
|
|
452
|
-
const connection = new AnswererConnection({
|
|
362
|
+
async peer(options) {
|
|
363
|
+
const peer = new Peer({
|
|
364
|
+
...options,
|
|
453
365
|
api: this.api,
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
config: {
|
|
459
|
-
...connectionConfig,
|
|
460
|
-
debug: this.debugEnabled,
|
|
461
|
-
},
|
|
366
|
+
iceServers: this.iceServers,
|
|
367
|
+
iceTransportPolicy: this.iceTransportPolicy,
|
|
368
|
+
webrtcAdapter: this.webrtcAdapter,
|
|
369
|
+
debug: this.debugEnabled,
|
|
462
370
|
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
371
|
+
await peer.initialize();
|
|
372
|
+
// Subscribe to poll:ice events for this peer's connection
|
|
373
|
+
const peerOfferId = peer.offerId;
|
|
374
|
+
const peerConnection = peer.getConnection();
|
|
375
|
+
if (peerConnection) {
|
|
376
|
+
const pollIceHandler = (data) => {
|
|
377
|
+
if (data.offerId === peerOfferId) {
|
|
378
|
+
peerConnection.handleRemoteIceCandidates(data.candidates);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
this.on('poll:ice', pollIceHandler);
|
|
382
|
+
// Clean up handler when connection closes
|
|
383
|
+
peerConnection.on('closed', () => {
|
|
384
|
+
this.off('poll:ice', pollIceHandler);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
// Start polling if not already running
|
|
388
|
+
if (!this.pollingManager.isRunning()) {
|
|
389
|
+
this.debug('Starting polling for peer connection');
|
|
390
|
+
this.pollingManager.start();
|
|
391
|
+
}
|
|
392
|
+
return peer;
|
|
466
393
|
}
|
|
467
394
|
// ============================================
|
|
468
|
-
//
|
|
395
|
+
// Discovery
|
|
469
396
|
// ============================================
|
|
470
397
|
/**
|
|
471
|
-
*
|
|
398
|
+
* Discover offers by tags
|
|
472
399
|
*
|
|
473
|
-
* @param
|
|
474
|
-
* @param options - Discovery options
|
|
400
|
+
* @param tags - Tags to search for (OR logic - matches any tag)
|
|
401
|
+
* @param options - Discovery options (pagination)
|
|
475
402
|
*
|
|
476
403
|
* @example
|
|
477
404
|
* ```typescript
|
|
478
|
-
* //
|
|
479
|
-
* const
|
|
480
|
-
*
|
|
481
|
-
* // Random discovery (no username)
|
|
482
|
-
* const service = await rondevu.findService('chat:1.0.0')
|
|
405
|
+
* // Discover offers matching any of the tags
|
|
406
|
+
* const result = await rondevu.discover(['chat', 'video'])
|
|
483
407
|
*
|
|
484
408
|
* // Paginated discovery
|
|
485
|
-
* const result = await rondevu.
|
|
486
|
-
* mode: 'paginated',
|
|
409
|
+
* const result = await rondevu.discover(['chat'], {
|
|
487
410
|
* limit: 20,
|
|
488
411
|
* offset: 0
|
|
489
412
|
* })
|
|
413
|
+
*
|
|
414
|
+
* // Access offers
|
|
415
|
+
* for (const offer of result.offers) {
|
|
416
|
+
* console.log(offer.username, offer.tags)
|
|
417
|
+
* }
|
|
490
418
|
* ```
|
|
491
419
|
*/
|
|
492
|
-
async
|
|
493
|
-
const {
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
const effectiveMode = mode || (hasUsername ? 'direct' : 'random');
|
|
497
|
-
if (effectiveMode === 'paginated') {
|
|
498
|
-
return await this.api.getService(serviceFqn, { limit, offset });
|
|
499
|
-
}
|
|
500
|
-
else {
|
|
501
|
-
// Both 'direct' and 'random' use the same API call
|
|
502
|
-
return await this.api.getService(serviceFqn);
|
|
503
|
-
}
|
|
420
|
+
async discover(tags, options) {
|
|
421
|
+
const { limit = 10, offset = 0 } = options || {};
|
|
422
|
+
// Always pass limit to ensure we get DiscoverResponse (paginated mode)
|
|
423
|
+
return (await this.api.discover({ tags, limit, offset }));
|
|
504
424
|
}
|
|
505
425
|
// ============================================
|
|
506
426
|
// WebRTC Signaling
|
|
@@ -508,15 +428,15 @@ export class Rondevu extends EventEmitter {
|
|
|
508
428
|
/**
|
|
509
429
|
* Post answer SDP to specific offer
|
|
510
430
|
*/
|
|
511
|
-
async postOfferAnswer(
|
|
512
|
-
await this.api.answerOffer(
|
|
431
|
+
async postOfferAnswer(offerId, sdp) {
|
|
432
|
+
await this.api.answerOffer(offerId, sdp);
|
|
513
433
|
return { success: true, offerId };
|
|
514
434
|
}
|
|
515
435
|
/**
|
|
516
436
|
* Get answer SDP (offerer polls this)
|
|
517
437
|
*/
|
|
518
|
-
async getOfferAnswer(
|
|
519
|
-
return await this.api.getOfferAnswer(
|
|
438
|
+
async getOfferAnswer(offerId) {
|
|
439
|
+
return await this.api.getOfferAnswer(offerId);
|
|
520
440
|
}
|
|
521
441
|
/**
|
|
522
442
|
* Combined polling for answers and ICE candidates
|
|
@@ -528,73 +448,26 @@ export class Rondevu extends EventEmitter {
|
|
|
528
448
|
/**
|
|
529
449
|
* Add ICE candidates to specific offer
|
|
530
450
|
*/
|
|
531
|
-
async addOfferIceCandidates(
|
|
532
|
-
return await this.api.addOfferIceCandidates(
|
|
451
|
+
async addOfferIceCandidates(offerId, candidates) {
|
|
452
|
+
return await this.api.addOfferIceCandidates(offerId, candidates);
|
|
533
453
|
}
|
|
534
454
|
/**
|
|
535
455
|
* Get ICE candidates for specific offer (with polling support)
|
|
536
456
|
*/
|
|
537
|
-
async getOfferIceCandidates(
|
|
538
|
-
return await this.api.getOfferIceCandidates(
|
|
457
|
+
async getOfferIceCandidates(offerId, since = 0) {
|
|
458
|
+
return await this.api.getOfferIceCandidates(offerId, since);
|
|
539
459
|
}
|
|
540
460
|
// ============================================
|
|
541
461
|
// Utility Methods
|
|
542
462
|
// ============================================
|
|
543
|
-
/**
|
|
544
|
-
* Get the current keypair (for backup/storage)
|
|
545
|
-
*/
|
|
546
|
-
getKeypair() {
|
|
547
|
-
return this.keypair;
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Get the username
|
|
551
|
-
*/
|
|
552
|
-
getUsername() {
|
|
553
|
-
return this.username;
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Get the public key
|
|
557
|
-
*/
|
|
558
|
-
getPublicKey() {
|
|
559
|
-
return this.keypair.publicKey;
|
|
560
|
-
}
|
|
561
463
|
/**
|
|
562
464
|
* Get active connections (for offerer side)
|
|
563
465
|
*/
|
|
564
466
|
getActiveConnections() {
|
|
565
467
|
return this.offerPool?.getActiveConnections() ?? new Map();
|
|
566
468
|
}
|
|
567
|
-
/**
|
|
568
|
-
* Get all active offers (legacy compatibility)
|
|
569
|
-
* @deprecated Use getActiveConnections() instead
|
|
570
|
-
*/
|
|
571
|
-
getActiveOffers() {
|
|
572
|
-
const offers = [];
|
|
573
|
-
const connections = this.offerPool?.getActiveConnections() ?? new Map();
|
|
574
|
-
for (const [offerId, connection] of connections.entries()) {
|
|
575
|
-
const pc = connection.getPeerConnection();
|
|
576
|
-
const dc = connection.getDataChannel();
|
|
577
|
-
if (pc) {
|
|
578
|
-
offers.push({
|
|
579
|
-
offerId,
|
|
580
|
-
serviceFqn: this.currentService ? `${this.currentService}@${this.username}` : '',
|
|
581
|
-
pc,
|
|
582
|
-
dc: dc || undefined,
|
|
583
|
-
answered: connection.getState() === 'connected',
|
|
584
|
-
createdAt: Date.now(),
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return offers;
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Access to underlying API for advanced operations
|
|
592
|
-
* @deprecated Use direct methods on Rondevu instance instead
|
|
593
|
-
*/
|
|
594
|
-
getAPIPublic() {
|
|
595
|
-
return this.api;
|
|
596
|
-
}
|
|
597
469
|
}
|
|
598
470
|
// Constants
|
|
471
|
+
Rondevu.DEFAULT_API_URL = 'https://api.ronde.vu';
|
|
599
472
|
Rondevu.DEFAULT_TTL_MS = 300000; // 5 minutes
|
|
600
473
|
Rondevu.POLLING_INTERVAL_MS = 1000; // 1 second
|