@xtr-dev/rondevu-client 0.9.2 → 0.10.1

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 +94 -0
  18. package/dist/service-client.js +186 -0
  19. package/dist/service-host.d.ts +103 -0
  20. package/dist/service-host.js +186 -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 +7 -0
  26. package/dist/webrtc-context.js +36 -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,94 @@
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
+ rtcConfiguration?: RTCConfiguration;
13
+ }
14
+ export interface ServiceClientEvents {
15
+ connected: ConnectionInterface;
16
+ disconnected: {
17
+ reason: string;
18
+ };
19
+ reconnecting: {
20
+ attempt: number;
21
+ maxAttempts: number;
22
+ };
23
+ error: Error;
24
+ }
25
+ /**
26
+ * ServiceClient - Connects to a hosted service
27
+ *
28
+ * Searches for available service offers and establishes a WebRTC connection.
29
+ * Optionally supports automatic reconnection on failure.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const rondevuService = new RondevuService({
34
+ * apiUrl: 'https://signal.example.com',
35
+ * username: 'client-user',
36
+ * })
37
+ *
38
+ * await rondevuService.initialize()
39
+ *
40
+ * const client = new ServiceClient({
41
+ * username: 'host-user',
42
+ * serviceFqn: 'chat.app@1.0.0',
43
+ * rondevuService,
44
+ * autoReconnect: true,
45
+ * })
46
+ *
47
+ * await client.connect()
48
+ *
49
+ * client.events.on('connected', (conn) => {
50
+ * console.log('Connected to service')
51
+ * conn.sendMessage('Hello!')
52
+ * })
53
+ * ```
54
+ */
55
+ export declare class ServiceClient {
56
+ private readonly username;
57
+ private readonly serviceFqn;
58
+ private readonly rondevuService;
59
+ private readonly autoReconnect;
60
+ private readonly reconnectDelay;
61
+ private readonly maxReconnectAttempts;
62
+ private readonly rtcConfiguration?;
63
+ private connection;
64
+ private reconnectAttempts;
65
+ private reconnectTimeout;
66
+ private readonly bin;
67
+ private isConnecting;
68
+ readonly events: EventBus<ServiceClientEvents>;
69
+ constructor(options: ServiceClientOptions);
70
+ /**
71
+ * Connect to the service
72
+ */
73
+ connect(): Promise<WebRTCRondevuConnection>;
74
+ /**
75
+ * Disconnect from the service
76
+ */
77
+ disconnect(): void;
78
+ /**
79
+ * Get the current connection
80
+ */
81
+ getConnection(): WebRTCRondevuConnection | null;
82
+ /**
83
+ * Check if currently connected
84
+ */
85
+ isConnected(): boolean;
86
+ /**
87
+ * Handle connection state changes
88
+ */
89
+ private handleConnectionStateChange;
90
+ /**
91
+ * Schedule a reconnection attempt
92
+ */
93
+ private scheduleReconnect;
94
+ }
@@ -0,0 +1,186 @@
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
+ this.rtcConfiguration = options.rtcConfiguration;
51
+ }
52
+ /**
53
+ * Connect to the service
54
+ */
55
+ async connect() {
56
+ if (this.isConnecting) {
57
+ throw new Error('Already connecting');
58
+ }
59
+ if (this.connection && this.connection.state === 'connected') {
60
+ return this.connection;
61
+ }
62
+ this.isConnecting = true;
63
+ 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,
87
+ });
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);
103
+ });
104
+ this.bin(cleanup);
105
+ 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;
111
+ }
112
+ catch (error) {
113
+ this.isConnecting = false;
114
+ this.events.emit('error', error);
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * Disconnect from the service
120
+ */
121
+ disconnect() {
122
+ if (this.reconnectTimeout) {
123
+ clearTimeout(this.reconnectTimeout);
124
+ this.reconnectTimeout = null;
125
+ }
126
+ if (this.connection) {
127
+ this.connection.disconnect();
128
+ this.connection = null;
129
+ }
130
+ this.bin.clean();
131
+ this.reconnectAttempts = 0;
132
+ }
133
+ /**
134
+ * Get the current connection
135
+ */
136
+ getConnection() {
137
+ return this.connection;
138
+ }
139
+ /**
140
+ * Check if currently connected
141
+ */
142
+ isConnected() {
143
+ return this.connection?.state === 'connected';
144
+ }
145
+ /**
146
+ * Handle connection state changes
147
+ */
148
+ handleConnectionStateChange(state) {
149
+ if (state === 'connected') {
150
+ this.events.emit('connected', this.connection);
151
+ this.reconnectAttempts = 0;
152
+ }
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();
158
+ }
159
+ }
160
+ }
161
+ /**
162
+ * Schedule a reconnection attempt
163
+ */
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);
185
+ }
186
+ }