@xtr-dev/rondevu-client 0.10.1 → 0.11.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,186 +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;
51
- this.rtcConfiguration = options.rtcConfiguration;
37
+ this.webrtcContext = new WebRTCContext(options.rtcConfiguration);
38
+ this.maxPeers = options.maxPeers || 5;
52
39
  }
53
40
  /**
54
- * Start hosting the service - creates initial pool of offers
41
+ * Start hosting the service
55
42
  */
56
43
  async start() {
57
- if (this.isStarted) {
58
- throw new Error('ServiceHost already started');
44
+ if (this.running) {
45
+ throw new Error('ServiceHost already running');
59
46
  }
60
- this.isStarted = true;
61
- await this.fillOfferPool();
62
- }
63
- /**
64
- * Stop hosting - closes all connections and cleans up
65
- */
66
- stop() {
67
- this.isStarted = false;
68
- this.connections.forEach(conn => conn.disconnect());
69
- this.connections.clear();
70
- this.bin.clean();
71
- }
72
- /**
73
- * Get current number of active connections
74
- */
75
- getConnectionCount() {
76
- return Array.from(this.connections.values()).filter(conn => conn.state === 'connected')
77
- .length;
78
- }
79
- /**
80
- * Get current number of pending offers
81
- */
82
- getPendingOfferCount() {
83
- return Array.from(this.connections.values()).filter(conn => conn.state === 'connecting')
84
- .length;
85
- }
86
- /**
87
- * Fill the offer pool up to maxPeers
88
- */
89
- async fillOfferPool() {
90
- const currentOffers = this.connections.size;
91
- const needed = this.maxPeers - currentOffers;
92
- if (needed <= 0) {
93
- return;
94
- }
95
- // Create multiple offers in parallel
96
- const offerPromises = [];
97
- for (let i = 0; i < needed; i++) {
98
- 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');
99
83
  }
100
- await Promise.allSettled(offerPromises);
84
+ await this.signaler.setOffer(offer);
101
85
  }
102
86
  /**
103
- * Create a single offer and publish it
87
+ * Create the next connection for incoming peers
104
88
  */
105
- async createOffer() {
106
- try {
107
- // Create temporary context with NoOp signaler
108
- const tempContext = new WebRTCContext(new NoOpSignaler(), this.rtcConfiguration);
109
- // Create connection (offerer role)
110
- const conn = new WebRTCRondevuConnection({
111
- id: `${this.service}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
112
- service: this.service,
113
- offer: null,
114
- context: tempContext,
115
- });
116
- // Wait for offer to be created
117
- await conn.ready;
118
- // Get offer SDP
119
- if (!conn.connection?.localDescription?.sdp) {
120
- throw new Error('Failed to create offer SDP');
121
- }
122
- const sdp = conn.connection.localDescription.sdp;
123
- // Publish service offer
124
- const service = await this.rondevuService.publishService({
125
- serviceFqn: this.service,
126
- sdp,
127
- ttl: this.ttl,
128
- isPublic: this.isPublic,
129
- metadata: this.metadata,
130
- });
131
- // Replace with real signaler now that we have offerId
132
- const realSignaler = new RondevuSignaler(this.rondevuService.getAPI(), service.offerId);
133
- tempContext.signaler = realSignaler;
134
- // Track connection
135
- this.connections.set(conn.id, conn);
136
- // Listen for state changes
137
- const cleanup = conn.events.on('state-change', state => {
138
- this.handleConnectionStateChange(conn, state);
139
- });
140
- this.bin(cleanup);
141
- }
142
- catch (error) {
143
- this.events.emit('error', error);
89
+ async createNextConnection() {
90
+ if (!this.signaler || !this.running) {
91
+ return;
144
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
145
97
  }
146
98
  /**
147
- * Handle connection state changes
99
+ * Stop hosting the service
148
100
  */
149
- handleConnectionStateChange(conn, state) {
150
- if (state === 'connected') {
151
- // Connection established - emit event
152
- this.events.emit('connection', conn);
153
- // Create new offer to replace this one
154
- if (this.isStarted) {
155
- this.fillOfferPool().catch(error => {
156
- this.events.emit('error', error);
157
- });
158
- }
101
+ dispose() {
102
+ this.running = false;
103
+ // Cleanup signaler
104
+ if (this.signaler) {
105
+ this.signaler.dispose();
106
+ this.signaler = null;
159
107
  }
160
- else if (state === 'disconnected') {
161
- // Connection closed - remove and create new offer
162
- this.connections.delete(conn.id);
163
- this.events.emit('connection-closed', {
164
- connectionId: conn.id,
165
- reason: state,
166
- });
167
- if (this.isStarted) {
168
- this.fillOfferPool().catch(error => {
169
- this.events.emit('error', error);
170
- });
171
- }
108
+ // Disconnect all connections
109
+ for (const conn of this.connections) {
110
+ conn.disconnect();
172
111
  }
112
+ this.connections = [];
173
113
  }
174
114
  /**
175
115
  * Get all active connections
176
116
  */
177
117
  getConnections() {
178
- return Array.from(this.connections.values());
179
- }
180
- /**
181
- * Get a specific connection by ID
182
- */
183
- getConnection(connectionId) {
184
- return this.connections.get(connectionId);
118
+ return [...this.connections];
185
119
  }
186
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,7 +1,5 @@
1
- import { Signaler } from './types';
2
1
  export declare class WebRTCContext {
3
- readonly signaler: Signaler;
4
2
  private readonly config?;
5
- constructor(signaler: Signaler, config?: RTCConfiguration | undefined);
3
+ constructor(config?: RTCConfiguration | undefined);
6
4
  createPeerConnection(): RTCPeerConnection;
7
5
  }
@@ -26,8 +26,7 @@ const DEFAULT_RTC_CONFIGURATION = {
26
26
  ],
27
27
  };
28
28
  export class WebRTCContext {
29
- constructor(signaler, config) {
30
- this.signaler = signaler;
29
+ constructor(config) {
31
30
  this.config = config;
32
31
  }
33
32
  createPeerConnection() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
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",