@xtr-dev/rondevu-client 0.9.2 → 0.10.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.
- package/dist/api.d.ts +147 -0
- package/dist/api.js +307 -0
- package/dist/bin.d.ts +35 -0
- package/dist/bin.js +35 -0
- package/dist/connection-manager.d.ts +104 -0
- package/dist/connection-manager.js +324 -0
- package/dist/connection.d.ts +112 -0
- package/dist/connection.js +194 -0
- package/dist/event-bus.d.ts +52 -0
- package/dist/event-bus.js +84 -0
- package/dist/index.d.ts +15 -11
- package/dist/index.js +9 -11
- package/dist/noop-signaler.d.ts +14 -0
- package/dist/noop-signaler.js +27 -0
- package/dist/rondevu-service.d.ts +81 -0
- package/dist/rondevu-service.js +131 -0
- package/dist/service-client.d.ts +92 -0
- package/dist/service-client.js +185 -0
- package/dist/service-host.d.ts +101 -0
- package/dist/service-host.js +185 -0
- package/dist/signaler.d.ts +25 -0
- package/dist/signaler.js +89 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +2 -0
- package/dist/webrtc-context.d.ts +6 -0
- package/dist/webrtc-context.js +34 -0
- package/package.json +16 -2
- package/dist/auth.d.ts +0 -20
- package/dist/auth.js +0 -41
- package/dist/durable/channel.d.ts +0 -115
- package/dist/durable/channel.js +0 -301
- package/dist/durable/connection.d.ts +0 -125
- package/dist/durable/connection.js +0 -370
- package/dist/durable/reconnection.d.ts +0 -90
- package/dist/durable/reconnection.js +0 -127
- package/dist/durable/service.d.ts +0 -103
- package/dist/durable/service.js +0 -264
- package/dist/durable/types.d.ts +0 -149
- package/dist/durable/types.js +0 -28
- package/dist/event-emitter.d.ts +0 -54
- package/dist/event-emitter.js +0 -102
- package/dist/offer-pool.d.ts +0 -86
- package/dist/offer-pool.js +0 -145
- package/dist/offers.d.ts +0 -101
- package/dist/offers.js +0 -202
- package/dist/peer/answering-state.d.ts +0 -11
- package/dist/peer/answering-state.js +0 -39
- package/dist/peer/closed-state.d.ts +0 -8
- package/dist/peer/closed-state.js +0 -10
- package/dist/peer/connected-state.d.ts +0 -8
- package/dist/peer/connected-state.js +0 -11
- package/dist/peer/creating-offer-state.d.ts +0 -12
- package/dist/peer/creating-offer-state.js +0 -45
- package/dist/peer/exchanging-ice-state.d.ts +0 -17
- package/dist/peer/exchanging-ice-state.js +0 -64
- package/dist/peer/failed-state.d.ts +0 -10
- package/dist/peer/failed-state.js +0 -16
- package/dist/peer/idle-state.d.ts +0 -7
- package/dist/peer/idle-state.js +0 -14
- package/dist/peer/index.d.ts +0 -71
- package/dist/peer/index.js +0 -176
- package/dist/peer/state.d.ts +0 -23
- package/dist/peer/state.js +0 -63
- package/dist/peer/types.d.ts +0 -43
- package/dist/peer/types.js +0 -1
- package/dist/peer/waiting-for-answer-state.d.ts +0 -17
- package/dist/peer/waiting-for-answer-state.js +0 -60
- package/dist/rondevu.d.ts +0 -184
- package/dist/rondevu.js +0 -171
- package/dist/service-pool.d.ts +0 -123
- package/dist/service-pool.js +0 -488
- package/dist/usernames.d.ts +0 -79
- package/dist/usernames.js +0 -153
|
@@ -0,0 +1,324 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe EventBus with event name to payload type mapping
|
|
3
|
+
*/
|
|
4
|
+
type EventHandler<T = any> = (data: T) => void;
|
|
5
|
+
/**
|
|
6
|
+
* EventBus - Type-safe event emitter with inferred event data types
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* interface MyEvents {
|
|
10
|
+
* 'user:connected': { userId: string; timestamp: number };
|
|
11
|
+
* 'user:disconnected': { userId: string };
|
|
12
|
+
* 'message:received': string;
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* const bus = new EventBus<MyEvents>();
|
|
16
|
+
*
|
|
17
|
+
* // TypeScript knows data is { userId: string; timestamp: number }
|
|
18
|
+
* bus.on('user:connected', (data) => {
|
|
19
|
+
* console.log(data.userId, data.timestamp);
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // TypeScript knows data is string
|
|
23
|
+
* bus.on('message:received', (data) => {
|
|
24
|
+
* console.log(data.toUpperCase());
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export declare class EventBus<TEvents extends Record<string, any>> {
|
|
28
|
+
private handlers;
|
|
29
|
+
constructor();
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to an event
|
|
32
|
+
* Returns a cleanup function to unsubscribe
|
|
33
|
+
*/
|
|
34
|
+
on<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Subscribe to an event once (auto-unsubscribe after first call)
|
|
37
|
+
*/
|
|
38
|
+
once<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void;
|
|
39
|
+
/**
|
|
40
|
+
* Unsubscribe from an event
|
|
41
|
+
*/
|
|
42
|
+
off<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void;
|
|
43
|
+
/**
|
|
44
|
+
* Emit an event with data
|
|
45
|
+
*/
|
|
46
|
+
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void;
|
|
47
|
+
/**
|
|
48
|
+
* Remove all handlers for a specific event, or all handlers if no event specified
|
|
49
|
+
*/
|
|
50
|
+
clear<K extends keyof TEvents>(event?: K): void;
|
|
51
|
+
}
|
|
52
|
+
export {};
|