@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.
Files changed (73) hide show
  1. package/dist/api.d.ts +147 -0
  2. package/dist/api.js +307 -0
  3. package/dist/bin.d.ts +35 -0
  4. package/dist/bin.js +35 -0
  5. package/dist/connection-manager.d.ts +104 -0
  6. package/dist/connection-manager.js +324 -0
  7. package/dist/connection.d.ts +112 -0
  8. package/dist/connection.js +194 -0
  9. package/dist/event-bus.d.ts +52 -0
  10. package/dist/event-bus.js +84 -0
  11. package/dist/index.d.ts +15 -11
  12. package/dist/index.js +9 -11
  13. package/dist/noop-signaler.d.ts +14 -0
  14. package/dist/noop-signaler.js +27 -0
  15. package/dist/rondevu-service.d.ts +81 -0
  16. package/dist/rondevu-service.js +131 -0
  17. package/dist/service-client.d.ts +92 -0
  18. package/dist/service-client.js +185 -0
  19. package/dist/service-host.d.ts +101 -0
  20. package/dist/service-host.js +185 -0
  21. package/dist/signaler.d.ts +25 -0
  22. package/dist/signaler.js +89 -0
  23. package/dist/types.d.ts +33 -0
  24. package/dist/types.js +2 -0
  25. package/dist/webrtc-context.d.ts +6 -0
  26. package/dist/webrtc-context.js +34 -0
  27. package/package.json +16 -2
  28. package/dist/auth.d.ts +0 -20
  29. package/dist/auth.js +0 -41
  30. package/dist/durable/channel.d.ts +0 -115
  31. package/dist/durable/channel.js +0 -301
  32. package/dist/durable/connection.d.ts +0 -125
  33. package/dist/durable/connection.js +0 -370
  34. package/dist/durable/reconnection.d.ts +0 -90
  35. package/dist/durable/reconnection.js +0 -127
  36. package/dist/durable/service.d.ts +0 -103
  37. package/dist/durable/service.js +0 -264
  38. package/dist/durable/types.d.ts +0 -149
  39. package/dist/durable/types.js +0 -28
  40. package/dist/event-emitter.d.ts +0 -54
  41. package/dist/event-emitter.js +0 -102
  42. package/dist/offer-pool.d.ts +0 -86
  43. package/dist/offer-pool.js +0 -145
  44. package/dist/offers.d.ts +0 -101
  45. package/dist/offers.js +0 -202
  46. package/dist/peer/answering-state.d.ts +0 -11
  47. package/dist/peer/answering-state.js +0 -39
  48. package/dist/peer/closed-state.d.ts +0 -8
  49. package/dist/peer/closed-state.js +0 -10
  50. package/dist/peer/connected-state.d.ts +0 -8
  51. package/dist/peer/connected-state.js +0 -11
  52. package/dist/peer/creating-offer-state.d.ts +0 -12
  53. package/dist/peer/creating-offer-state.js +0 -45
  54. package/dist/peer/exchanging-ice-state.d.ts +0 -17
  55. package/dist/peer/exchanging-ice-state.js +0 -64
  56. package/dist/peer/failed-state.d.ts +0 -10
  57. package/dist/peer/failed-state.js +0 -16
  58. package/dist/peer/idle-state.d.ts +0 -7
  59. package/dist/peer/idle-state.js +0 -14
  60. package/dist/peer/index.d.ts +0 -71
  61. package/dist/peer/index.js +0 -176
  62. package/dist/peer/state.d.ts +0 -23
  63. package/dist/peer/state.js +0 -63
  64. package/dist/peer/types.d.ts +0 -43
  65. package/dist/peer/types.js +0 -1
  66. package/dist/peer/waiting-for-answer-state.d.ts +0 -17
  67. package/dist/peer/waiting-for-answer-state.js +0 -60
  68. package/dist/rondevu.d.ts +0 -184
  69. package/dist/rondevu.js +0 -171
  70. package/dist/service-pool.d.ts +0 -123
  71. package/dist/service-pool.js +0 -488
  72. package/dist/usernames.d.ts +0 -79
  73. package/dist/usernames.js +0 -153
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Type-safe EventBus with event name to payload type mapping
3
+ */
4
+ /**
5
+ * EventBus - Type-safe event emitter with inferred event data types
6
+ *
7
+ * @example
8
+ * interface MyEvents {
9
+ * 'user:connected': { userId: string; timestamp: number };
10
+ * 'user:disconnected': { userId: string };
11
+ * 'message:received': string;
12
+ * }
13
+ *
14
+ * const bus = new EventBus<MyEvents>();
15
+ *
16
+ * // TypeScript knows data is { userId: string; timestamp: number }
17
+ * bus.on('user:connected', (data) => {
18
+ * console.log(data.userId, data.timestamp);
19
+ * });
20
+ *
21
+ * // TypeScript knows data is string
22
+ * bus.on('message:received', (data) => {
23
+ * console.log(data.toUpperCase());
24
+ * });
25
+ */
26
+ export class EventBus {
27
+ constructor() {
28
+ this.handlers = new Map();
29
+ }
30
+ /**
31
+ * Subscribe to an event
32
+ * Returns a cleanup function to unsubscribe
33
+ */
34
+ on(event, handler) {
35
+ if (!this.handlers.has(event)) {
36
+ this.handlers.set(event, new Set());
37
+ }
38
+ this.handlers.get(event).add(handler);
39
+ // Return cleanup function
40
+ return () => this.off(event, handler);
41
+ }
42
+ /**
43
+ * Subscribe to an event once (auto-unsubscribe after first call)
44
+ */
45
+ once(event, handler) {
46
+ const wrappedHandler = (data) => {
47
+ handler(data);
48
+ this.off(event, wrappedHandler);
49
+ };
50
+ this.on(event, wrappedHandler);
51
+ }
52
+ /**
53
+ * Unsubscribe from an event
54
+ */
55
+ off(event, handler) {
56
+ const eventHandlers = this.handlers.get(event);
57
+ if (eventHandlers) {
58
+ eventHandlers.delete(handler);
59
+ if (eventHandlers.size === 0) {
60
+ this.handlers.delete(event);
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * Emit an event with data
66
+ */
67
+ emit(event, data) {
68
+ const eventHandlers = this.handlers.get(event);
69
+ if (eventHandlers) {
70
+ eventHandlers.forEach(handler => handler(data));
71
+ }
72
+ }
73
+ /**
74
+ * Remove all handlers for a specific event, or all handlers if no event specified
75
+ */
76
+ clear(event) {
77
+ if (event !== undefined) {
78
+ this.handlers.delete(event);
79
+ }
80
+ else {
81
+ this.handlers.clear();
82
+ }
83
+ }
84
+ }
package/dist/index.d.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  /**
2
2
  * @xtr-dev/rondevu-client
3
- * WebRTC peer signaling and discovery client with durable connections
3
+ * WebRTC peer signaling client
4
4
  */
5
- export { Rondevu } from './rondevu.js';
6
- export type { RondevuOptions } from './rondevu.js';
7
- export { RondevuAuth } from './auth.js';
8
- export type { Credentials, FetchFunction } from './auth.js';
9
- export { RondevuUsername } from './usernames.js';
10
- export type { UsernameClaimResult, UsernameCheckResult } from './usernames.js';
11
- export { DurableConnection } from './durable/connection.js';
12
- export { DurableChannel } from './durable/channel.js';
13
- export { DurableService } from './durable/service.js';
14
- export type { DurableConnectionState, DurableChannelState, DurableConnectionConfig, DurableChannelConfig, DurableServiceConfig, QueuedMessage, DurableConnectionEvents, DurableChannelEvents, DurableServiceEvents, ConnectionInfo, ServiceInfo } from './durable/types.js';
5
+ export { EventBus } from './event-bus.js';
6
+ export { RondevuAPI } from './api.js';
7
+ export { RondevuService } from './rondevu-service.js';
8
+ export { RondevuSignaler } from './signaler.js';
9
+ export { ServiceHost } from './service-host.js';
10
+ export { ServiceClient } from './service-client.js';
11
+ export { WebRTCRondevuConnection } from './connection.js';
12
+ export { createBin } from './bin.js';
13
+ export type { ConnectionInterface, QueueMessageOptions, Message, ConnectionEvents, Signaler, } from './types.js';
14
+ export type { Credentials, Keypair, OfferRequest, Offer, ServiceRequest, Service, IceCandidate, } from './api.js';
15
+ export type { Binnable } from './bin.js';
16
+ export type { RondevuServiceOptions, PublishServiceOptions } from './rondevu-service.js';
17
+ export type { ServiceHostOptions, ServiceHostEvents } from './service-host.js';
18
+ export type { ServiceClientOptions, ServiceClientEvents } from './service-client.js';
package/dist/index.js CHANGED
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * @xtr-dev/rondevu-client
3
- * WebRTC peer signaling and discovery client with durable connections
3
+ * WebRTC peer signaling client
4
4
  */
5
- // Export main client class
6
- export { Rondevu } from './rondevu.js';
7
- // Export authentication
8
- export { RondevuAuth } from './auth.js';
9
- // Export username API
10
- export { RondevuUsername } from './usernames.js';
11
- // Export durable connection APIs
12
- export { DurableConnection } from './durable/connection.js';
13
- export { DurableChannel } from './durable/channel.js';
14
- export { DurableService } from './durable/service.js';
5
+ export { EventBus } from './event-bus.js';
6
+ export { RondevuAPI } from './api.js';
7
+ export { RondevuService } from './rondevu-service.js';
8
+ export { RondevuSignaler } from './signaler.js';
9
+ export { ServiceHost } from './service-host.js';
10
+ export { ServiceClient } from './service-client.js';
11
+ export { WebRTCRondevuConnection } from './connection.js';
12
+ export { createBin } from './bin.js';
@@ -0,0 +1,14 @@
1
+ import { Signaler } from './types.js';
2
+ import { Binnable } from './bin.js';
3
+ /**
4
+ * NoOpSignaler - A signaler that does nothing
5
+ * Used as a placeholder during connection setup before the real signaler is available
6
+ */
7
+ export declare class NoOpSignaler implements Signaler {
8
+ addIceCandidate(_candidate: RTCIceCandidate): void;
9
+ addListener(_callback: (candidate: RTCIceCandidate) => void): Binnable;
10
+ addOfferListener(_callback: (offer: RTCSessionDescriptionInit) => void): Binnable;
11
+ addAnswerListener(_callback: (answer: RTCSessionDescriptionInit) => void): Binnable;
12
+ setOffer(_offer: RTCSessionDescriptionInit): Promise<void>;
13
+ setAnswer(_answer: RTCSessionDescriptionInit): Promise<void>;
14
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * NoOpSignaler - A signaler that does nothing
3
+ * Used as a placeholder during connection setup before the real signaler is available
4
+ */
5
+ export class NoOpSignaler {
6
+ addIceCandidate(_candidate) {
7
+ // No-op
8
+ }
9
+ addListener(_callback) {
10
+ // Return no-op cleanup function
11
+ return () => { };
12
+ }
13
+ addOfferListener(_callback) {
14
+ // Return no-op cleanup function
15
+ return () => { };
16
+ }
17
+ addAnswerListener(_callback) {
18
+ // Return no-op cleanup function
19
+ return () => { };
20
+ }
21
+ async setOffer(_offer) {
22
+ // No-op
23
+ }
24
+ async setAnswer(_answer) {
25
+ // No-op
26
+ }
27
+ }
@@ -0,0 +1,81 @@
1
+ import { RondevuAPI, Credentials, Keypair, Service } from './api.js';
2
+ export interface RondevuServiceOptions {
3
+ apiUrl: string;
4
+ username: string;
5
+ keypair?: Keypair;
6
+ credentials?: Credentials;
7
+ }
8
+ export interface PublishServiceOptions {
9
+ serviceFqn: string;
10
+ sdp: string;
11
+ ttl?: number;
12
+ isPublic?: boolean;
13
+ metadata?: Record<string, any>;
14
+ }
15
+ /**
16
+ * RondevuService - High-level service management with automatic signature handling
17
+ *
18
+ * Provides a simplified API for:
19
+ * - Username claiming with Ed25519 signatures
20
+ * - Service publishing with automatic signature generation
21
+ * - Keypair management
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Initialize service (generates keypair automatically)
26
+ * const service = new RondevuService({
27
+ * apiUrl: 'https://signal.example.com',
28
+ * username: 'myusername',
29
+ * })
30
+ *
31
+ * await service.initialize()
32
+ *
33
+ * // Claim username (one time)
34
+ * await service.claimUsername()
35
+ *
36
+ * // Publish a service
37
+ * const publishedService = await service.publishService({
38
+ * serviceFqn: 'chat.app@1.0.0',
39
+ * sdp: offerSdp,
40
+ * ttl: 300000,
41
+ * isPublic: true,
42
+ * })
43
+ * ```
44
+ */
45
+ export declare class RondevuService {
46
+ private readonly api;
47
+ private readonly username;
48
+ private keypair;
49
+ private usernameClaimed;
50
+ constructor(options: RondevuServiceOptions);
51
+ /**
52
+ * Initialize the service - generates keypair if not provided
53
+ * Call this before using other methods
54
+ */
55
+ initialize(): Promise<void>;
56
+ /**
57
+ * Claim the username with Ed25519 signature
58
+ * Should be called once before publishing services
59
+ */
60
+ claimUsername(): Promise<void>;
61
+ /**
62
+ * Publish a service with automatic signature generation
63
+ */
64
+ publishService(options: PublishServiceOptions): Promise<Service>;
65
+ /**
66
+ * Get the current keypair (for backup/storage)
67
+ */
68
+ getKeypair(): Keypair | null;
69
+ /**
70
+ * Get the public key
71
+ */
72
+ getPublicKey(): string | null;
73
+ /**
74
+ * Check if username has been claimed
75
+ */
76
+ isUsernameClaimed(): boolean;
77
+ /**
78
+ * Access to underlying API for advanced operations
79
+ */
80
+ getAPI(): RondevuAPI;
81
+ }
@@ -0,0 +1,131 @@
1
+ import { RondevuAPI } from './api.js';
2
+ /**
3
+ * RondevuService - High-level service management with automatic signature handling
4
+ *
5
+ * Provides a simplified API for:
6
+ * - Username claiming with Ed25519 signatures
7
+ * - Service publishing with automatic signature generation
8
+ * - Keypair management
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // Initialize service (generates keypair automatically)
13
+ * const service = new RondevuService({
14
+ * apiUrl: 'https://signal.example.com',
15
+ * username: 'myusername',
16
+ * })
17
+ *
18
+ * await service.initialize()
19
+ *
20
+ * // Claim username (one time)
21
+ * await service.claimUsername()
22
+ *
23
+ * // Publish a service
24
+ * const publishedService = await service.publishService({
25
+ * serviceFqn: 'chat.app@1.0.0',
26
+ * sdp: offerSdp,
27
+ * ttl: 300000,
28
+ * isPublic: true,
29
+ * })
30
+ * ```
31
+ */
32
+ export class RondevuService {
33
+ constructor(options) {
34
+ this.keypair = null;
35
+ this.usernameClaimed = false;
36
+ this.username = options.username;
37
+ this.keypair = options.keypair || null;
38
+ this.api = new RondevuAPI(options.apiUrl, options.credentials);
39
+ }
40
+ /**
41
+ * Initialize the service - generates keypair if not provided
42
+ * Call this before using other methods
43
+ */
44
+ async initialize() {
45
+ if (!this.keypair) {
46
+ this.keypair = await RondevuAPI.generateKeypair();
47
+ }
48
+ // Register with API if no credentials provided
49
+ if (!this.api['credentials']) {
50
+ const credentials = await this.api.register();
51
+ this.api.credentials = credentials;
52
+ }
53
+ }
54
+ /**
55
+ * Claim the username with Ed25519 signature
56
+ * Should be called once before publishing services
57
+ */
58
+ async claimUsername() {
59
+ if (!this.keypair) {
60
+ throw new Error('Service not initialized. Call initialize() first.');
61
+ }
62
+ // Check if username is already claimed
63
+ const check = await this.api.checkUsername(this.username);
64
+ if (!check.available) {
65
+ // Verify it's claimed by us
66
+ if (check.owner === this.keypair.publicKey) {
67
+ this.usernameClaimed = true;
68
+ return;
69
+ }
70
+ throw new Error(`Username "${this.username}" is already claimed by another user`);
71
+ }
72
+ // Generate signature for username claim
73
+ const message = `claim-username-${this.username}-${Date.now()}`;
74
+ const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
75
+ // Claim the username
76
+ await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message);
77
+ this.usernameClaimed = true;
78
+ }
79
+ /**
80
+ * Publish a service with automatic signature generation
81
+ */
82
+ async publishService(options) {
83
+ if (!this.keypair) {
84
+ throw new Error('Service not initialized. Call initialize() first.');
85
+ }
86
+ if (!this.usernameClaimed) {
87
+ throw new Error('Username not claimed. Call claimUsername() first or the server will reject the service.');
88
+ }
89
+ const { serviceFqn, sdp, ttl, isPublic, metadata } = options;
90
+ // Generate signature for service publication
91
+ const message = `publish-${this.username}-${serviceFqn}-${Date.now()}`;
92
+ const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
93
+ // Create service request
94
+ const serviceRequest = {
95
+ username: this.username,
96
+ serviceFqn,
97
+ sdp,
98
+ signature,
99
+ message,
100
+ ttl,
101
+ isPublic,
102
+ metadata,
103
+ };
104
+ // Publish to server
105
+ return await this.api.publishService(serviceRequest);
106
+ }
107
+ /**
108
+ * Get the current keypair (for backup/storage)
109
+ */
110
+ getKeypair() {
111
+ return this.keypair;
112
+ }
113
+ /**
114
+ * Get the public key
115
+ */
116
+ getPublicKey() {
117
+ return this.keypair?.publicKey || null;
118
+ }
119
+ /**
120
+ * Check if username has been claimed
121
+ */
122
+ isUsernameClaimed() {
123
+ return this.usernameClaimed;
124
+ }
125
+ /**
126
+ * Access to underlying API for advanced operations
127
+ */
128
+ getAPI() {
129
+ return this.api;
130
+ }
131
+ }
@@ -0,0 +1,92 @@
1
+ import { WebRTCRondevuConnection } from './connection.js';
2
+ import { RondevuService } from './rondevu-service.js';
3
+ import { EventBus } from './event-bus.js';
4
+ import { ConnectionInterface } from './types.js';
5
+ export interface ServiceClientOptions {
6
+ username: string;
7
+ serviceFqn: string;
8
+ rondevuService: RondevuService;
9
+ autoReconnect?: boolean;
10
+ reconnectDelay?: number;
11
+ maxReconnectAttempts?: number;
12
+ }
13
+ export interface ServiceClientEvents {
14
+ connected: ConnectionInterface;
15
+ disconnected: {
16
+ reason: string;
17
+ };
18
+ reconnecting: {
19
+ attempt: number;
20
+ maxAttempts: number;
21
+ };
22
+ error: Error;
23
+ }
24
+ /**
25
+ * ServiceClient - Connects to a hosted service
26
+ *
27
+ * Searches for available service offers and establishes a WebRTC connection.
28
+ * Optionally supports automatic reconnection on failure.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const rondevuService = new RondevuService({
33
+ * apiUrl: 'https://signal.example.com',
34
+ * username: 'client-user',
35
+ * })
36
+ *
37
+ * await rondevuService.initialize()
38
+ *
39
+ * const client = new ServiceClient({
40
+ * username: 'host-user',
41
+ * serviceFqn: 'chat.app@1.0.0',
42
+ * rondevuService,
43
+ * autoReconnect: true,
44
+ * })
45
+ *
46
+ * await client.connect()
47
+ *
48
+ * client.events.on('connected', (conn) => {
49
+ * console.log('Connected to service')
50
+ * conn.sendMessage('Hello!')
51
+ * })
52
+ * ```
53
+ */
54
+ export declare class ServiceClient {
55
+ private readonly username;
56
+ private readonly serviceFqn;
57
+ private readonly rondevuService;
58
+ private readonly autoReconnect;
59
+ private readonly reconnectDelay;
60
+ private readonly maxReconnectAttempts;
61
+ private connection;
62
+ private reconnectAttempts;
63
+ private reconnectTimeout;
64
+ private readonly bin;
65
+ private isConnecting;
66
+ readonly events: EventBus<ServiceClientEvents>;
67
+ constructor(options: ServiceClientOptions);
68
+ /**
69
+ * Connect to the service
70
+ */
71
+ connect(): Promise<WebRTCRondevuConnection>;
72
+ /**
73
+ * Disconnect from the service
74
+ */
75
+ disconnect(): void;
76
+ /**
77
+ * Get the current connection
78
+ */
79
+ getConnection(): WebRTCRondevuConnection | null;
80
+ /**
81
+ * Check if currently connected
82
+ */
83
+ isConnected(): boolean;
84
+ /**
85
+ * Handle connection state changes
86
+ */
87
+ private handleConnectionStateChange;
88
+ /**
89
+ * Schedule a reconnection attempt
90
+ */
91
+ private scheduleReconnect;
92
+ }
@@ -0,0 +1,185 @@
1
+ import { WebRTCRondevuConnection } from './connection.js';
2
+ import { WebRTCContext } from './webrtc-context.js';
3
+ import { RondevuSignaler } from './signaler.js';
4
+ import { EventBus } from './event-bus.js';
5
+ import { createBin } from './bin.js';
6
+ /**
7
+ * ServiceClient - Connects to a hosted service
8
+ *
9
+ * Searches for available service offers and establishes a WebRTC connection.
10
+ * Optionally supports automatic reconnection on failure.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const rondevuService = new RondevuService({
15
+ * apiUrl: 'https://signal.example.com',
16
+ * username: 'client-user',
17
+ * })
18
+ *
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,
26
+ * })
27
+ *
28
+ * await client.connect()
29
+ *
30
+ * client.events.on('connected', (conn) => {
31
+ * console.log('Connected to service')
32
+ * conn.sendMessage('Hello!')
33
+ * })
34
+ * ```
35
+ */
36
+ export class ServiceClient {
37
+ constructor(options) {
38
+ this.connection = null;
39
+ this.reconnectAttempts = 0;
40
+ this.reconnectTimeout = null;
41
+ this.bin = createBin();
42
+ this.isConnecting = false;
43
+ 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;
49
+ this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
50
+ }
51
+ /**
52
+ * Connect to the service
53
+ */
54
+ async connect() {
55
+ if (this.isConnecting) {
56
+ throw new Error('Already connecting');
57
+ }
58
+ if (this.connection && this.connection.state === 'connected') {
59
+ return this.connection;
60
+ }
61
+ this.isConnecting = true;
62
+ 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,
86
+ });
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);
102
+ });
103
+ this.bin(cleanup);
104
+ 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;
110
+ }
111
+ catch (error) {
112
+ this.isConnecting = false;
113
+ this.events.emit('error', error);
114
+ throw error;
115
+ }
116
+ }
117
+ /**
118
+ * Disconnect from the service
119
+ */
120
+ disconnect() {
121
+ if (this.reconnectTimeout) {
122
+ clearTimeout(this.reconnectTimeout);
123
+ this.reconnectTimeout = null;
124
+ }
125
+ if (this.connection) {
126
+ this.connection.disconnect();
127
+ this.connection = null;
128
+ }
129
+ this.bin.clean();
130
+ this.reconnectAttempts = 0;
131
+ }
132
+ /**
133
+ * Get the current connection
134
+ */
135
+ getConnection() {
136
+ return this.connection;
137
+ }
138
+ /**
139
+ * Check if currently connected
140
+ */
141
+ isConnected() {
142
+ return this.connection?.state === 'connected';
143
+ }
144
+ /**
145
+ * Handle connection state changes
146
+ */
147
+ handleConnectionStateChange(state) {
148
+ if (state === 'connected') {
149
+ this.events.emit('connected', this.connection);
150
+ this.reconnectAttempts = 0;
151
+ }
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();
157
+ }
158
+ }
159
+ }
160
+ /**
161
+ * Schedule a reconnection attempt
162
+ */
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);
184
+ }
185
+ }