@xtr-dev/rondevu-client 0.12.3 → 0.13.0

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.
@@ -1,324 +0,0 @@
1
- /**
2
- * ConnectionManager - Manages WebRTC peer connections with automatic reconnection
3
- *
4
- * Provides high-level API for:
5
- * - Hosting services (creating offers and publishing them)
6
- * - Connecting to services (finding and answering offers)
7
- * - Automatic reconnection when connections fail
8
- * - Connection lifecycle management
9
- */
10
- import { WebRTCContext } from './webrtc-context.js';
11
- import { WebRTCRondevuConnection } from './connection.js';
12
- import { RondevuAPI } from './api.js';
13
- import { RondevuSignaler } from './signaler.js';
14
- import { createBin } from './bin.js';
15
- /**
16
- * ConnectionManager - High-level connection management
17
- *
18
- * @example
19
- * // Host a service
20
- * const manager = new ConnectionManager({
21
- * apiUrl: 'https://api.ronde.vu',
22
- * credentials: await api.register()
23
- * })
24
- *
25
- * await manager.hostService({
26
- * service: 'chat.app@1.0.0',
27
- * onConnection: (conn) => {
28
- * conn.events.on('message', msg => console.log('Received:', msg))
29
- * }
30
- * })
31
- *
32
- * @example
33
- * // Connect to a service
34
- * const connection = await manager.connectToService({
35
- * username: 'alice',
36
- * service: 'chat.app@1.0.0'
37
- * })
38
- *
39
- * await connection.sendMessage('Hello!')
40
- */
41
- export class ConnectionManager {
42
- constructor(options) {
43
- this.connections = new Map();
44
- this.bin = createBin();
45
- this.api = new RondevuAPI(options.apiUrl, options.credentials);
46
- this.username = options.username;
47
- this.autoReconnect = options.autoReconnect ?? true;
48
- this.reconnectDelay = options.reconnectDelay ?? 5000;
49
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
50
- }
51
- /**
52
- * Host a service - Creates an offer and publishes it to the signaling server
53
- *
54
- * The service will automatically accept incoming connections and manage them.
55
- * Each new connection triggers the onConnection callback.
56
- *
57
- * @param options - Service hosting options
58
- * @returns Promise that resolves when the service is published
59
- */
60
- async hostService(options) {
61
- const { service, ttl = 300000, onConnection } = options;
62
- console.log(`[ConnectionManager] Hosting service: ${service}`);
63
- // Create WebRTC context with a temporary signaler
64
- // We'll replace it once we have the offer ID
65
- const tempSignaler = this.createTempSignaler();
66
- const context = new WebRTCContext(tempSignaler);
67
- // Create connection (offerer role)
68
- const connection = new WebRTCRondevuConnection({
69
- id: `host-${service}-${Date.now()}`,
70
- host: this.username,
71
- service,
72
- offer: null,
73
- context,
74
- });
75
- // Wait for offer to be created
76
- await connection.ready;
77
- // Get the offer SDP
78
- if (!connection.connection) {
79
- throw new Error('RTCPeerConnection not initialized');
80
- }
81
- const offerSdp = connection.connection.localDescription?.sdp;
82
- if (!offerSdp) {
83
- throw new Error('Failed to create offer');
84
- }
85
- console.log(`[ConnectionManager] Offer created for ${service}`);
86
- // Create offer on server
87
- const offers = await this.api.createOffers([
88
- {
89
- sdp: offerSdp,
90
- ttl,
91
- },
92
- ]);
93
- const offerId = offers[0].id;
94
- console.log(`[ConnectionManager] Offer published: ${offerId}`);
95
- // Now create the real signaler with the offer ID
96
- const signaler = new RondevuSignaler(this.api, offerId);
97
- context.signaler = signaler;
98
- // Set up connection tracking
99
- const bin = createBin();
100
- // Track connection state
101
- bin(connection.events.on('state-change', state => {
102
- console.log(`[ConnectionManager] ${service} state: ${state}`);
103
- if (state === 'connected' && onConnection) {
104
- onConnection(connection);
105
- }
106
- // Handle disconnection
107
- if (state === 'disconnected' && this.autoReconnect) {
108
- this.scheduleReconnect(connection.id, service, offerId);
109
- }
110
- }));
111
- // Cleanup function
112
- const cleanup = () => bin.clean();
113
- // Store managed connection
114
- this.connections.set(connection.id, {
115
- connection,
116
- offerId,
117
- service,
118
- cleanup,
119
- reconnectAttempts: 0,
120
- isReconnecting: false,
121
- });
122
- // Clean up on expiry
123
- setTimeout(() => {
124
- console.log(`[ConnectionManager] Service ${service} expired`);
125
- this.removeConnection(connection.id);
126
- }, ttl - 1000);
127
- console.log(`[ConnectionManager] Service ${service} is now hosted`);
128
- }
129
- /**
130
- * Connect to a hosted service
131
- *
132
- * Searches for the service, retrieves the offer, and creates an answering connection.
133
- *
134
- * @param options - Connection options
135
- * @returns The established connection
136
- */
137
- async connectToService(options) {
138
- const { username, service, onConnection } = options;
139
- console.log(`[ConnectionManager] Connecting to ${username}/${service}`);
140
- // Search for the service
141
- const services = await this.api.searchServices(username, service);
142
- if (services.length === 0) {
143
- throw new Error(`Service not found: ${username}/${service}`);
144
- }
145
- // Get the first available service
146
- const serviceInfo = services[0];
147
- console.log(`[ConnectionManager] Found service: ${serviceInfo.uuid}`);
148
- // Get the service details (includes offer)
149
- const serviceDetails = await this.api.getService(serviceInfo.uuid);
150
- const offerId = serviceDetails.offerId;
151
- // Get the offer
152
- const offer = await this.api.getOffer(offerId);
153
- console.log(`[ConnectionManager] Retrieved offer: ${offerId}`);
154
- // Create signaler
155
- const signaler = new RondevuSignaler(this.api, offerId);
156
- const context = new WebRTCContext(signaler);
157
- // Create connection (answerer role)
158
- const connection = new WebRTCRondevuConnection({
159
- id: `client-${service}-${Date.now()}`,
160
- host: username,
161
- service,
162
- offer: {
163
- type: 'offer',
164
- sdp: offer.sdp,
165
- },
166
- context,
167
- });
168
- // Wait for answer to be created
169
- await connection.ready;
170
- // Get the answer SDP
171
- if (!connection.connection) {
172
- throw new Error('RTCPeerConnection not initialized');
173
- }
174
- const answerSdp = connection.connection.localDescription?.sdp;
175
- if (!answerSdp) {
176
- throw new Error('Failed to create answer');
177
- }
178
- console.log(`[ConnectionManager] Answer created`);
179
- // Send answer to server
180
- await this.api.answerOffer(offerId, answerSdp);
181
- console.log(`[ConnectionManager] Answer sent to server`);
182
- // Set up connection tracking
183
- const bin = createBin();
184
- // Track connection state
185
- bin(connection.events.on('state-change', state => {
186
- console.log(`[ConnectionManager] Client ${service} state: ${state}`);
187
- if (state === 'connected' && onConnection) {
188
- onConnection(connection);
189
- }
190
- // Handle disconnection
191
- if (state === 'disconnected' && this.autoReconnect) {
192
- this.scheduleReconnect(connection.id, service, offerId);
193
- }
194
- }));
195
- // Cleanup function
196
- const cleanup = () => bin.clean();
197
- // Store managed connection
198
- this.connections.set(connection.id, {
199
- connection,
200
- offerId,
201
- service,
202
- cleanup,
203
- reconnectAttempts: 0,
204
- isReconnecting: false,
205
- });
206
- return connection;
207
- }
208
- /**
209
- * Get a connection by ID
210
- */
211
- getConnection(id) {
212
- return this.connections.get(id)?.connection;
213
- }
214
- /**
215
- * Get all managed connections
216
- */
217
- getAllConnections() {
218
- return Array.from(this.connections.values()).map(mc => mc.connection);
219
- }
220
- /**
221
- * Remove a connection
222
- */
223
- removeConnection(id) {
224
- const managed = this.connections.get(id);
225
- if (managed) {
226
- managed.cleanup();
227
- this.connections.delete(id);
228
- console.log(`[ConnectionManager] Removed connection: ${id}`);
229
- }
230
- }
231
- /**
232
- * Schedule reconnection for a failed connection
233
- */
234
- scheduleReconnect(connectionId, service, offerId) {
235
- const managed = this.connections.get(connectionId);
236
- if (!managed)
237
- return;
238
- if (managed.isReconnecting) {
239
- console.log(`[ConnectionManager] Already reconnecting: ${connectionId}`);
240
- return;
241
- }
242
- if (managed.reconnectAttempts >= this.maxReconnectAttempts) {
243
- console.log(`[ConnectionManager] Max reconnect attempts reached for ${connectionId}`);
244
- this.removeConnection(connectionId);
245
- return;
246
- }
247
- managed.isReconnecting = true;
248
- managed.reconnectAttempts++;
249
- console.log(`[ConnectionManager] Scheduling reconnect for ${service} (attempt ${managed.reconnectAttempts}/${this.maxReconnectAttempts})`);
250
- setTimeout(() => {
251
- this.attemptReconnect(connectionId, service, offerId);
252
- }, this.reconnectDelay);
253
- }
254
- /**
255
- * Attempt to reconnect a failed connection
256
- */
257
- async attemptReconnect(connectionId, service, offerId) {
258
- const managed = this.connections.get(connectionId);
259
- if (!managed)
260
- return;
261
- try {
262
- console.log(`[ConnectionManager] Attempting reconnect for ${service}`);
263
- // Get fresh offer from server
264
- const offer = await this.api.getOffer(offerId);
265
- // Create new signaler
266
- const signaler = new RondevuSignaler(this.api, offerId);
267
- const context = new WebRTCContext(signaler);
268
- // Create new connection
269
- const newConnection = new WebRTCRondevuConnection({
270
- id: connectionId,
271
- host: managed.connection.host,
272
- service,
273
- offer: {
274
- type: 'offer',
275
- sdp: offer.sdp,
276
- },
277
- context,
278
- });
279
- // Wait for answer
280
- await newConnection.ready;
281
- // Send answer
282
- if (newConnection.connection) {
283
- const answerSdp = newConnection.connection.localDescription?.sdp;
284
- if (answerSdp) {
285
- await this.api.answerOffer(offerId, answerSdp);
286
- }
287
- }
288
- // Replace old connection
289
- managed.connection = newConnection;
290
- managed.isReconnecting = false;
291
- console.log(`[ConnectionManager] Reconnected ${service}`);
292
- }
293
- catch (error) {
294
- console.error(`[ConnectionManager] Reconnect failed:`, error);
295
- managed.isReconnecting = false;
296
- // Schedule next attempt
297
- this.scheduleReconnect(connectionId, service, offerId);
298
- }
299
- }
300
- /**
301
- * Create a temporary signaler (used during initial offer creation)
302
- */
303
- createTempSignaler() {
304
- return {
305
- addIceCandidate: () => { },
306
- addListener: () => () => { },
307
- setOffer: () => { },
308
- setAnswer: () => { },
309
- };
310
- }
311
- /**
312
- * Clean up all connections and resources
313
- */
314
- destroy() {
315
- console.log('[ConnectionManager] Destroying connection manager');
316
- // Clean up all connections
317
- for (const [id, managed] of this.connections.entries()) {
318
- managed.cleanup();
319
- this.connections.delete(id);
320
- }
321
- // Clean up global resources
322
- this.bin.clean();
323
- }
324
- }
@@ -1,112 +0,0 @@
1
- import { ConnectionEvents, ConnectionInterface, Message, QueueMessageOptions } from './types.js';
2
- import { EventBus } from './event-bus.js';
3
- import { WebRTCContext } from './webrtc-context';
4
- export type WebRTCRondevuConnectionOptions = {
5
- id: string;
6
- service: string;
7
- offer: RTCSessionDescriptionInit | null;
8
- context: WebRTCContext;
9
- };
10
- /**
11
- * WebRTCRondevuConnection - WebRTC peer connection wrapper with Rondevu signaling
12
- *
13
- * Manages a WebRTC peer connection lifecycle including:
14
- * - Automatic offer/answer creation based on role
15
- * - ICE candidate exchange via Rondevu signaling server
16
- * - Connection state management with type-safe events
17
- * - Data channel creation and message handling
18
- *
19
- * The connection automatically determines its role (offerer or answerer) based on whether
20
- * an offer is provided in the constructor. The offerer creates the data channel, while
21
- * the answerer receives it via the 'datachannel' event.
22
- *
23
- * @example
24
- * ```typescript
25
- * // Offerer side (creates offer)
26
- * const connection = new WebRTCRondevuConnection(
27
- * 'conn-123',
28
- * 'peer-username',
29
- * 'chat.service@1.0.0'
30
- * );
31
- *
32
- * await connection.ready; // Wait for local offer
33
- * const sdp = connection.connection.localDescription!.sdp!;
34
- * // Send sdp to signaling server...
35
- *
36
- * // Answerer side (receives offer)
37
- * const connection = new WebRTCRondevuConnection(
38
- * 'conn-123',
39
- * 'peer-username',
40
- * 'chat.service@1.0.0',
41
- * { type: 'offer', sdp: remoteOfferSdp }
42
- * );
43
- *
44
- * await connection.ready; // Wait for local answer
45
- * const answerSdp = connection.connection.localDescription!.sdp!;
46
- * // Send answer to signaling server...
47
- *
48
- * // Both sides: Set up signaler and listen for state changes
49
- * connection.setSignaler(signaler);
50
- * connection.events.on('state-change', (state) => {
51
- * console.log('Connection state:', state);
52
- * });
53
- * ```
54
- */
55
- export declare class WebRTCRondevuConnection implements ConnectionInterface {
56
- private readonly side;
57
- readonly expiresAt: number;
58
- readonly lastActive: number;
59
- readonly events: EventBus<ConnectionEvents>;
60
- readonly ready: Promise<void>;
61
- private iceBin;
62
- private ctx;
63
- id: string;
64
- service: string;
65
- private _conn;
66
- private _state;
67
- constructor({ context: ctx, offer, id, service }: WebRTCRondevuConnectionOptions);
68
- /**
69
- * Getter method for retrieving the current connection.
70
- *
71
- * @return {RTCPeerConnection|null} The current connection instance.
72
- */
73
- get connection(): RTCPeerConnection | null;
74
- /**
75
- * Update connection state and emit state-change event
76
- */
77
- private setState;
78
- /**
79
- * Start ICE candidate exchange when gathering begins
80
- */
81
- private startIce;
82
- /**
83
- * Stop ICE candidate exchange when gathering completes
84
- */
85
- private stopIce;
86
- /**
87
- * Disconnects the current connection and cleans up resources.
88
- * Closes the active connection if it exists, resets the connection instance to null,
89
- * stops the ICE process, and updates the state to 'disconnected'.
90
- *
91
- * @return {void} No return value.
92
- */
93
- disconnect(): void;
94
- /**
95
- * Current connection state
96
- */
97
- get state(): "connected" | "disconnected" | "connecting";
98
- /**
99
- * Queue a message for sending when connection is established
100
- *
101
- * @param message - Message to queue (string or ArrayBuffer)
102
- * @param options - Queue options (e.g., expiration time)
103
- */
104
- queueMessage(message: Message, options?: QueueMessageOptions): Promise<void>;
105
- /**
106
- * Send a message immediately
107
- *
108
- * @param message - Message to send (string or ArrayBuffer)
109
- * @returns Promise resolving to true if sent successfully
110
- */
111
- sendMessage(message: Message): Promise<boolean>;
112
- }
@@ -1,194 +0,0 @@
1
- import { isConnectionState, } from './types.js';
2
- import { EventBus } from './event-bus.js';
3
- import { createBin } from './bin.js';
4
- /**
5
- * WebRTCRondevuConnection - WebRTC peer connection wrapper with Rondevu signaling
6
- *
7
- * Manages a WebRTC peer connection lifecycle including:
8
- * - Automatic offer/answer creation based on role
9
- * - ICE candidate exchange via Rondevu signaling server
10
- * - Connection state management with type-safe events
11
- * - Data channel creation and message handling
12
- *
13
- * The connection automatically determines its role (offerer or answerer) based on whether
14
- * an offer is provided in the constructor. The offerer creates the data channel, while
15
- * the answerer receives it via the 'datachannel' event.
16
- *
17
- * @example
18
- * ```typescript
19
- * // Offerer side (creates offer)
20
- * const connection = new WebRTCRondevuConnection(
21
- * 'conn-123',
22
- * 'peer-username',
23
- * 'chat.service@1.0.0'
24
- * );
25
- *
26
- * await connection.ready; // Wait for local offer
27
- * const sdp = connection.connection.localDescription!.sdp!;
28
- * // Send sdp to signaling server...
29
- *
30
- * // Answerer side (receives offer)
31
- * const connection = new WebRTCRondevuConnection(
32
- * 'conn-123',
33
- * 'peer-username',
34
- * 'chat.service@1.0.0',
35
- * { type: 'offer', sdp: remoteOfferSdp }
36
- * );
37
- *
38
- * await connection.ready; // Wait for local answer
39
- * const answerSdp = connection.connection.localDescription!.sdp!;
40
- * // Send answer to signaling server...
41
- *
42
- * // Both sides: Set up signaler and listen for state changes
43
- * connection.setSignaler(signaler);
44
- * connection.events.on('state-change', (state) => {
45
- * console.log('Connection state:', state);
46
- * });
47
- * ```
48
- */
49
- export class WebRTCRondevuConnection {
50
- constructor({ context: ctx, offer, id, service }) {
51
- this.expiresAt = 0;
52
- this.lastActive = 0;
53
- this.events = new EventBus();
54
- this.iceBin = createBin();
55
- this._conn = null;
56
- this._state = 'disconnected';
57
- this.ctx = ctx;
58
- this.id = id;
59
- this.service = service;
60
- this._conn = ctx.createPeerConnection();
61
- this.side = offer ? 'answer' : 'offer';
62
- // setup data channel
63
- if (offer) {
64
- this._conn.addEventListener('datachannel', e => {
65
- const channel = e.channel;
66
- channel.addEventListener('message', e => {
67
- console.log('Message from peer:', e);
68
- });
69
- channel.addEventListener('open', () => {
70
- channel.send('I am ' + this.side);
71
- });
72
- });
73
- }
74
- else {
75
- const channel = this._conn.createDataChannel('vu.ronde.protocol');
76
- channel.addEventListener('message', e => {
77
- console.log('Message from peer:', e);
78
- });
79
- channel.addEventListener('open', () => {
80
- channel.send('I am ' + this.side);
81
- });
82
- }
83
- // setup description exchange
84
- this.ready = offer
85
- ? this._conn
86
- .setRemoteDescription(offer)
87
- .then(() => this._conn?.createAnswer())
88
- .then(async (answer) => {
89
- if (!answer || !this._conn)
90
- throw new Error('Connection disappeared');
91
- await this._conn.setLocalDescription(answer);
92
- return await ctx.signaler.setAnswer(answer);
93
- })
94
- : this._conn.createOffer().then(async (offer) => {
95
- if (!this._conn)
96
- throw new Error('Connection disappeared');
97
- await this._conn.setLocalDescription(offer);
98
- return await ctx.signaler.setOffer(offer);
99
- });
100
- // propagate connection state changes
101
- this._conn.addEventListener('connectionstatechange', () => {
102
- console.log(this.side, 'connection state changed: ', this._conn.connectionState);
103
- const state = isConnectionState(this._conn.connectionState)
104
- ? this._conn.connectionState
105
- : 'disconnected';
106
- this.setState(state);
107
- });
108
- this._conn.addEventListener('iceconnectionstatechange', () => {
109
- console.log(this.side, 'ice connection state changed: ', this._conn.iceConnectionState);
110
- });
111
- // start ICE candidate exchange when gathering begins
112
- this._conn.addEventListener('icegatheringstatechange', () => {
113
- if (this._conn.iceGatheringState === 'gathering') {
114
- this.startIce();
115
- }
116
- else if (this._conn.iceGatheringState === 'complete') {
117
- this.stopIce();
118
- }
119
- });
120
- }
121
- /**
122
- * Getter method for retrieving the current connection.
123
- *
124
- * @return {RTCPeerConnection|null} The current connection instance.
125
- */
126
- get connection() {
127
- return this._conn;
128
- }
129
- /**
130
- * Update connection state and emit state-change event
131
- */
132
- setState(state) {
133
- this._state = state;
134
- this.events.emit('state-change', state);
135
- }
136
- /**
137
- * Start ICE candidate exchange when gathering begins
138
- */
139
- startIce() {
140
- const listener = ({ candidate }) => {
141
- if (candidate)
142
- this.ctx.signaler.addIceCandidate(candidate);
143
- };
144
- if (!this._conn)
145
- throw new Error('Connection disappeared');
146
- this._conn.addEventListener('icecandidate', listener);
147
- this.iceBin(this.ctx.signaler.addListener((candidate) => this._conn?.addIceCandidate(candidate)), () => this._conn?.removeEventListener('icecandidate', listener));
148
- }
149
- /**
150
- * Stop ICE candidate exchange when gathering completes
151
- */
152
- stopIce() {
153
- this.iceBin.clean();
154
- }
155
- /**
156
- * Disconnects the current connection and cleans up resources.
157
- * Closes the active connection if it exists, resets the connection instance to null,
158
- * stops the ICE process, and updates the state to 'disconnected'.
159
- *
160
- * @return {void} No return value.
161
- */
162
- disconnect() {
163
- this._conn?.close();
164
- this._conn = null;
165
- this.stopIce();
166
- this.setState('disconnected');
167
- }
168
- /**
169
- * Current connection state
170
- */
171
- get state() {
172
- return this._state;
173
- }
174
- /**
175
- * Queue a message for sending when connection is established
176
- *
177
- * @param message - Message to queue (string or ArrayBuffer)
178
- * @param options - Queue options (e.g., expiration time)
179
- */
180
- queueMessage(message, options = {}) {
181
- // TODO: Implement message queuing
182
- return Promise.resolve(undefined);
183
- }
184
- /**
185
- * Send a message immediately
186
- *
187
- * @param message - Message to send (string or ArrayBuffer)
188
- * @returns Promise resolving to true if sent successfully
189
- */
190
- sendMessage(message) {
191
- // TODO: Implement message sending via data channel
192
- return Promise.resolve(false);
193
- }
194
- }