@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,116 +1,96 @@
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';
3
+ import { RTCDurableConnection } from './durable-connection.js';
4
4
  import { EventBus } from './event-bus.js';
5
- import { createBin } from './bin.js';
6
5
  /**
7
- * ServiceClient - Connects to a hosted service
6
+ * ServiceClient - High-level wrapper for connecting to a WebRTC service
8
7
  *
9
- * Searches for available service offers and establishes a WebRTC connection.
10
- * Optionally supports automatic reconnection on failure.
8
+ * Simplifies client connection by handling:
9
+ * - Service discovery
10
+ * - Offer/answer exchange
11
+ * - ICE candidate polling
12
+ * - Automatic reconnection
11
13
  *
12
14
  * @example
13
15
  * ```typescript
14
- * const rondevuService = new RondevuService({
15
- * apiUrl: 'https://signal.example.com',
16
- * username: 'client-user',
16
+ * const client = new ServiceClient({
17
+ * username: 'host-user',
18
+ * serviceFqn: 'chat.app@1.0.0',
19
+ * rondevuService: myService
17
20
  * })
18
21
  *
19
- * await rondevuService.initialize()
20
- *
21
- * const client = new ServiceClient({
22
- * username: 'host-user',
23
- * serviceFqn: 'chat.app@1.0.0',
24
- * rondevuService,
25
- * autoReconnect: true,
22
+ * client.events.on('connected', conn => {
23
+ * conn.events.on('message', msg => console.log('Received:', msg))
24
+ * conn.sendMessage('Hello from client!')
26
25
  * })
27
26
  *
28
27
  * await client.connect()
29
- *
30
- * client.events.on('connected', (conn) => {
31
- * console.log('Connected to service')
32
- * conn.sendMessage('Hello!')
33
- * })
34
28
  * ```
35
29
  */
36
30
  export class ServiceClient {
37
31
  constructor(options) {
32
+ this.options = options;
33
+ this.signaler = null;
38
34
  this.connection = null;
39
35
  this.reconnectAttempts = 0;
40
- this.reconnectTimeout = null;
41
- this.bin = createBin();
42
36
  this.isConnecting = false;
43
37
  this.events = new EventBus();
44
- this.username = options.username;
45
- this.serviceFqn = options.serviceFqn;
46
- this.rondevuService = options.rondevuService;
47
- this.autoReconnect = options.autoReconnect !== false;
48
- this.reconnectDelay = options.reconnectDelay || 2000;
38
+ this.webrtcContext = new WebRTCContext(options.rtcConfiguration);
39
+ this.autoReconnect = options.autoReconnect !== undefined ? options.autoReconnect : true;
49
40
  this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
50
- this.rtcConfiguration = options.rtcConfiguration;
51
41
  }
52
42
  /**
53
43
  * Connect to the service
54
44
  */
55
45
  async connect() {
56
46
  if (this.isConnecting) {
57
- throw new Error('Already connecting');
47
+ throw new Error('Connection already in progress');
58
48
  }
59
- if (this.connection && this.connection.state === 'connected') {
60
- return this.connection;
49
+ if (this.connection) {
50
+ throw new Error('Already connected. Disconnect first.');
61
51
  }
62
52
  this.isConnecting = true;
63
53
  try {
64
- // Search for available services
65
- const services = await this.rondevuService
66
- .getAPI()
67
- .searchServices(this.username, this.serviceFqn);
68
- if (services.length === 0) {
69
- throw new Error(`No services found for ${this.username}/${this.serviceFqn}`);
70
- }
71
- // Get the first available service
72
- const service = services[0];
73
- // Get service details including SDP
74
- const serviceDetails = await this.rondevuService.getAPI().getService(service.uuid);
75
- // Create WebRTC context with signaler for this offer
76
- const signaler = new RondevuSignaler(this.rondevuService.getAPI(), serviceDetails.offerId);
77
- const context = new WebRTCContext(signaler, this.rtcConfiguration);
78
- // Create connection (answerer role)
79
- const conn = new WebRTCRondevuConnection({
80
- id: `client-${this.serviceFqn}-${Date.now()}`,
81
- service: this.serviceFqn,
82
- offer: {
83
- type: 'offer',
84
- sdp: serviceDetails.sdp,
85
- },
86
- context,
54
+ // Create signaler
55
+ this.signaler = new RondevuSignaler(this.options.rondevuService, this.options.serviceFqn, this.options.username);
56
+ // Wait for remote offer from signaler
57
+ const remoteOffer = await new Promise((resolve, reject) => {
58
+ const timeout = setTimeout(() => {
59
+ reject(new Error('Service discovery timeout'));
60
+ }, 30000);
61
+ this.signaler.addOfferListener((offer) => {
62
+ clearTimeout(timeout);
63
+ resolve(offer);
64
+ });
87
65
  });
88
- // Wait for answer to be created
89
- await conn.ready;
90
- // Get answer SDP
91
- if (!conn.connection?.localDescription?.sdp) {
92
- throw new Error('Failed to create answer SDP');
93
- }
94
- const answerSdp = conn.connection.localDescription.sdp;
95
- // Send answer to server
96
- await this.rondevuService.getAPI().answerOffer(serviceDetails.offerId, answerSdp);
97
- // Track connection
98
- this.connection = conn;
99
- this.reconnectAttempts = 0;
100
- // Listen for state changes
101
- const cleanup = conn.events.on('state-change', state => {
102
- this.handleConnectionStateChange(state);
66
+ // Create connection with remote offer (makes us the answerer)
67
+ const connection = new RTCDurableConnection({
68
+ context: this.webrtcContext,
69
+ signaler: this.signaler,
70
+ offer: remoteOffer
71
+ });
72
+ // Wait for connection to be ready
73
+ await connection.ready;
74
+ // Set up connection event listeners
75
+ connection.events.on('state-change', (state) => {
76
+ if (state === 'connected') {
77
+ this.reconnectAttempts = 0;
78
+ this.events.emit('connected', connection);
79
+ }
80
+ else if (state === 'disconnected') {
81
+ this.events.emit('disconnected', undefined);
82
+ if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
83
+ this.attemptReconnect();
84
+ }
85
+ }
103
86
  });
104
- this.bin(cleanup);
87
+ this.connection = connection;
105
88
  this.isConnecting = false;
106
- // Emit connected event when actually connected
107
- if (conn.state === 'connected') {
108
- this.events.emit('connected', conn);
109
- }
110
- return conn;
89
+ return connection;
111
90
  }
112
- catch (error) {
91
+ catch (err) {
113
92
  this.isConnecting = false;
93
+ const error = err instanceof Error ? err : new Error(String(err));
114
94
  this.events.emit('error', error);
115
95
  throw error;
116
96
  }
@@ -118,69 +98,61 @@ export class ServiceClient {
118
98
  /**
119
99
  * Disconnect from the service
120
100
  */
121
- disconnect() {
122
- if (this.reconnectTimeout) {
123
- clearTimeout(this.reconnectTimeout);
124
- this.reconnectTimeout = null;
101
+ dispose() {
102
+ if (this.signaler) {
103
+ this.signaler.dispose();
104
+ this.signaler = null;
125
105
  }
126
106
  if (this.connection) {
127
107
  this.connection.disconnect();
128
108
  this.connection = null;
129
109
  }
130
- this.bin.clean();
110
+ this.isConnecting = false;
131
111
  this.reconnectAttempts = 0;
132
112
  }
133
113
  /**
134
- * Get the current connection
114
+ * @deprecated Use dispose() instead
135
115
  */
136
- getConnection() {
137
- return this.connection;
138
- }
139
- /**
140
- * Check if currently connected
141
- */
142
- isConnected() {
143
- return this.connection?.state === 'connected';
116
+ disconnect() {
117
+ this.dispose();
144
118
  }
145
119
  /**
146
- * Handle connection state changes
120
+ * Attempt to reconnect
147
121
  */
148
- handleConnectionStateChange(state) {
149
- if (state === 'connected') {
150
- this.events.emit('connected', this.connection);
151
- this.reconnectAttempts = 0;
122
+ async attemptReconnect() {
123
+ this.reconnectAttempts++;
124
+ this.events.emit('reconnecting', {
125
+ attempt: this.reconnectAttempts,
126
+ maxAttempts: this.maxReconnectAttempts
127
+ });
128
+ // Cleanup old connection
129
+ if (this.signaler) {
130
+ this.signaler.dispose();
131
+ this.signaler = null;
132
+ }
133
+ if (this.connection) {
134
+ this.connection = null;
152
135
  }
153
- else if (state === 'disconnected') {
154
- this.events.emit('disconnected', { reason: 'Connection closed' });
155
- // Attempt reconnection if enabled
156
- if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
157
- this.scheduleReconnect();
136
+ // Wait a bit before reconnecting
137
+ await new Promise(resolve => setTimeout(resolve, 1000 * this.reconnectAttempts));
138
+ try {
139
+ await this.connect();
140
+ }
141
+ catch (err) {
142
+ console.error('Reconnection attempt failed:', err);
143
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
144
+ this.attemptReconnect();
145
+ }
146
+ else {
147
+ const error = new Error('Max reconnection attempts reached');
148
+ this.events.emit('error', error);
158
149
  }
159
150
  }
160
151
  }
161
152
  /**
162
- * Schedule a reconnection attempt
153
+ * Get the current connection
163
154
  */
164
- scheduleReconnect() {
165
- if (this.reconnectTimeout) {
166
- return;
167
- }
168
- this.reconnectAttempts++;
169
- this.events.emit('reconnecting', {
170
- attempt: this.reconnectAttempts,
171
- maxAttempts: this.maxReconnectAttempts,
172
- });
173
- // Exponential backoff
174
- const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
175
- this.reconnectTimeout = setTimeout(() => {
176
- this.reconnectTimeout = null;
177
- this.connect().catch(error => {
178
- this.events.emit('error', error);
179
- // Schedule next attempt if we haven't exceeded max attempts
180
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
181
- this.scheduleReconnect();
182
- }
183
- });
184
- }, delay);
155
+ getConnection() {
156
+ return this.connection;
185
157
  }
186
158
  }
@@ -1,103 +1,67 @@
1
- import { WebRTCRondevuConnection } from './connection.js';
2
1
  import { RondevuService } from './rondevu-service.js';
2
+ import { RTCDurableConnection } from './durable-connection.js';
3
3
  import { EventBus } from './event-bus.js';
4
- import { ConnectionInterface } from './types.js';
5
4
  export interface ServiceHostOptions {
6
5
  service: string;
7
6
  rondevuService: RondevuService;
8
7
  maxPeers?: number;
9
8
  ttl?: number;
10
9
  isPublic?: boolean;
11
- metadata?: Record<string, any>;
12
10
  rtcConfiguration?: RTCConfiguration;
11
+ metadata?: Record<string, any>;
13
12
  }
14
13
  export interface ServiceHostEvents {
15
- connection: ConnectionInterface;
16
- 'connection-closed': {
17
- connectionId: string;
18
- reason: string;
19
- };
14
+ connection: RTCDurableConnection;
20
15
  error: Error;
21
16
  }
22
17
  /**
23
- * ServiceHost - Manages a pool of WebRTC offers for a service
18
+ * ServiceHost - High-level wrapper for hosting a WebRTC service
24
19
  *
25
- * Maintains up to maxPeers concurrent offers, automatically replacing
26
- * them when connections are established or expire.
20
+ * Simplifies hosting by handling:
21
+ * - Offer/answer exchange
22
+ * - ICE candidate polling
23
+ * - Connection pool management
24
+ * - Automatic reconnection
27
25
  *
28
26
  * @example
29
27
  * ```typescript
30
- * const rondevuService = new RondevuService({
31
- * apiUrl: 'https://signal.example.com',
32
- * username: 'myusername',
28
+ * const host = new ServiceHost({
29
+ * service: 'chat.app@1.0.0',
30
+ * rondevuService: myService,
31
+ * maxPeers: 5
33
32
  * })
34
33
  *
35
- * await rondevuService.initialize()
36
- * await rondevuService.claimUsername()
37
- *
38
- * const host = new ServiceHost({
39
- * service: 'chat.app@1.0.0',
40
- * rondevuService,
41
- * maxPeers: 5,
34
+ * host.events.on('connection', conn => {
35
+ * conn.events.on('message', msg => console.log('Received:', msg))
36
+ * conn.sendMessage('Hello!')
42
37
  * })
43
38
  *
44
39
  * await host.start()
45
- *
46
- * host.events.on('connection', (conn) => {
47
- * console.log('New connection:', conn.id)
48
- * conn.events.on('message', (msg) => {
49
- * console.log('Message:', msg)
50
- * })
51
- * })
52
40
  * ```
53
41
  */
54
42
  export declare class ServiceHost {
43
+ private options;
44
+ events: EventBus<ServiceHostEvents>;
45
+ private signaler;
46
+ private webrtcContext;
55
47
  private connections;
56
- private readonly service;
57
- private readonly rondevuService;
58
- private readonly maxPeers;
59
- private readonly ttl;
60
- private readonly isPublic;
61
- private readonly metadata?;
62
- private readonly rtcConfiguration?;
63
- private readonly bin;
64
- private isStarted;
65
- readonly events: EventBus<ServiceHostEvents>;
48
+ private maxPeers;
49
+ private running;
66
50
  constructor(options: ServiceHostOptions);
67
51
  /**
68
- * Start hosting the service - creates initial pool of offers
52
+ * Start hosting the service
69
53
  */
70
54
  start(): Promise<void>;
71
55
  /**
72
- * Stop hosting - closes all connections and cleans up
73
- */
74
- stop(): void;
75
- /**
76
- * Get current number of active connections
56
+ * Create the next connection for incoming peers
77
57
  */
78
- getConnectionCount(): number;
58
+ private createNextConnection;
79
59
  /**
80
- * Get current number of pending offers
60
+ * Stop hosting the service
81
61
  */
82
- getPendingOfferCount(): number;
83
- /**
84
- * Fill the offer pool up to maxPeers
85
- */
86
- private fillOfferPool;
87
- /**
88
- * Create a single offer and publish it
89
- */
90
- private createOffer;
91
- /**
92
- * Handle connection state changes
93
- */
94
- private handleConnectionStateChange;
62
+ dispose(): void;
95
63
  /**
96
64
  * Get all active connections
97
65
  */
98
- getConnections(): WebRTCRondevuConnection[];
99
- /**
100
- * Get a specific connection by ID
101
- */
102
- getConnection(connectionId: string): WebRTCRondevuConnection | undefined;
66
+ getConnections(): RTCDurableConnection[];
103
67
  }