@xtr-dev/rondevu-client 0.10.0 → 0.10.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.
@@ -1,185 +1,120 @@
1
- import { WebRTCRondevuConnection } from './connection.js';
1
+ import { RondevuSignaler } from './rondevu-signaler.js';
2
2
  import { WebRTCContext } from './webrtc-context.js';
3
- import { RondevuSignaler } from './signaler.js';
4
- import { NoOpSignaler } from './noop-signaler.js';
3
+ import { RTCDurableConnection } from './durable-connection.js';
5
4
  import { EventBus } from './event-bus.js';
6
- import { createBin } from './bin.js';
7
5
  /**
8
- * ServiceHost - Manages a pool of WebRTC offers for a service
6
+ * ServiceHost - High-level wrapper for hosting a WebRTC service
9
7
  *
10
- * Maintains up to maxPeers concurrent offers, automatically replacing
11
- * them when connections are established or expire.
8
+ * Simplifies hosting by handling:
9
+ * - Offer/answer exchange
10
+ * - ICE candidate polling
11
+ * - Connection pool management
12
+ * - Automatic reconnection
12
13
  *
13
14
  * @example
14
15
  * ```typescript
15
- * const rondevuService = new RondevuService({
16
- * apiUrl: 'https://signal.example.com',
17
- * username: 'myusername',
16
+ * const host = new ServiceHost({
17
+ * service: 'chat.app@1.0.0',
18
+ * rondevuService: myService,
19
+ * maxPeers: 5
18
20
  * })
19
21
  *
20
- * await rondevuService.initialize()
21
- * await rondevuService.claimUsername()
22
- *
23
- * const host = new ServiceHost({
24
- * service: 'chat.app@1.0.0',
25
- * rondevuService,
26
- * maxPeers: 5,
22
+ * host.events.on('connection', conn => {
23
+ * conn.events.on('message', msg => console.log('Received:', msg))
24
+ * conn.sendMessage('Hello!')
27
25
  * })
28
26
  *
29
27
  * await host.start()
30
- *
31
- * host.events.on('connection', (conn) => {
32
- * console.log('New connection:', conn.id)
33
- * conn.events.on('message', (msg) => {
34
- * console.log('Message:', msg)
35
- * })
36
- * })
37
28
  * ```
38
29
  */
39
30
  export class ServiceHost {
40
31
  constructor(options) {
41
- this.connections = new Map();
42
- this.bin = createBin();
43
- this.isStarted = false;
32
+ this.options = options;
33
+ this.signaler = null;
34
+ this.connections = [];
35
+ this.running = false;
44
36
  this.events = new EventBus();
45
- this.service = options.service;
46
- this.rondevuService = options.rondevuService;
47
- this.maxPeers = options.maxPeers || 20;
48
- this.ttl = options.ttl || 300000;
49
- this.isPublic = options.isPublic !== false;
50
- this.metadata = options.metadata;
37
+ this.webrtcContext = new WebRTCContext(options.rtcConfiguration);
38
+ this.maxPeers = options.maxPeers || 5;
51
39
  }
52
40
  /**
53
- * Start hosting the service - creates initial pool of offers
41
+ * Start hosting the service
54
42
  */
55
43
  async start() {
56
- if (this.isStarted) {
57
- throw new Error('ServiceHost already started');
44
+ if (this.running) {
45
+ throw new Error('ServiceHost already running');
58
46
  }
59
- this.isStarted = true;
60
- await this.fillOfferPool();
61
- }
62
- /**
63
- * Stop hosting - closes all connections and cleans up
64
- */
65
- stop() {
66
- this.isStarted = false;
67
- this.connections.forEach(conn => conn.disconnect());
68
- this.connections.clear();
69
- this.bin.clean();
70
- }
71
- /**
72
- * Get current number of active connections
73
- */
74
- getConnectionCount() {
75
- return Array.from(this.connections.values()).filter(conn => conn.state === 'connected')
76
- .length;
77
- }
78
- /**
79
- * Get current number of pending offers
80
- */
81
- getPendingOfferCount() {
82
- return Array.from(this.connections.values()).filter(conn => conn.state === 'connecting')
83
- .length;
84
- }
85
- /**
86
- * Fill the offer pool up to maxPeers
87
- */
88
- async fillOfferPool() {
89
- const currentOffers = this.connections.size;
90
- const needed = this.maxPeers - currentOffers;
91
- if (needed <= 0) {
92
- return;
93
- }
94
- // Create multiple offers in parallel
95
- const offerPromises = [];
96
- for (let i = 0; i < needed; i++) {
97
- offerPromises.push(this.createOffer());
47
+ this.running = true;
48
+ // Create signaler
49
+ this.signaler = new RondevuSignaler(this.options.rondevuService, this.options.service);
50
+ // Create first connection (offerer)
51
+ const connection = new RTCDurableConnection({
52
+ context: this.webrtcContext,
53
+ signaler: this.signaler,
54
+ offer: null // null means we're the offerer
55
+ });
56
+ // Wait for connection to be ready
57
+ await connection.ready;
58
+ // Set up connection event listeners
59
+ connection.events.on('state-change', (state) => {
60
+ if (state === 'connected') {
61
+ this.connections.push(connection);
62
+ this.events.emit('connection', connection);
63
+ // Create next connection if under maxPeers
64
+ if (this.connections.length < this.maxPeers) {
65
+ this.createNextConnection().catch(err => {
66
+ console.error('Failed to create next connection:', err);
67
+ this.events.emit('error', err);
68
+ });
69
+ }
70
+ }
71
+ else if (state === 'disconnected') {
72
+ // Remove from connections list
73
+ const index = this.connections.indexOf(connection);
74
+ if (index > -1) {
75
+ this.connections.splice(index, 1);
76
+ }
77
+ }
78
+ });
79
+ // Publish service with the offer
80
+ const offer = connection.connection?.localDescription;
81
+ if (!offer?.sdp) {
82
+ throw new Error('Offer SDP is empty');
98
83
  }
99
- await Promise.allSettled(offerPromises);
84
+ await this.signaler.setOffer(offer);
100
85
  }
101
86
  /**
102
- * Create a single offer and publish it
87
+ * Create the next connection for incoming peers
103
88
  */
104
- async createOffer() {
105
- try {
106
- // Create temporary context with NoOp signaler
107
- const tempContext = new WebRTCContext(new NoOpSignaler());
108
- // Create connection (offerer role)
109
- const conn = new WebRTCRondevuConnection({
110
- id: `${this.service}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
111
- service: this.service,
112
- offer: null,
113
- context: tempContext,
114
- });
115
- // Wait for offer to be created
116
- await conn.ready;
117
- // Get offer SDP
118
- if (!conn.connection?.localDescription?.sdp) {
119
- throw new Error('Failed to create offer SDP');
120
- }
121
- const sdp = conn.connection.localDescription.sdp;
122
- // Publish service offer
123
- const service = await this.rondevuService.publishService({
124
- serviceFqn: this.service,
125
- sdp,
126
- ttl: this.ttl,
127
- isPublic: this.isPublic,
128
- metadata: this.metadata,
129
- });
130
- // Replace with real signaler now that we have offerId
131
- const realSignaler = new RondevuSignaler(this.rondevuService.getAPI(), service.offerId);
132
- tempContext.signaler = realSignaler;
133
- // Track connection
134
- this.connections.set(conn.id, conn);
135
- // Listen for state changes
136
- const cleanup = conn.events.on('state-change', state => {
137
- this.handleConnectionStateChange(conn, state);
138
- });
139
- this.bin(cleanup);
140
- }
141
- catch (error) {
142
- this.events.emit('error', error);
89
+ async createNextConnection() {
90
+ if (!this.signaler || !this.running) {
91
+ return;
143
92
  }
93
+ // For now, we'll use the same offer for all connections
94
+ // In a production scenario, you'd create multiple offers
95
+ // This is a limitation of the current service model
96
+ // which publishes one offer per service
144
97
  }
145
98
  /**
146
- * Handle connection state changes
99
+ * Stop hosting the service
147
100
  */
148
- handleConnectionStateChange(conn, state) {
149
- if (state === 'connected') {
150
- // Connection established - emit event
151
- this.events.emit('connection', conn);
152
- // Create new offer to replace this one
153
- if (this.isStarted) {
154
- this.fillOfferPool().catch(error => {
155
- this.events.emit('error', error);
156
- });
157
- }
101
+ dispose() {
102
+ this.running = false;
103
+ // Cleanup signaler
104
+ if (this.signaler) {
105
+ this.signaler.dispose();
106
+ this.signaler = null;
158
107
  }
159
- else if (state === 'disconnected') {
160
- // Connection closed - remove and create new offer
161
- this.connections.delete(conn.id);
162
- this.events.emit('connection-closed', {
163
- connectionId: conn.id,
164
- reason: state,
165
- });
166
- if (this.isStarted) {
167
- this.fillOfferPool().catch(error => {
168
- this.events.emit('error', error);
169
- });
170
- }
108
+ // Disconnect all connections
109
+ for (const conn of this.connections) {
110
+ conn.disconnect();
171
111
  }
112
+ this.connections = [];
172
113
  }
173
114
  /**
174
115
  * Get all active connections
175
116
  */
176
117
  getConnections() {
177
- return Array.from(this.connections.values());
178
- }
179
- /**
180
- * Get a specific connection by ID
181
- */
182
- getConnection(connectionId) {
183
- return this.connections.get(connectionId);
118
+ return [...this.connections];
184
119
  }
185
120
  }
package/dist/types.d.ts CHANGED
@@ -14,8 +14,6 @@ export interface ConnectionEvents {
14
14
  export declare const ConnectionStates: readonly ["connected", "disconnected", "connecting"];
15
15
  export declare const isConnectionState: (state: string) => state is (typeof ConnectionStates)[number];
16
16
  export interface ConnectionInterface {
17
- id: string;
18
- service: string;
19
17
  state: (typeof ConnectionStates)[number];
20
18
  lastActive: number;
21
19
  expiresAt?: number;
@@ -24,7 +22,7 @@ export interface ConnectionInterface {
24
22
  sendMessage(message: Message): Promise<boolean>;
25
23
  }
26
24
  export interface Signaler {
27
- addIceCandidate(candidate: RTCIceCandidate): Promise<void> | void;
25
+ addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
28
26
  addListener(callback: (candidate: RTCIceCandidate) => void): Binnable;
29
27
  addOfferListener(callback: (offer: RTCSessionDescriptionInit) => void): Binnable;
30
28
  addAnswerListener(callback: (answer: RTCSessionDescriptionInit) => void): Binnable;
package/dist/types.js CHANGED
@@ -1,2 +1,6 @@
1
- export const ConnectionStates = ['connected', 'disconnected', 'connecting'];
1
+ export const ConnectionStates = [
2
+ 'connected',
3
+ 'disconnected',
4
+ 'connecting'
5
+ ];
2
6
  export const isConnectionState = (state) => ConnectionStates.includes(state);
@@ -1,6 +1,5 @@
1
- import { Signaler } from './types';
2
1
  export declare class WebRTCContext {
3
- readonly signaler: Signaler;
4
- constructor(signaler: Signaler);
2
+ private readonly config?;
3
+ constructor(config?: RTCConfiguration | undefined);
5
4
  createPeerConnection(): RTCPeerConnection;
6
5
  }
@@ -1,34 +1,35 @@
1
+ const DEFAULT_RTC_CONFIGURATION = {
2
+ iceServers: [
3
+ {
4
+ urls: 'stun:stun.relay.metered.ca:80',
5
+ },
6
+ {
7
+ urls: 'turn:standard.relay.metered.ca:80',
8
+ username: 'c53a9c971da5e6f3bc959d8d',
9
+ credential: 'QaccPqtPPaxyokXp',
10
+ },
11
+ {
12
+ urls: 'turn:standard.relay.metered.ca:80?transport=tcp',
13
+ username: 'c53a9c971da5e6f3bc959d8d',
14
+ credential: 'QaccPqtPPaxyokXp',
15
+ },
16
+ {
17
+ urls: 'turn:standard.relay.metered.ca:443',
18
+ username: 'c53a9c971da5e6f3bc959d8d',
19
+ credential: 'QaccPqtPPaxyokXp',
20
+ },
21
+ {
22
+ urls: 'turns:standard.relay.metered.ca:443?transport=tcp',
23
+ username: 'c53a9c971da5e6f3bc959d8d',
24
+ credential: 'QaccPqtPPaxyokXp',
25
+ },
26
+ ],
27
+ };
1
28
  export class WebRTCContext {
2
- constructor(signaler) {
3
- this.signaler = signaler;
29
+ constructor(config) {
30
+ this.config = config;
4
31
  }
5
32
  createPeerConnection() {
6
- return new RTCPeerConnection({
7
- iceServers: [
8
- {
9
- urls: 'stun:stun.relay.metered.ca:80',
10
- },
11
- {
12
- urls: 'turn:standard.relay.metered.ca:80',
13
- username: 'c53a9c971da5e6f3bc959d8d',
14
- credential: 'QaccPqtPPaxyokXp',
15
- },
16
- {
17
- urls: 'turn:standard.relay.metered.ca:80?transport=tcp',
18
- username: 'c53a9c971da5e6f3bc959d8d',
19
- credential: 'QaccPqtPPaxyokXp',
20
- },
21
- {
22
- urls: 'turn:standard.relay.metered.ca:443',
23
- username: 'c53a9c971da5e6f3bc959d8d',
24
- credential: 'QaccPqtPPaxyokXp',
25
- },
26
- {
27
- urls: 'turns:standard.relay.metered.ca:443?transport=tcp',
28
- username: 'c53a9c971da5e6f3bc959d8d',
29
- credential: 'QaccPqtPPaxyokXp',
30
- },
31
- ],
32
- });
33
+ return new RTCPeerConnection(this.config || DEFAULT_RTC_CONFIGURATION);
33
34
  }
34
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.10.0",
3
+ "version": "0.10.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",