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