@xtr-dev/rondevu-client 0.18.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 +121 -48
- package/dist/rondevu.js +175 -40
- 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 {
|
|
@@ -14,11 +15,18 @@ export interface RondevuOptions {
|
|
|
14
15
|
rtcIceCandidate?: typeof RTCIceCandidate;
|
|
15
16
|
}
|
|
16
17
|
export interface OfferContext {
|
|
17
|
-
pc: RTCPeerConnection;
|
|
18
18
|
dc?: RTCDataChannel;
|
|
19
19
|
offer: RTCSessionDescriptionInit;
|
|
20
20
|
}
|
|
21
|
-
|
|
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>;
|
|
22
30
|
export interface PublishServiceOptions {
|
|
23
31
|
service: string;
|
|
24
32
|
maxOffers: number;
|
|
@@ -39,6 +47,59 @@ export interface ConnectToServiceOptions {
|
|
|
39
47
|
onConnection?: (context: ConnectionContext) => void | Promise<void>;
|
|
40
48
|
rtcConfig?: RTCConfiguration;
|
|
41
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
|
+
}
|
|
42
103
|
/**
|
|
43
104
|
* Rondevu - Complete WebRTC signaling client
|
|
44
105
|
*
|
|
@@ -72,12 +133,12 @@ export interface ConnectToServiceOptions {
|
|
|
72
133
|
* await rondevu.publishService({
|
|
73
134
|
* service: 'chat:2.0.0',
|
|
74
135
|
* maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
75
|
-
* offerFactory: async (
|
|
76
|
-
*
|
|
136
|
+
* offerFactory: async (pc) => {
|
|
137
|
+
* // pc is created by Rondevu with ICE handlers already attached
|
|
77
138
|
* const dc = pc.createDataChannel('chat')
|
|
78
139
|
* const offer = await pc.createOffer()
|
|
79
140
|
* await pc.setLocalDescription(offer)
|
|
80
|
-
* return {
|
|
141
|
+
* return { dc, offer }
|
|
81
142
|
* }
|
|
82
143
|
* })
|
|
83
144
|
*
|
|
@@ -93,7 +154,7 @@ export interface ConnectToServiceOptions {
|
|
|
93
154
|
* rondevu.stopFilling()
|
|
94
155
|
* ```
|
|
95
156
|
*/
|
|
96
|
-
export declare class Rondevu {
|
|
157
|
+
export declare class Rondevu extends EventEmitter {
|
|
97
158
|
private static readonly DEFAULT_TTL_MS;
|
|
98
159
|
private static readonly POLLING_INTERVAL_MS;
|
|
99
160
|
private api;
|
|
@@ -142,6 +203,7 @@ export declare class Rondevu {
|
|
|
142
203
|
isUsernameClaimed(): Promise<boolean>;
|
|
143
204
|
/**
|
|
144
205
|
* Default offer factory - creates a simple data channel connection
|
|
206
|
+
* The RTCPeerConnection is created by Rondevu and passed in
|
|
145
207
|
*/
|
|
146
208
|
private defaultOfferFactory;
|
|
147
209
|
/**
|
|
@@ -160,6 +222,10 @@ export declare class Rondevu {
|
|
|
160
222
|
publishService(options: PublishServiceOptions): Promise<void>;
|
|
161
223
|
/**
|
|
162
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.
|
|
163
229
|
*/
|
|
164
230
|
private setupIceCandidateHandler;
|
|
165
231
|
/**
|
|
@@ -184,6 +250,32 @@ export declare class Rondevu {
|
|
|
184
250
|
* Closes all active peer connections
|
|
185
251
|
*/
|
|
186
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
|
+
};
|
|
187
279
|
/**
|
|
188
280
|
* Resolve the full service FQN from various input options
|
|
189
281
|
* Supports direct FQN, service+username, or service discovery
|
|
@@ -221,49 +313,30 @@ export declare class Rondevu {
|
|
|
221
313
|
*/
|
|
222
314
|
connectToService(options: ConnectToServiceOptions): Promise<ConnectionContext>;
|
|
223
315
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
createdAt: number;
|
|
247
|
-
expiresAt: number;
|
|
248
|
-
}>;
|
|
249
|
-
/**
|
|
250
|
-
* Discover multiple available services with pagination
|
|
251
|
-
* 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
|
+
* ```
|
|
252
338
|
*/
|
|
253
|
-
|
|
254
|
-
services: Array<{
|
|
255
|
-
serviceId: string;
|
|
256
|
-
username: string;
|
|
257
|
-
serviceFqn: string;
|
|
258
|
-
offerId: string;
|
|
259
|
-
sdp: string;
|
|
260
|
-
createdAt: number;
|
|
261
|
-
expiresAt: number;
|
|
262
|
-
}>;
|
|
263
|
-
count: number;
|
|
264
|
-
limit: number;
|
|
265
|
-
offset: number;
|
|
266
|
-
}>;
|
|
339
|
+
findService(serviceFqn: string, options?: FindServiceOptions): Promise<ServiceResult | PaginatedServiceResult>;
|
|
267
340
|
/**
|
|
268
341
|
* Post answer SDP to specific offer
|
|
269
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 {
|
|
142
|
+
export class Rondevu extends EventEmitter {
|
|
101
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;
|
|
@@ -224,13 +267,13 @@ export class Rondevu {
|
|
|
224
267
|
// ============================================
|
|
225
268
|
/**
|
|
226
269
|
* Default offer factory - creates a simple data channel connection
|
|
270
|
+
* The RTCPeerConnection is created by Rondevu and passed in
|
|
227
271
|
*/
|
|
228
|
-
async defaultOfferFactory(
|
|
229
|
-
const pc = new RTCPeerConnection(rtcConfig);
|
|
272
|
+
async defaultOfferFactory(pc) {
|
|
230
273
|
const dc = pc.createDataChannel('default');
|
|
231
274
|
const offer = await pc.createOffer();
|
|
232
275
|
await pc.setLocalDescription(offer);
|
|
233
|
-
return {
|
|
276
|
+
return { dc, offer };
|
|
234
277
|
}
|
|
235
278
|
/**
|
|
236
279
|
* Publish a service with automatic offer management
|
|
@@ -256,6 +299,10 @@ export class Rondevu {
|
|
|
256
299
|
}
|
|
257
300
|
/**
|
|
258
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.
|
|
259
306
|
*/
|
|
260
307
|
setupIceCandidateHandler(pc, serviceFqn, offerId) {
|
|
261
308
|
pc.onicecandidate = async (event) => {
|
|
@@ -267,6 +314,8 @@ export class Rondevu {
|
|
|
267
314
|
const candidateData = typeof event.candidate.toJSON === 'function'
|
|
268
315
|
? event.candidate.toJSON()
|
|
269
316
|
: event.candidate;
|
|
317
|
+
// Emit local ICE candidate event
|
|
318
|
+
this.emit('ice:candidate:local', offerId, candidateData);
|
|
270
319
|
await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]);
|
|
271
320
|
}
|
|
272
321
|
catch (err) {
|
|
@@ -285,25 +334,26 @@ export class Rondevu {
|
|
|
285
334
|
const rtcConfig = {
|
|
286
335
|
iceServers: this.iceServers
|
|
287
336
|
};
|
|
288
|
-
this.debug('Creating new offer...');
|
|
289
|
-
// Create the offer using the factory
|
|
290
|
-
// Note: The factory may call setLocalDescription() which triggers ICE gathering
|
|
291
|
-
const { pc, dc, offer } = await this.offerFactory(rtcConfig);
|
|
292
337
|
// Auto-append username to service
|
|
293
338
|
const serviceFqn = `${this.currentService}@${this.username}`;
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
|
|
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
|
|
297
345
|
const earlyIceCandidates = [];
|
|
298
|
-
let offerId
|
|
299
|
-
// Set up a queuing ICE candidate handler immediately after getting the pc
|
|
300
|
-
// This captures any candidates that fire before we have the offerId
|
|
346
|
+
let offerId;
|
|
301
347
|
pc.onicecandidate = async (event) => {
|
|
302
348
|
if (event.candidate) {
|
|
303
349
|
// Handle both browser and Node.js (wrtc) environments
|
|
304
350
|
const candidateData = typeof event.candidate.toJSON === 'function'
|
|
305
351
|
? event.candidate.toJSON()
|
|
306
352
|
: event.candidate;
|
|
353
|
+
// Emit local ICE candidate event
|
|
354
|
+
if (offerId) {
|
|
355
|
+
this.emit('ice:candidate:local', offerId, candidateData);
|
|
356
|
+
}
|
|
307
357
|
if (offerId) {
|
|
308
358
|
// We have the offerId, send directly
|
|
309
359
|
try {
|
|
@@ -320,7 +370,22 @@ export class Rondevu {
|
|
|
320
370
|
}
|
|
321
371
|
}
|
|
322
372
|
};
|
|
323
|
-
//
|
|
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
|
|
324
389
|
const result = await this.api.publishService({
|
|
325
390
|
serviceFqn,
|
|
326
391
|
offers: [{ sdp: offer.sdp }],
|
|
@@ -329,7 +394,7 @@ export class Rondevu {
|
|
|
329
394
|
message: '',
|
|
330
395
|
});
|
|
331
396
|
offerId = result.offers[0].offerId;
|
|
332
|
-
// Store active offer
|
|
397
|
+
// 5. Store active offer
|
|
333
398
|
this.activeOffers.set(offerId, {
|
|
334
399
|
offerId,
|
|
335
400
|
serviceFqn,
|
|
@@ -339,7 +404,15 @@ export class Rondevu {
|
|
|
339
404
|
createdAt: Date.now()
|
|
340
405
|
});
|
|
341
406
|
this.debug(`Offer created: ${offerId}`);
|
|
342
|
-
|
|
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
|
|
343
416
|
if (earlyIceCandidates.length > 0) {
|
|
344
417
|
this.debug(`Sending ${earlyIceCandidates.length} early ICE candidates`);
|
|
345
418
|
try {
|
|
@@ -349,10 +422,11 @@ export class Rondevu {
|
|
|
349
422
|
console.error('[Rondevu] Failed to send early ICE candidates:', err);
|
|
350
423
|
}
|
|
351
424
|
}
|
|
352
|
-
// Monitor connection state
|
|
425
|
+
// 7. Monitor connection state
|
|
353
426
|
pc.onconnectionstatechange = () => {
|
|
354
427
|
this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`);
|
|
355
428
|
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
|
429
|
+
this.emit('connection:closed', offerId);
|
|
356
430
|
this.activeOffers.delete(offerId);
|
|
357
431
|
this.fillOffers(); // Try to replace failed offer
|
|
358
432
|
}
|
|
@@ -395,6 +469,7 @@ export class Rondevu {
|
|
|
395
469
|
});
|
|
396
470
|
activeOffer.answered = true;
|
|
397
471
|
this.lastPollTimestamp = answer.answeredAt;
|
|
472
|
+
this.emit('offer:answered', answer.offerId, answer.answererId);
|
|
398
473
|
// Create replacement offer
|
|
399
474
|
this.fillOffers();
|
|
400
475
|
}
|
|
@@ -406,6 +481,7 @@ export class Rondevu {
|
|
|
406
481
|
const answererCandidates = candidates.filter(c => c.role === 'answerer');
|
|
407
482
|
for (const item of answererCandidates) {
|
|
408
483
|
if (item.candidate) {
|
|
484
|
+
this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
|
|
409
485
|
await activeOffer.pc.addIceCandidate(new RTCIceCandidate(item.candidate));
|
|
410
486
|
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt);
|
|
411
487
|
}
|
|
@@ -458,6 +534,47 @@ export class Rondevu {
|
|
|
458
534
|
}
|
|
459
535
|
this.activeOffers.clear();
|
|
460
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
|
+
}
|
|
461
578
|
/**
|
|
462
579
|
* Resolve the full service FQN from various input options
|
|
463
580
|
* Supports direct FQN, service+username, or service discovery
|
|
@@ -473,7 +590,7 @@ export class Rondevu {
|
|
|
473
590
|
else if (service) {
|
|
474
591
|
// Discovery mode - get random service
|
|
475
592
|
this.debug(`Discovering service: ${service}`);
|
|
476
|
-
const discovered = await this.
|
|
593
|
+
const discovered = await this.findService(service);
|
|
477
594
|
return discovered.serviceFqn;
|
|
478
595
|
}
|
|
479
596
|
else {
|
|
@@ -491,6 +608,7 @@ export class Rondevu {
|
|
|
491
608
|
const result = await this.api.getOfferIceCandidates(serviceFqn, offerId, lastIceTimestamp);
|
|
492
609
|
for (const item of result.candidates) {
|
|
493
610
|
if (item.candidate) {
|
|
611
|
+
this.emit('ice:candidate:remote', offerId, item.candidate, item.role);
|
|
494
612
|
await pc.addIceCandidate(new RTCIceCandidate(item.candidate));
|
|
495
613
|
lastIceTimestamp = item.createdAt;
|
|
496
614
|
}
|
|
@@ -555,6 +673,7 @@ export class Rondevu {
|
|
|
555
673
|
pc.ondatachannel = (event) => {
|
|
556
674
|
this.debug('Data channel received from offerer');
|
|
557
675
|
dc = event.channel;
|
|
676
|
+
this.emit('connection:opened', serviceData.offerId, dc);
|
|
558
677
|
resolve(dc);
|
|
559
678
|
};
|
|
560
679
|
});
|
|
@@ -612,25 +731,41 @@ export class Rondevu {
|
|
|
612
731
|
// Service Discovery
|
|
613
732
|
// ============================================
|
|
614
733
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
*
|
|
630
|
-
*
|
|
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
|
+
* ```
|
|
631
756
|
*/
|
|
632
|
-
async
|
|
633
|
-
|
|
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
|
+
}
|
|
634
769
|
}
|
|
635
770
|
// ============================================
|
|
636
771
|
// WebRTC Signaling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtr-dev/rondevu-client",
|
|
3
|
-
"version": "0.18.
|
|
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",
|