@xtr-dev/rondevu-client 0.17.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/api.d.ts +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/node-crypto-adapter.js +0 -2
- package/dist/rondevu.d.ts +125 -48
- package/dist/rondevu.js +219 -37
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -49,8 +49,8 @@ const rondevu = await Rondevu.connect({
|
|
|
49
49
|
await rondevu.publishService({
|
|
50
50
|
service: 'chat:1.0.0',
|
|
51
51
|
maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
52
|
-
offerFactory: async (
|
|
53
|
-
|
|
52
|
+
offerFactory: async (pc) => {
|
|
53
|
+
// pc is created by Rondevu with ICE handlers already attached
|
|
54
54
|
const dc = pc.createDataChannel('chat')
|
|
55
55
|
|
|
56
56
|
dc.addEventListener('open', () => {
|
|
@@ -64,7 +64,7 @@ await rondevu.publishService({
|
|
|
64
64
|
|
|
65
65
|
const offer = await pc.createOffer()
|
|
66
66
|
await pc.setLocalDescription(offer)
|
|
67
|
-
return {
|
|
67
|
+
return { dc, offer }
|
|
68
68
|
}
|
|
69
69
|
})
|
|
70
70
|
|
package/dist/api.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* @xtr-dev/rondevu-client
|
|
3
3
|
* WebRTC peer signaling client
|
|
4
4
|
*/
|
|
5
|
-
export { Rondevu } from './rondevu.js';
|
|
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
8
|
export { WebCryptoAdapter } from './web-crypto-adapter.js';
|
|
9
9
|
export { NodeCryptoAdapter } from './node-crypto-adapter.js';
|
|
10
10
|
export type { Signaler, Binnable, } from './types.js';
|
|
11
11
|
export type { Keypair, OfferRequest, ServiceRequest, Service, ServiceOffer, IceCandidate, } from './api.js';
|
|
12
|
-
export type { RondevuOptions, PublishServiceOptions, ConnectToServiceOptions, ConnectionContext, OfferContext, OfferFactory } from './rondevu.js';
|
|
12
|
+
export type { RondevuOptions, PublishServiceOptions, ConnectToServiceOptions, ConnectionContext, OfferContext, OfferFactory, ActiveOffer, FindServiceOptions, ServiceResult, PaginatedServiceResult } from './rondevu.js';
|
|
13
13
|
export type { CryptoAdapter } from './crypto-adapter.js';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @xtr-dev/rondevu-client
|
|
3
3
|
* WebRTC peer signaling client
|
|
4
4
|
*/
|
|
5
|
-
export { Rondevu } from './rondevu.js';
|
|
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
8
|
// Export crypto adapters
|
|
@@ -65,12 +65,10 @@ export class NodeCryptoAdapter {
|
|
|
65
65
|
}
|
|
66
66
|
bytesToBase64(bytes) {
|
|
67
67
|
// Node.js Buffer provides native base64 encoding
|
|
68
|
-
// @ts-expect-error - Buffer is available in Node.js but not in browser TypeScript definitions
|
|
69
68
|
return Buffer.from(bytes).toString('base64');
|
|
70
69
|
}
|
|
71
70
|
base64ToBytes(base64) {
|
|
72
71
|
// Node.js Buffer provides native base64 decoding
|
|
73
|
-
// @ts-expect-error - Buffer is available in Node.js but not in browser TypeScript definitions
|
|
74
72
|
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
75
73
|
}
|
|
76
74
|
randomBytes(length) {
|
package/dist/rondevu.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RondevuAPI, Keypair, IceCandidate, BatcherOptions } from './api.js';
|
|
2
2
|
import { CryptoAdapter } from './crypto-adapter.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
3
4
|
export type IceServerPreset = 'ipv4-turn' | 'hostname-turns' | 'google-stun' | 'relay-only';
|
|
4
5
|
export declare const ICE_SERVER_PRESETS: Record<IceServerPreset, RTCIceServer[]>;
|
|
5
6
|
export interface RondevuOptions {
|
|
@@ -10,13 +11,22 @@ export interface RondevuOptions {
|
|
|
10
11
|
batching?: BatcherOptions | false;
|
|
11
12
|
iceServers?: IceServerPreset | RTCIceServer[];
|
|
12
13
|
debug?: boolean;
|
|
14
|
+
rtcPeerConnection?: typeof RTCPeerConnection;
|
|
15
|
+
rtcIceCandidate?: typeof RTCIceCandidate;
|
|
13
16
|
}
|
|
14
17
|
export interface OfferContext {
|
|
15
|
-
pc: RTCPeerConnection;
|
|
16
18
|
dc?: RTCDataChannel;
|
|
17
19
|
offer: RTCSessionDescriptionInit;
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Factory function for creating WebRTC offers.
|
|
23
|
+
* Rondevu creates the RTCPeerConnection and passes it to the factory,
|
|
24
|
+
* allowing ICE candidate handlers to be set up before setLocalDescription() is called.
|
|
25
|
+
*
|
|
26
|
+
* @param pc - The RTCPeerConnection created by Rondevu (already configured with ICE servers)
|
|
27
|
+
* @returns Promise containing the data channel (optional) and offer SDP
|
|
28
|
+
*/
|
|
29
|
+
export type OfferFactory = (pc: RTCPeerConnection) => Promise<OfferContext>;
|
|
20
30
|
export interface PublishServiceOptions {
|
|
21
31
|
service: string;
|
|
22
32
|
maxOffers: number;
|
|
@@ -37,6 +47,59 @@ export interface ConnectToServiceOptions {
|
|
|
37
47
|
onConnection?: (context: ConnectionContext) => void | Promise<void>;
|
|
38
48
|
rtcConfig?: RTCConfiguration;
|
|
39
49
|
}
|
|
50
|
+
export interface ActiveOffer {
|
|
51
|
+
offerId: string;
|
|
52
|
+
serviceFqn: string;
|
|
53
|
+
pc: RTCPeerConnection;
|
|
54
|
+
dc?: RTCDataChannel;
|
|
55
|
+
answered: boolean;
|
|
56
|
+
createdAt: number;
|
|
57
|
+
}
|
|
58
|
+
export interface FindServiceOptions {
|
|
59
|
+
mode?: 'direct' | 'random' | 'paginated';
|
|
60
|
+
limit?: number;
|
|
61
|
+
offset?: number;
|
|
62
|
+
}
|
|
63
|
+
export interface ServiceResult {
|
|
64
|
+
serviceId: string;
|
|
65
|
+
username: string;
|
|
66
|
+
serviceFqn: string;
|
|
67
|
+
offerId: string;
|
|
68
|
+
sdp: string;
|
|
69
|
+
createdAt: number;
|
|
70
|
+
expiresAt: number;
|
|
71
|
+
}
|
|
72
|
+
export interface PaginatedServiceResult {
|
|
73
|
+
services: ServiceResult[];
|
|
74
|
+
count: number;
|
|
75
|
+
limit: number;
|
|
76
|
+
offset: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Base error class for Rondevu errors
|
|
80
|
+
*/
|
|
81
|
+
export declare class RondevuError extends Error {
|
|
82
|
+
context?: Record<string, any> | undefined;
|
|
83
|
+
constructor(message: string, context?: Record<string, any> | undefined);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Network-related errors (API calls, connectivity)
|
|
87
|
+
*/
|
|
88
|
+
export declare class NetworkError extends RondevuError {
|
|
89
|
+
constructor(message: string, context?: Record<string, any>);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Validation errors (invalid input, malformed data)
|
|
93
|
+
*/
|
|
94
|
+
export declare class ValidationError extends RondevuError {
|
|
95
|
+
constructor(message: string, context?: Record<string, any>);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* WebRTC connection errors (peer connection failures, ICE issues)
|
|
99
|
+
*/
|
|
100
|
+
export declare class ConnectionError extends RondevuError {
|
|
101
|
+
constructor(message: string, context?: Record<string, any>);
|
|
102
|
+
}
|
|
40
103
|
/**
|
|
41
104
|
* Rondevu - Complete WebRTC signaling client
|
|
42
105
|
*
|
|
@@ -70,12 +133,12 @@ export interface ConnectToServiceOptions {
|
|
|
70
133
|
* await rondevu.publishService({
|
|
71
134
|
* service: 'chat:2.0.0',
|
|
72
135
|
* maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
73
|
-
* offerFactory: async (
|
|
74
|
-
*
|
|
136
|
+
* offerFactory: async (pc) => {
|
|
137
|
+
* // pc is created by Rondevu with ICE handlers already attached
|
|
75
138
|
* const dc = pc.createDataChannel('chat')
|
|
76
139
|
* const offer = await pc.createOffer()
|
|
77
140
|
* await pc.setLocalDescription(offer)
|
|
78
|
-
* return {
|
|
141
|
+
* return { dc, offer }
|
|
79
142
|
* }
|
|
80
143
|
* })
|
|
81
144
|
*
|
|
@@ -91,7 +154,7 @@ export interface ConnectToServiceOptions {
|
|
|
91
154
|
* rondevu.stopFilling()
|
|
92
155
|
* ```
|
|
93
156
|
*/
|
|
94
|
-
export declare class Rondevu {
|
|
157
|
+
export declare class Rondevu extends EventEmitter {
|
|
95
158
|
private static readonly DEFAULT_TTL_MS;
|
|
96
159
|
private static readonly POLLING_INTERVAL_MS;
|
|
97
160
|
private api;
|
|
@@ -103,6 +166,8 @@ export declare class Rondevu {
|
|
|
103
166
|
private batchingOptions?;
|
|
104
167
|
private iceServers;
|
|
105
168
|
private debugEnabled;
|
|
169
|
+
private rtcPeerConnection?;
|
|
170
|
+
private rtcIceCandidate?;
|
|
106
171
|
private currentService;
|
|
107
172
|
private maxOffers;
|
|
108
173
|
private offerFactory;
|
|
@@ -138,6 +203,7 @@ export declare class Rondevu {
|
|
|
138
203
|
isUsernameClaimed(): Promise<boolean>;
|
|
139
204
|
/**
|
|
140
205
|
* Default offer factory - creates a simple data channel connection
|
|
206
|
+
* The RTCPeerConnection is created by Rondevu and passed in
|
|
141
207
|
*/
|
|
142
208
|
private defaultOfferFactory;
|
|
143
209
|
/**
|
|
@@ -156,6 +222,10 @@ export declare class Rondevu {
|
|
|
156
222
|
publishService(options: PublishServiceOptions): Promise<void>;
|
|
157
223
|
/**
|
|
158
224
|
* Set up ICE candidate handler to send candidates to the server
|
|
225
|
+
*
|
|
226
|
+
* Note: This is used by connectToService() where the offerId is already known.
|
|
227
|
+
* For createOffer(), we use inline ICE handling with early candidate queuing
|
|
228
|
+
* since the offerId isn't available until after the factory completes.
|
|
159
229
|
*/
|
|
160
230
|
private setupIceCandidateHandler;
|
|
161
231
|
/**
|
|
@@ -180,6 +250,32 @@ export declare class Rondevu {
|
|
|
180
250
|
* Closes all active peer connections
|
|
181
251
|
*/
|
|
182
252
|
stopFilling(): void;
|
|
253
|
+
/**
|
|
254
|
+
* Get the count of active offers
|
|
255
|
+
* @returns Number of active offers
|
|
256
|
+
*/
|
|
257
|
+
getOfferCount(): number;
|
|
258
|
+
/**
|
|
259
|
+
* Check if an offer is currently connected
|
|
260
|
+
* @param offerId - The offer ID to check
|
|
261
|
+
* @returns True if the offer exists and has been answered
|
|
262
|
+
*/
|
|
263
|
+
isConnected(offerId: string): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Disconnect all active offers
|
|
266
|
+
* Similar to stopFilling() but doesn't stop the polling/filling process
|
|
267
|
+
*/
|
|
268
|
+
disconnectAll(): Promise<void>;
|
|
269
|
+
/**
|
|
270
|
+
* Get the current service status
|
|
271
|
+
* @returns Object with service state information
|
|
272
|
+
*/
|
|
273
|
+
getServiceStatus(): {
|
|
274
|
+
active: boolean;
|
|
275
|
+
offerCount: number;
|
|
276
|
+
maxOffers: number;
|
|
277
|
+
filling: boolean;
|
|
278
|
+
};
|
|
183
279
|
/**
|
|
184
280
|
* Resolve the full service FQN from various input options
|
|
185
281
|
* Supports direct FQN, service+username, or service discovery
|
|
@@ -217,49 +313,30 @@ export declare class Rondevu {
|
|
|
217
313
|
*/
|
|
218
314
|
connectToService(options: ConnectToServiceOptions): Promise<ConnectionContext>;
|
|
219
315
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
createdAt: number;
|
|
243
|
-
expiresAt: number;
|
|
244
|
-
}>;
|
|
245
|
-
/**
|
|
246
|
-
* Discover multiple available services with pagination
|
|
247
|
-
* Example: chat:1.0.0 (without @username)
|
|
316
|
+
* Find a service - unified discovery method
|
|
317
|
+
*
|
|
318
|
+
* Replaces getService(), discoverService(), and discoverServices() with a single method.
|
|
319
|
+
*
|
|
320
|
+
* @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
|
|
321
|
+
* @param options - Discovery options
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```typescript
|
|
325
|
+
* // Direct lookup (has username)
|
|
326
|
+
* const service = await rondevu.findService('chat:1.0.0@alice')
|
|
327
|
+
*
|
|
328
|
+
* // Random discovery (no username)
|
|
329
|
+
* const service = await rondevu.findService('chat:1.0.0')
|
|
330
|
+
*
|
|
331
|
+
* // Paginated discovery
|
|
332
|
+
* const result = await rondevu.findService('chat:1.0.0', {
|
|
333
|
+
* mode: 'paginated',
|
|
334
|
+
* limit: 20,
|
|
335
|
+
* offset: 0
|
|
336
|
+
* })
|
|
337
|
+
* ```
|
|
248
338
|
*/
|
|
249
|
-
|
|
250
|
-
services: Array<{
|
|
251
|
-
serviceId: string;
|
|
252
|
-
username: string;
|
|
253
|
-
serviceFqn: string;
|
|
254
|
-
offerId: string;
|
|
255
|
-
sdp: string;
|
|
256
|
-
createdAt: number;
|
|
257
|
-
expiresAt: number;
|
|
258
|
-
}>;
|
|
259
|
-
count: number;
|
|
260
|
-
limit: number;
|
|
261
|
-
offset: number;
|
|
262
|
-
}>;
|
|
339
|
+
findService(serviceFqn: string, options?: FindServiceOptions): Promise<ServiceResult | PaginatedServiceResult>;
|
|
263
340
|
/**
|
|
264
341
|
* Post answer SDP to specific offer
|
|
265
342
|
*/
|
package/dist/rondevu.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RondevuAPI } from './api.js';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
2
3
|
// ICE server presets
|
|
3
4
|
export const ICE_SERVER_PRESETS = {
|
|
4
5
|
'ipv4-turn': [
|
|
@@ -43,6 +44,47 @@ export const ICE_SERVER_PRESETS = {
|
|
|
43
44
|
}
|
|
44
45
|
]
|
|
45
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Base error class for Rondevu errors
|
|
49
|
+
*/
|
|
50
|
+
export class RondevuError extends Error {
|
|
51
|
+
constructor(message, context) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.context = context;
|
|
54
|
+
this.name = 'RondevuError';
|
|
55
|
+
Object.setPrototypeOf(this, RondevuError.prototype);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Network-related errors (API calls, connectivity)
|
|
60
|
+
*/
|
|
61
|
+
export class NetworkError extends RondevuError {
|
|
62
|
+
constructor(message, context) {
|
|
63
|
+
super(message, context);
|
|
64
|
+
this.name = 'NetworkError';
|
|
65
|
+
Object.setPrototypeOf(this, NetworkError.prototype);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validation errors (invalid input, malformed data)
|
|
70
|
+
*/
|
|
71
|
+
export class ValidationError extends RondevuError {
|
|
72
|
+
constructor(message, context) {
|
|
73
|
+
super(message, context);
|
|
74
|
+
this.name = 'ValidationError';
|
|
75
|
+
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* WebRTC connection errors (peer connection failures, ICE issues)
|
|
80
|
+
*/
|
|
81
|
+
export class ConnectionError extends RondevuError {
|
|
82
|
+
constructor(message, context) {
|
|
83
|
+
super(message, context);
|
|
84
|
+
this.name = 'ConnectionError';
|
|
85
|
+
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
46
88
|
/**
|
|
47
89
|
* Rondevu - Complete WebRTC signaling client
|
|
48
90
|
*
|
|
@@ -76,12 +118,12 @@ export const ICE_SERVER_PRESETS = {
|
|
|
76
118
|
* await rondevu.publishService({
|
|
77
119
|
* service: 'chat:2.0.0',
|
|
78
120
|
* maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
79
|
-
* offerFactory: async (
|
|
80
|
-
*
|
|
121
|
+
* offerFactory: async (pc) => {
|
|
122
|
+
* // pc is created by Rondevu with ICE handlers already attached
|
|
81
123
|
* const dc = pc.createDataChannel('chat')
|
|
82
124
|
* const offer = await pc.createOffer()
|
|
83
125
|
* await pc.setLocalDescription(offer)
|
|
84
|
-
* return {
|
|
126
|
+
* return { dc, offer }
|
|
85
127
|
* }
|
|
86
128
|
* })
|
|
87
129
|
*
|
|
@@ -97,8 +139,9 @@ export const ICE_SERVER_PRESETS = {
|
|
|
97
139
|
* rondevu.stopFilling()
|
|
98
140
|
* ```
|
|
99
141
|
*/
|
|
100
|
-
export class Rondevu {
|
|
101
|
-
constructor(apiUrl, username, keypair, api, iceServers, cryptoAdapter, batchingOptions, debugEnabled = false) {
|
|
142
|
+
export class Rondevu extends EventEmitter {
|
|
143
|
+
constructor(apiUrl, username, keypair, api, iceServers, cryptoAdapter, batchingOptions, debugEnabled = false, rtcPeerConnection, rtcIceCandidate) {
|
|
144
|
+
super();
|
|
102
145
|
this.usernameClaimed = false;
|
|
103
146
|
// Service management
|
|
104
147
|
this.currentService = null;
|
|
@@ -118,6 +161,8 @@ export class Rondevu {
|
|
|
118
161
|
this.cryptoAdapter = cryptoAdapter;
|
|
119
162
|
this.batchingOptions = batchingOptions;
|
|
120
163
|
this.debugEnabled = debugEnabled;
|
|
164
|
+
this.rtcPeerConnection = rtcPeerConnection;
|
|
165
|
+
this.rtcIceCandidate = rtcIceCandidate;
|
|
121
166
|
this.debug('Instance created:', {
|
|
122
167
|
username: this.username,
|
|
123
168
|
publicKey: this.keypair.publicKey,
|
|
@@ -146,6 +191,13 @@ export class Rondevu {
|
|
|
146
191
|
*/
|
|
147
192
|
static async connect(options) {
|
|
148
193
|
const username = options.username || Rondevu.generateAnonymousUsername();
|
|
194
|
+
// Apply WebRTC polyfills to global scope if provided (Node.js environments)
|
|
195
|
+
if (options.rtcPeerConnection) {
|
|
196
|
+
globalThis.RTCPeerConnection = options.rtcPeerConnection;
|
|
197
|
+
}
|
|
198
|
+
if (options.rtcIceCandidate) {
|
|
199
|
+
globalThis.RTCIceCandidate = options.rtcIceCandidate;
|
|
200
|
+
}
|
|
149
201
|
// Handle preset string or custom array
|
|
150
202
|
let iceServers;
|
|
151
203
|
if (typeof options.iceServers === 'string') {
|
|
@@ -181,7 +233,7 @@ export class Rondevu {
|
|
|
181
233
|
const api = new RondevuAPI(options.apiUrl, username, keypair, options.cryptoAdapter, options.batching);
|
|
182
234
|
if (options.debug)
|
|
183
235
|
console.log('[Rondevu] Created API instance');
|
|
184
|
-
return new Rondevu(options.apiUrl, username, keypair, api, iceServers, options.cryptoAdapter, options.batching, options.debug || false);
|
|
236
|
+
return new Rondevu(options.apiUrl, username, keypair, api, iceServers, options.cryptoAdapter, options.batching, options.debug || false, options.rtcPeerConnection, options.rtcIceCandidate);
|
|
185
237
|
}
|
|
186
238
|
/**
|
|
187
239
|
* Generate an anonymous username with timestamp and random component
|
|
@@ -215,13 +267,13 @@ export class Rondevu {
|
|
|
215
267
|
// ============================================
|
|
216
268
|
/**
|
|
217
269
|
* Default offer factory - creates a simple data channel connection
|
|
270
|
+
* The RTCPeerConnection is created by Rondevu and passed in
|
|
218
271
|
*/
|
|
219
|
-
async defaultOfferFactory(
|
|
220
|
-
const pc = new RTCPeerConnection(rtcConfig);
|
|
272
|
+
async defaultOfferFactory(pc) {
|
|
221
273
|
const dc = pc.createDataChannel('default');
|
|
222
274
|
const offer = await pc.createOffer();
|
|
223
275
|
await pc.setLocalDescription(offer);
|
|
224
|
-
return {
|
|
276
|
+
return { dc, offer };
|
|
225
277
|
}
|
|
226
278
|
/**
|
|
227
279
|
* Publish a service with automatic offer management
|
|
@@ -247,6 +299,10 @@ export class Rondevu {
|
|
|
247
299
|
}
|
|
248
300
|
/**
|
|
249
301
|
* Set up ICE candidate handler to send candidates to the server
|
|
302
|
+
*
|
|
303
|
+
* Note: This is used by connectToService() where the offerId is already known.
|
|
304
|
+
* For createOffer(), we use inline ICE handling with early candidate queuing
|
|
305
|
+
* since the offerId isn't available until after the factory completes.
|
|
250
306
|
*/
|
|
251
307
|
setupIceCandidateHandler(pc, serviceFqn, offerId) {
|
|
252
308
|
pc.onicecandidate = async (event) => {
|
|
@@ -258,6 +314,8 @@ export class Rondevu {
|
|
|
258
314
|
const candidateData = typeof event.candidate.toJSON === 'function'
|
|
259
315
|
? event.candidate.toJSON()
|
|
260
316
|
: event.candidate;
|
|
317
|
+
// Emit local ICE candidate event
|
|
318
|
+
this.emit('ice:candidate:local', offerId, candidateData);
|
|
261
319
|
await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]);
|
|
262
320
|
}
|
|
263
321
|
catch (err) {
|
|
@@ -276,12 +334,58 @@ export class Rondevu {
|
|
|
276
334
|
const rtcConfig = {
|
|
277
335
|
iceServers: this.iceServers
|
|
278
336
|
};
|
|
279
|
-
this.debug('Creating new offer...');
|
|
280
|
-
// Create the offer using the factory
|
|
281
|
-
const { pc, dc, offer } = await this.offerFactory(rtcConfig);
|
|
282
337
|
// Auto-append username to service
|
|
283
338
|
const serviceFqn = `${this.currentService}@${this.username}`;
|
|
284
|
-
|
|
339
|
+
this.debug('Creating new offer...');
|
|
340
|
+
// 1. Create the RTCPeerConnection - Rondevu controls this to set up handlers early
|
|
341
|
+
const pc = new RTCPeerConnection(rtcConfig);
|
|
342
|
+
// 2. Set up ICE candidate handler with queuing BEFORE the factory runs
|
|
343
|
+
// This ensures we capture all candidates, even those generated immediately
|
|
344
|
+
// when setLocalDescription() is called in the factory
|
|
345
|
+
const earlyIceCandidates = [];
|
|
346
|
+
let offerId;
|
|
347
|
+
pc.onicecandidate = async (event) => {
|
|
348
|
+
if (event.candidate) {
|
|
349
|
+
// Handle both browser and Node.js (wrtc) environments
|
|
350
|
+
const candidateData = typeof event.candidate.toJSON === 'function'
|
|
351
|
+
? event.candidate.toJSON()
|
|
352
|
+
: event.candidate;
|
|
353
|
+
// Emit local ICE candidate event
|
|
354
|
+
if (offerId) {
|
|
355
|
+
this.emit('ice:candidate:local', offerId, candidateData);
|
|
356
|
+
}
|
|
357
|
+
if (offerId) {
|
|
358
|
+
// We have the offerId, send directly
|
|
359
|
+
try {
|
|
360
|
+
await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]);
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
console.error('[Rondevu] Failed to send ICE candidate:', err);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// Queue for later - we don't have the offerId yet
|
|
368
|
+
this.debug('Queuing early ICE candidate');
|
|
369
|
+
earlyIceCandidates.push(candidateData);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
// 3. Call the factory with the pc - factory creates data channel and offer
|
|
374
|
+
// When factory calls setLocalDescription(), ICE gathering starts and
|
|
375
|
+
// candidates are captured by the handler we set up above
|
|
376
|
+
let dc;
|
|
377
|
+
let offer;
|
|
378
|
+
try {
|
|
379
|
+
const factoryResult = await this.offerFactory(pc);
|
|
380
|
+
dc = factoryResult.dc;
|
|
381
|
+
offer = factoryResult.offer;
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
// Clean up the connection if factory fails
|
|
385
|
+
pc.close();
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
// 4. Publish to server to get offerId
|
|
285
389
|
const result = await this.api.publishService({
|
|
286
390
|
serviceFqn,
|
|
287
391
|
offers: [{ sdp: offer.sdp }],
|
|
@@ -289,8 +393,8 @@ export class Rondevu {
|
|
|
289
393
|
signature: '',
|
|
290
394
|
message: '',
|
|
291
395
|
});
|
|
292
|
-
|
|
293
|
-
// Store active offer
|
|
396
|
+
offerId = result.offers[0].offerId;
|
|
397
|
+
// 5. Store active offer
|
|
294
398
|
this.activeOffers.set(offerId, {
|
|
295
399
|
offerId,
|
|
296
400
|
serviceFqn,
|
|
@@ -300,12 +404,29 @@ export class Rondevu {
|
|
|
300
404
|
createdAt: Date.now()
|
|
301
405
|
});
|
|
302
406
|
this.debug(`Offer created: ${offerId}`);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
407
|
+
this.emit('offer:created', offerId, serviceFqn);
|
|
408
|
+
// Set up data channel open handler (offerer side)
|
|
409
|
+
if (dc) {
|
|
410
|
+
dc.onopen = () => {
|
|
411
|
+
this.debug(`Data channel opened for offer ${offerId}`);
|
|
412
|
+
this.emit('connection:opened', offerId, dc);
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// 6. Send any queued early ICE candidates
|
|
416
|
+
if (earlyIceCandidates.length > 0) {
|
|
417
|
+
this.debug(`Sending ${earlyIceCandidates.length} early ICE candidates`);
|
|
418
|
+
try {
|
|
419
|
+
await this.api.addOfferIceCandidates(serviceFqn, offerId, earlyIceCandidates);
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
console.error('[Rondevu] Failed to send early ICE candidates:', err);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// 7. Monitor connection state
|
|
306
426
|
pc.onconnectionstatechange = () => {
|
|
307
427
|
this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`);
|
|
308
428
|
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
|
429
|
+
this.emit('connection:closed', offerId);
|
|
309
430
|
this.activeOffers.delete(offerId);
|
|
310
431
|
this.fillOffers(); // Try to replace failed offer
|
|
311
432
|
}
|
|
@@ -348,6 +469,7 @@ export class Rondevu {
|
|
|
348
469
|
});
|
|
349
470
|
activeOffer.answered = true;
|
|
350
471
|
this.lastPollTimestamp = answer.answeredAt;
|
|
472
|
+
this.emit('offer:answered', answer.offerId, answer.answererId);
|
|
351
473
|
// Create replacement offer
|
|
352
474
|
this.fillOffers();
|
|
353
475
|
}
|
|
@@ -359,6 +481,7 @@ export class Rondevu {
|
|
|
359
481
|
const answererCandidates = candidates.filter(c => c.role === 'answerer');
|
|
360
482
|
for (const item of answererCandidates) {
|
|
361
483
|
if (item.candidate) {
|
|
484
|
+
this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
|
|
362
485
|
await activeOffer.pc.addIceCandidate(new RTCIceCandidate(item.candidate));
|
|
363
486
|
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt);
|
|
364
487
|
}
|
|
@@ -411,6 +534,47 @@ export class Rondevu {
|
|
|
411
534
|
}
|
|
412
535
|
this.activeOffers.clear();
|
|
413
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Get the count of active offers
|
|
539
|
+
* @returns Number of active offers
|
|
540
|
+
*/
|
|
541
|
+
getOfferCount() {
|
|
542
|
+
return this.activeOffers.size;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Check if an offer is currently connected
|
|
546
|
+
* @param offerId - The offer ID to check
|
|
547
|
+
* @returns True if the offer exists and has been answered
|
|
548
|
+
*/
|
|
549
|
+
isConnected(offerId) {
|
|
550
|
+
const offer = this.activeOffers.get(offerId);
|
|
551
|
+
return offer ? offer.answered : false;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Disconnect all active offers
|
|
555
|
+
* Similar to stopFilling() but doesn't stop the polling/filling process
|
|
556
|
+
*/
|
|
557
|
+
async disconnectAll() {
|
|
558
|
+
this.debug('Disconnecting all offers');
|
|
559
|
+
for (const [offerId, offer] of this.activeOffers.entries()) {
|
|
560
|
+
this.debug(`Closing offer ${offerId}`);
|
|
561
|
+
offer.dc?.close();
|
|
562
|
+
offer.pc.close();
|
|
563
|
+
}
|
|
564
|
+
this.activeOffers.clear();
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Get the current service status
|
|
568
|
+
* @returns Object with service state information
|
|
569
|
+
*/
|
|
570
|
+
getServiceStatus() {
|
|
571
|
+
return {
|
|
572
|
+
active: this.currentService !== null,
|
|
573
|
+
offerCount: this.activeOffers.size,
|
|
574
|
+
maxOffers: this.maxOffers,
|
|
575
|
+
filling: this.filling
|
|
576
|
+
};
|
|
577
|
+
}
|
|
414
578
|
/**
|
|
415
579
|
* Resolve the full service FQN from various input options
|
|
416
580
|
* Supports direct FQN, service+username, or service discovery
|
|
@@ -426,7 +590,7 @@ export class Rondevu {
|
|
|
426
590
|
else if (service) {
|
|
427
591
|
// Discovery mode - get random service
|
|
428
592
|
this.debug(`Discovering service: ${service}`);
|
|
429
|
-
const discovered = await this.
|
|
593
|
+
const discovered = await this.findService(service);
|
|
430
594
|
return discovered.serviceFqn;
|
|
431
595
|
}
|
|
432
596
|
else {
|
|
@@ -444,6 +608,7 @@ export class Rondevu {
|
|
|
444
608
|
const result = await this.api.getOfferIceCandidates(serviceFqn, offerId, lastIceTimestamp);
|
|
445
609
|
for (const item of result.candidates) {
|
|
446
610
|
if (item.candidate) {
|
|
611
|
+
this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
|
|
447
612
|
await pc.addIceCandidate(new RTCIceCandidate(item.candidate));
|
|
448
613
|
lastIceTimestamp = item.createdAt;
|
|
449
614
|
}
|
|
@@ -508,6 +673,7 @@ export class Rondevu {
|
|
|
508
673
|
pc.ondatachannel = (event) => {
|
|
509
674
|
this.debug('Data channel received from offerer');
|
|
510
675
|
dc = event.channel;
|
|
676
|
+
this.emit('connection:opened', serviceData.offerId, dc);
|
|
511
677
|
resolve(dc);
|
|
512
678
|
};
|
|
513
679
|
});
|
|
@@ -565,25 +731,41 @@ export class Rondevu {
|
|
|
565
731
|
// Service Discovery
|
|
566
732
|
// ============================================
|
|
567
733
|
/**
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
*
|
|
583
|
-
*
|
|
734
|
+
* Find a service - unified discovery method
|
|
735
|
+
*
|
|
736
|
+
* Replaces getService(), discoverService(), and discoverServices() with a single method.
|
|
737
|
+
*
|
|
738
|
+
* @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
|
|
739
|
+
* @param options - Discovery options
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* // Direct lookup (has username)
|
|
744
|
+
* const service = await rondevu.findService('chat:1.0.0@alice')
|
|
745
|
+
*
|
|
746
|
+
* // Random discovery (no username)
|
|
747
|
+
* const service = await rondevu.findService('chat:1.0.0')
|
|
748
|
+
*
|
|
749
|
+
* // Paginated discovery
|
|
750
|
+
* const result = await rondevu.findService('chat:1.0.0', {
|
|
751
|
+
* mode: 'paginated',
|
|
752
|
+
* limit: 20,
|
|
753
|
+
* offset: 0
|
|
754
|
+
* })
|
|
755
|
+
* ```
|
|
584
756
|
*/
|
|
585
|
-
async
|
|
586
|
-
|
|
757
|
+
async findService(serviceFqn, options) {
|
|
758
|
+
const { mode, limit = 10, offset = 0 } = options || {};
|
|
759
|
+
// Auto-detect mode if not specified
|
|
760
|
+
const hasUsername = serviceFqn.includes('@');
|
|
761
|
+
const effectiveMode = mode || (hasUsername ? 'direct' : 'random');
|
|
762
|
+
if (effectiveMode === 'paginated') {
|
|
763
|
+
return await this.api.getService(serviceFqn, { limit, offset });
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
// Both 'direct' and 'random' use the same API call
|
|
767
|
+
return await this.api.getService(serviceFqn);
|
|
768
|
+
}
|
|
587
769
|
}
|
|
588
770
|
// ============================================
|
|
589
771
|
// WebRTC Signaling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtr-dev/rondevu-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@eslint/js": "^9.39.1",
|
|
28
|
+
"@types/node": "^25.0.2",
|
|
28
29
|
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
|
29
30
|
"@typescript-eslint/parser": "^8.48.1",
|
|
30
31
|
"eslint": "^9.39.1",
|