@xtr-dev/rondevu-client 0.12.4 → 0.13.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,60 +1,70 @@
1
1
  import { RondevuAPI } from './api.js';
2
2
  /**
3
- * RondevuService - High-level service management with automatic signature handling
3
+ * Rondevu - Complete WebRTC signaling client
4
4
  *
5
- * Provides a simplified API for:
5
+ * Provides a unified API for:
6
6
  * - Username claiming with Ed25519 signatures
7
7
  * - Service publishing with automatic signature generation
8
+ * - Service discovery (direct, random, paginated)
9
+ * - WebRTC signaling (offer/answer exchange, ICE relay)
8
10
  * - Keypair management
9
11
  *
10
12
  * @example
11
13
  * ```typescript
12
- * // Initialize service (generates keypair automatically)
13
- * const service = new RondevuService({
14
+ * // Initialize (generates keypair automatically)
15
+ * const rondevu = new Rondevu({
14
16
  * apiUrl: 'https://signal.example.com',
15
- * username: 'myusername',
17
+ * username: 'alice',
16
18
  * })
17
19
  *
18
- * await service.initialize()
20
+ * await rondevu.initialize()
19
21
  *
20
22
  * // Claim username (one time)
21
- * await service.claimUsername()
23
+ * await rondevu.claimUsername()
22
24
  *
23
25
  * // Publish a service
24
- * const publishedService = await service.publishService({
25
- * serviceFqn: 'chat.app@1.0.0',
26
+ * const publishedService = await rondevu.publishService({
27
+ * serviceFqn: 'chat:1.0.0@alice',
26
28
  * offers: [{ sdp: offerSdp }],
27
29
  * ttl: 300000,
28
- * isPublic: true,
29
30
  * })
31
+ *
32
+ * // Discover a service
33
+ * const service = await rondevu.getService('chat:1.0.0@bob')
34
+ *
35
+ * // Post answer
36
+ * await rondevu.postOfferAnswer(service.serviceFqn, service.offerId, answerSdp)
30
37
  * ```
31
38
  */
32
- export class RondevuService {
39
+ export class Rondevu {
33
40
  constructor(options) {
34
41
  this.keypair = null;
35
42
  this.usernameClaimed = false;
36
43
  this.username = options.username;
37
44
  this.keypair = options.keypair || null;
38
45
  this.api = new RondevuAPI(options.apiUrl, options.credentials);
39
- console.log('[RondevuService] Constructor called:', {
46
+ console.log('[Rondevu] Constructor called:', {
40
47
  username: this.username,
41
48
  hasKeypair: !!this.keypair,
42
49
  publicKey: this.keypair?.publicKey
43
50
  });
44
51
  }
52
+ // ============================================
53
+ // Initialization
54
+ // ============================================
45
55
  /**
46
56
  * Initialize the service - generates keypair if not provided
47
57
  * Call this before using other methods
48
58
  */
49
59
  async initialize() {
50
- console.log('[RondevuService] Initialize called, hasKeypair:', !!this.keypair);
60
+ console.log('[Rondevu] Initialize called, hasKeypair:', !!this.keypair);
51
61
  if (!this.keypair) {
52
- console.log('[RondevuService] Generating new keypair...');
62
+ console.log('[Rondevu] Generating new keypair...');
53
63
  this.keypair = await RondevuAPI.generateKeypair();
54
- console.log('[RondevuService] Generated keypair, publicKey:', this.keypair.publicKey);
64
+ console.log('[Rondevu] Generated keypair, publicKey:', this.keypair.publicKey);
55
65
  }
56
66
  else {
57
- console.log('[RondevuService] Using existing keypair, publicKey:', this.keypair.publicKey);
67
+ console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey);
58
68
  }
59
69
  // Register with API if no credentials provided
60
70
  if (!this.api['credentials']) {
@@ -62,13 +72,16 @@ export class RondevuService {
62
72
  this.api.setCredentials(credentials);
63
73
  }
64
74
  }
75
+ // ============================================
76
+ // Username Management
77
+ // ============================================
65
78
  /**
66
79
  * Claim the username with Ed25519 signature
67
80
  * Should be called once before publishing services
68
81
  */
69
82
  async claimUsername() {
70
83
  if (!this.keypair) {
71
- throw new Error('Service not initialized. Call initialize() first.');
84
+ throw new Error('Not initialized. Call initialize() first.');
72
85
  }
73
86
  // Check if username is already claimed
74
87
  const check = await this.api.checkUsername(this.username);
@@ -87,34 +100,116 @@ export class RondevuService {
87
100
  await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message);
88
101
  this.usernameClaimed = true;
89
102
  }
103
+ /**
104
+ * Check if username has been claimed (checks with server)
105
+ */
106
+ async isUsernameClaimed() {
107
+ if (!this.keypair) {
108
+ return false;
109
+ }
110
+ try {
111
+ const check = await this.api.checkUsername(this.username);
112
+ // Debug logging
113
+ console.log('[Rondevu] Username check:', {
114
+ username: this.username,
115
+ available: check.available,
116
+ serverPublicKey: check.publicKey,
117
+ localPublicKey: this.keypair.publicKey,
118
+ match: check.publicKey === this.keypair.publicKey
119
+ });
120
+ // Username is claimed if it's not available and owned by our public key
121
+ const claimed = !check.available && check.publicKey === this.keypair.publicKey;
122
+ // Update internal flag to match server state
123
+ this.usernameClaimed = claimed;
124
+ return claimed;
125
+ }
126
+ catch (err) {
127
+ console.error('Failed to check username claim status:', err);
128
+ return false;
129
+ }
130
+ }
131
+ // ============================================
132
+ // Service Publishing
133
+ // ============================================
90
134
  /**
91
135
  * Publish a service with automatic signature generation
92
136
  */
93
137
  async publishService(options) {
94
138
  if (!this.keypair) {
95
- throw new Error('Service not initialized. Call initialize() first.');
139
+ throw new Error('Not initialized. Call initialize() first.');
96
140
  }
97
141
  if (!this.usernameClaimed) {
98
142
  throw new Error('Username not claimed. Call claimUsername() first or the server will reject the service.');
99
143
  }
100
- const { serviceFqn, offers, ttl, isPublic, metadata } = options;
144
+ const { serviceFqn, offers, ttl } = options;
101
145
  // Generate signature for service publication
102
146
  const message = `publish:${this.username}:${serviceFqn}:${Date.now()}`;
103
147
  const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey);
104
148
  // Create service request
105
149
  const serviceRequest = {
106
- username: this.username,
107
- serviceFqn,
150
+ serviceFqn, // Must include @username
108
151
  offers,
109
152
  signature,
110
153
  message,
111
154
  ttl,
112
- isPublic,
113
- metadata,
114
155
  };
115
156
  // Publish to server
116
157
  return await this.api.publishService(serviceRequest);
117
158
  }
159
+ // ============================================
160
+ // Service Discovery
161
+ // ============================================
162
+ /**
163
+ * Get service by FQN (with username) - Direct lookup
164
+ * Example: chat:1.0.0@alice
165
+ */
166
+ async getService(serviceFqn) {
167
+ return await this.api.getService(serviceFqn);
168
+ }
169
+ /**
170
+ * Discover a random available service without knowing the username
171
+ * Example: chat:1.0.0 (without @username)
172
+ */
173
+ async discoverService(serviceVersion) {
174
+ return await this.api.discoverService(serviceVersion);
175
+ }
176
+ /**
177
+ * Discover multiple available services with pagination
178
+ * Example: chat:1.0.0 (without @username)
179
+ */
180
+ async discoverServices(serviceVersion, limit = 10, offset = 0) {
181
+ return await this.api.discoverServices(serviceVersion, limit, offset);
182
+ }
183
+ // ============================================
184
+ // WebRTC Signaling
185
+ // ============================================
186
+ /**
187
+ * Post answer SDP to specific offer
188
+ */
189
+ async postOfferAnswer(serviceFqn, offerId, sdp) {
190
+ return await this.api.postOfferAnswer(serviceFqn, offerId, sdp);
191
+ }
192
+ /**
193
+ * Get answer SDP (offerer polls this)
194
+ */
195
+ async getOfferAnswer(serviceFqn, offerId) {
196
+ return await this.api.getOfferAnswer(serviceFqn, offerId);
197
+ }
198
+ /**
199
+ * Add ICE candidates to specific offer
200
+ */
201
+ async addOfferIceCandidates(serviceFqn, offerId, candidates) {
202
+ return await this.api.addOfferIceCandidates(serviceFqn, offerId, candidates);
203
+ }
204
+ /**
205
+ * Get ICE candidates for specific offer (with polling support)
206
+ */
207
+ async getOfferIceCandidates(serviceFqn, offerId, since = 0) {
208
+ return await this.api.getOfferIceCandidates(serviceFqn, offerId, since);
209
+ }
210
+ // ============================================
211
+ // Utility Methods
212
+ // ============================================
118
213
  /**
119
214
  * Get the current keypair (for backup/storage)
120
215
  */
@@ -133,36 +228,9 @@ export class RondevuService {
133
228
  getPublicKey() {
134
229
  return this.keypair?.publicKey || null;
135
230
  }
136
- /**
137
- * Check if username has been claimed (checks with server)
138
- */
139
- async isUsernameClaimed() {
140
- if (!this.keypair) {
141
- return false;
142
- }
143
- try {
144
- const check = await this.api.checkUsername(this.username);
145
- // Debug logging
146
- console.log('[RondevuService] Username check:', {
147
- username: this.username,
148
- available: check.available,
149
- serverPublicKey: check.publicKey,
150
- localPublicKey: this.keypair.publicKey,
151
- match: check.publicKey === this.keypair.publicKey
152
- });
153
- // Username is claimed if it's not available and owned by our public key
154
- const claimed = !check.available && check.publicKey === this.keypair.publicKey;
155
- // Update internal flag to match server state
156
- this.usernameClaimed = claimed;
157
- return claimed;
158
- }
159
- catch (err) {
160
- console.error('Failed to check username claim status:', err);
161
- return false;
162
- }
163
- }
164
231
  /**
165
232
  * Access to underlying API for advanced operations
233
+ * @deprecated Use direct methods on Rondevu instance instead
166
234
  */
167
235
  getAPI() {
168
236
  return this.api;
package/dist/types.d.ts CHANGED
@@ -1,26 +1,13 @@
1
1
  /**
2
- * Core connection types
2
+ * Core signaling types
3
+ */
4
+ /**
5
+ * Cleanup function returned by listener methods
6
+ */
7
+ export type Binnable = () => void;
8
+ /**
9
+ * Signaler interface for WebRTC offer/answer/ICE exchange
3
10
  */
4
- import { EventBus } from './event-bus.js';
5
- import { Binnable } from './bin.js';
6
- export type Message = string | ArrayBuffer;
7
- export interface QueueMessageOptions {
8
- expiresAt?: number;
9
- }
10
- export interface ConnectionEvents {
11
- 'state-change': ConnectionInterface['state'];
12
- message: Message;
13
- }
14
- export declare const ConnectionStates: readonly ["connected", "disconnected", "connecting"];
15
- export declare const isConnectionState: (state: string) => state is (typeof ConnectionStates)[number];
16
- export interface ConnectionInterface {
17
- state: (typeof ConnectionStates)[number];
18
- lastActive: number;
19
- expiresAt?: number;
20
- events: EventBus<ConnectionEvents>;
21
- queueMessage(message: Message, options?: QueueMessageOptions): Promise<void>;
22
- sendMessage(message: Message): Promise<boolean>;
23
- }
24
11
  export interface Signaler {
25
12
  addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
26
13
  addListener(callback: (candidate: RTCIceCandidate) => void): Binnable;
package/dist/types.js CHANGED
@@ -1,6 +1,4 @@
1
- export const ConnectionStates = [
2
- 'connected',
3
- 'disconnected',
4
- 'connecting'
5
- ];
6
- export const isConnectionState = (state) => ConnectionStates.includes(state);
1
+ /**
2
+ * Core signaling types
3
+ */
4
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.12.4",
3
+ "version": "0.13.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",
package/dist/bin.d.ts DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Binnable - A cleanup function that can be synchronous or asynchronous
3
- *
4
- * Used to unsubscribe from events, close connections, or perform other cleanup operations.
5
- */
6
- export type Binnable = () => void | Promise<void>;
7
- /**
8
- * Create a cleanup function collector (garbage bin)
9
- *
10
- * Collects cleanup functions and provides a single `clean()` method to execute all of them.
11
- * Useful for managing multiple cleanup operations in a single place.
12
- *
13
- * @returns A function that accepts cleanup functions and has a `clean()` method
14
- *
15
- * @example
16
- * ```typescript
17
- * const bin = createBin();
18
- *
19
- * // Add cleanup functions
20
- * bin(
21
- * () => console.log('Cleanup 1'),
22
- * () => connection.close(),
23
- * () => clearInterval(timer)
24
- * );
25
- *
26
- * // Later, clean everything
27
- * bin.clean(); // Executes all cleanup functions
28
- * ```
29
- */
30
- export declare const createBin: () => ((...rubbish: Binnable[]) => number) & {
31
- /**
32
- * Execute all cleanup functions and clear the bin
33
- */
34
- clean: () => void;
35
- };
package/dist/bin.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Create a cleanup function collector (garbage bin)
3
- *
4
- * Collects cleanup functions and provides a single `clean()` method to execute all of them.
5
- * Useful for managing multiple cleanup operations in a single place.
6
- *
7
- * @returns A function that accepts cleanup functions and has a `clean()` method
8
- *
9
- * @example
10
- * ```typescript
11
- * const bin = createBin();
12
- *
13
- * // Add cleanup functions
14
- * bin(
15
- * () => console.log('Cleanup 1'),
16
- * () => connection.close(),
17
- * () => clearInterval(timer)
18
- * );
19
- *
20
- * // Later, clean everything
21
- * bin.clean(); // Executes all cleanup functions
22
- * ```
23
- */
24
- export const createBin = () => {
25
- const bin = [];
26
- return Object.assign((...rubbish) => bin.push(...rubbish), {
27
- /**
28
- * Execute all cleanup functions and clear the bin
29
- */
30
- clean: () => {
31
- bin.forEach(binnable => binnable());
32
- bin.length = 0;
33
- },
34
- });
35
- };
@@ -1,104 +0,0 @@
1
- import { WebRTCRondevuConnection } from './connection.js';
2
- import { Credentials } from './api.js';
3
- import { ConnectionInterface } from './types.js';
4
- export interface ConnectionManagerOptions {
5
- apiUrl: string;
6
- username: string;
7
- credentials?: Credentials;
8
- autoReconnect?: boolean;
9
- reconnectDelay?: number;
10
- maxReconnectAttempts?: number;
11
- }
12
- export interface HostServiceOptions {
13
- service: string;
14
- ttl?: number;
15
- onConnection?: (connection: ConnectionInterface) => void;
16
- }
17
- export interface ConnectToServiceOptions {
18
- username: string;
19
- service: string;
20
- onConnection?: (connection: ConnectionInterface) => void;
21
- }
22
- /**
23
- * ConnectionManager - High-level connection management
24
- *
25
- * @example
26
- * // Host a service
27
- * const manager = new ConnectionManager({
28
- * apiUrl: 'https://api.ronde.vu',
29
- * credentials: await api.register()
30
- * })
31
- *
32
- * await manager.hostService({
33
- * service: 'chat.app@1.0.0',
34
- * onConnection: (conn) => {
35
- * conn.events.on('message', msg => console.log('Received:', msg))
36
- * }
37
- * })
38
- *
39
- * @example
40
- * // Connect to a service
41
- * const connection = await manager.connectToService({
42
- * username: 'alice',
43
- * service: 'chat.app@1.0.0'
44
- * })
45
- *
46
- * await connection.sendMessage('Hello!')
47
- */
48
- export declare class ConnectionManager {
49
- private readonly api;
50
- private readonly username;
51
- private readonly connections;
52
- private readonly autoReconnect;
53
- private readonly reconnectDelay;
54
- private readonly maxReconnectAttempts;
55
- private readonly bin;
56
- constructor(options: ConnectionManagerOptions);
57
- /**
58
- * Host a service - Creates an offer and publishes it to the signaling server
59
- *
60
- * The service will automatically accept incoming connections and manage them.
61
- * Each new connection triggers the onConnection callback.
62
- *
63
- * @param options - Service hosting options
64
- * @returns Promise that resolves when the service is published
65
- */
66
- hostService(options: HostServiceOptions): Promise<void>;
67
- /**
68
- * Connect to a hosted service
69
- *
70
- * Searches for the service, retrieves the offer, and creates an answering connection.
71
- *
72
- * @param options - Connection options
73
- * @returns The established connection
74
- */
75
- connectToService(options: ConnectToServiceOptions): Promise<WebRTCRondevuConnection>;
76
- /**
77
- * Get a connection by ID
78
- */
79
- getConnection(id: string): WebRTCRondevuConnection | undefined;
80
- /**
81
- * Get all managed connections
82
- */
83
- getAllConnections(): WebRTCRondevuConnection[];
84
- /**
85
- * Remove a connection
86
- */
87
- private removeConnection;
88
- /**
89
- * Schedule reconnection for a failed connection
90
- */
91
- private scheduleReconnect;
92
- /**
93
- * Attempt to reconnect a failed connection
94
- */
95
- private attemptReconnect;
96
- /**
97
- * Create a temporary signaler (used during initial offer creation)
98
- */
99
- private createTempSignaler;
100
- /**
101
- * Clean up all connections and resources
102
- */
103
- destroy(): void;
104
- }