@xtr-dev/rondevu-client 0.8.2 → 0.9.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.
@@ -14,6 +14,7 @@ export class ServicePool {
14
14
  this.credentials = credentials;
15
15
  this.options = options;
16
16
  this.connections = new Map();
17
+ this.peerConnections = new Map();
17
18
  this.status = {
18
19
  activeOffers: 0,
19
20
  activeConnections: 0,
@@ -34,10 +35,14 @@ export class ServicePool {
34
35
  this.uuid = service.uuid;
35
36
  // 2. Create additional offers for pool (poolSize - 1)
36
37
  const additionalOffers = [];
38
+ const additionalPeerConnections = [];
39
+ const additionalDataChannels = [];
37
40
  if (poolSize > 1) {
38
41
  try {
39
- const offers = await this.createOffers(poolSize - 1);
40
- additionalOffers.push(...offers);
42
+ const result = await this.createOffers(poolSize - 1);
43
+ additionalOffers.push(...result.offers);
44
+ additionalPeerConnections.push(...result.peerConnections);
45
+ additionalDataChannels.push(...result.dataChannels);
41
46
  }
42
47
  catch (error) {
43
48
  this.handleError(error, 'initial-offer-creation');
@@ -51,12 +56,20 @@ export class ServicePool {
51
56
  onRefill: (count) => this.createOffers(count),
52
57
  onError: (err, ctx) => this.handleError(err, ctx)
53
58
  });
54
- // Add all offers to pool
59
+ // Add all offers to pool with their peer connections and data channels
55
60
  const allOffers = [
56
- { id: service.offerId, peerId: this.credentials.peerId, sdp: '', topics: [], expiresAt: service.expiresAt, lastSeen: Date.now() },
61
+ { id: service.offerId, peerId: this.credentials.peerId, sdp: service.offerSdp, topics: [], expiresAt: service.expiresAt, lastSeen: Date.now() },
57
62
  ...additionalOffers
58
63
  ];
59
- await this.offerPool.addOffers(allOffers);
64
+ const allPeerConnections = [
65
+ service.peerConnection,
66
+ ...additionalPeerConnections
67
+ ];
68
+ const allDataChannels = [
69
+ service.dataChannel,
70
+ ...additionalDataChannels
71
+ ];
72
+ await this.offerPool.addOffers(allOffers, allPeerConnections, allDataChannels);
60
73
  // 4. Start polling
61
74
  await this.offerPool.start();
62
75
  // Update status
@@ -79,12 +92,24 @@ export class ServicePool {
79
92
  if (this.offerPool) {
80
93
  await this.offerPool.stop();
81
94
  }
82
- // 2. Delete remaining offers
95
+ // 2. Close peer connections from the pool
96
+ if (this.offerPool) {
97
+ const poolPeerConnections = this.offerPool.getActivePeerConnections();
98
+ poolPeerConnections.forEach(pc => {
99
+ try {
100
+ pc.close();
101
+ }
102
+ catch {
103
+ // Ignore errors during cleanup
104
+ }
105
+ });
106
+ }
107
+ // 3. Delete remaining offers
83
108
  if (this.offerPool) {
84
109
  const offerIds = this.offerPool.getActiveOfferIds();
85
110
  await Promise.allSettled(offerIds.map(id => this.offersApi.delete(id).catch(() => { })));
86
111
  }
87
- // 3. Close active connections
112
+ // 4. Close active connections
88
113
  const closePromises = Array.from(this.connections.values()).map(async (conn) => {
89
114
  try {
90
115
  // Give a brief moment for graceful closure
@@ -96,7 +121,7 @@ export class ServicePool {
96
121
  }
97
122
  });
98
123
  await Promise.allSettled(closePromises);
99
- // 4. Delete service if we have a serviceId
124
+ // 5. Delete service if we have a serviceId
100
125
  if (this.serviceId) {
101
126
  try {
102
127
  const response = await fetch(`${this.baseUrl}/services/${this.serviceId}`, {
@@ -125,41 +150,52 @@ export class ServicePool {
125
150
  async handleConnection(answer) {
126
151
  const connectionId = this.generateConnectionId();
127
152
  try {
128
- // Create peer connection
153
+ // Use the existing peer connection from the pool
129
154
  const peer = new RondevuPeer(this.offersApi, this.options.rtcConfig || {
130
155
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
131
- });
156
+ }, answer.peerConnection // Use the existing peer connection
157
+ );
132
158
  peer.role = 'offerer';
133
159
  peer.offerId = answer.offerId;
160
+ // Verify peer connection is in correct state
161
+ if (peer.pc.signalingState !== 'have-local-offer') {
162
+ console.error('Peer connection state info:', {
163
+ signalingState: peer.pc.signalingState,
164
+ connectionState: peer.pc.connectionState,
165
+ iceConnectionState: peer.pc.iceConnectionState,
166
+ iceGatheringState: peer.pc.iceGatheringState,
167
+ hasLocalDescription: !!peer.pc.localDescription,
168
+ hasRemoteDescription: !!peer.pc.remoteDescription,
169
+ localDescriptionType: peer.pc.localDescription?.type,
170
+ remoteDescriptionType: peer.pc.remoteDescription?.type,
171
+ offerId: answer.offerId
172
+ });
173
+ throw new Error(`Invalid signaling state: ${peer.pc.signalingState}. Expected 'have-local-offer' to set remote answer.`);
174
+ }
134
175
  // Set remote description (the answer)
135
176
  await peer.pc.setRemoteDescription({
136
177
  type: 'answer',
137
178
  sdp: answer.sdp
138
179
  });
139
- // Wait for data channel (answerer creates it, we receive it)
140
- const channel = await new Promise((resolve, reject) => {
141
- const timeout = setTimeout(() => reject(new Error('Timeout waiting for data channel')), 30000);
142
- peer.on('datachannel', (ch) => {
143
- clearTimeout(timeout);
144
- resolve(ch);
145
- });
146
- // Also check if channel already exists
147
- if (peer.pc.ondatachannel) {
148
- const existingHandler = peer.pc.ondatachannel;
149
- peer.pc.ondatachannel = (event) => {
180
+ // Use the data channel we created when making the offer
181
+ if (!answer.dataChannel) {
182
+ throw new Error('No data channel found for answered offer');
183
+ }
184
+ const channel = answer.dataChannel;
185
+ // Wait for the channel to open (it was created when we made the offer)
186
+ if (channel.readyState !== 'open') {
187
+ await new Promise((resolve, reject) => {
188
+ const timeout = setTimeout(() => reject(new Error('Timeout waiting for data channel to open')), 30000);
189
+ channel.onopen = () => {
150
190
  clearTimeout(timeout);
151
- resolve(event.channel);
152
- if (existingHandler)
153
- existingHandler.call(peer.pc, event);
191
+ resolve();
154
192
  };
155
- }
156
- else {
157
- peer.pc.ondatachannel = (event) => {
193
+ channel.onerror = (error) => {
158
194
  clearTimeout(timeout);
159
- resolve(event.channel);
195
+ reject(new Error('Data channel error'));
160
196
  };
161
- }
162
- });
197
+ });
198
+ }
163
199
  // Register connection
164
200
  this.connections.set(connectionId, {
165
201
  peer,
@@ -199,11 +235,13 @@ export class ServicePool {
199
235
  */
200
236
  async createOffers(count) {
201
237
  if (count <= 0) {
202
- return [];
238
+ return { offers: [], peerConnections: [], dataChannels: [] };
203
239
  }
204
240
  // Server supports max 10 offers per request
205
241
  const batchSize = Math.min(count, 10);
206
242
  const offers = [];
243
+ const peerConnections = [];
244
+ const dataChannels = [];
207
245
  try {
208
246
  // Create peer connections and generate offers
209
247
  const offerRequests = [];
@@ -211,8 +249,9 @@ export class ServicePool {
211
249
  const pc = new RTCPeerConnection(this.options.rtcConfig || {
212
250
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
213
251
  });
214
- // Create data channel (required for offers)
215
- pc.createDataChannel('rondevu-service');
252
+ // Create data channel (required for offers) and save reference
253
+ const channel = pc.createDataChannel('rondevu-service');
254
+ dataChannels.push(channel);
216
255
  // Create offer
217
256
  const offer = await pc.createOffer();
218
257
  await pc.setLocalDescription(offer);
@@ -225,19 +264,39 @@ export class ServicePool {
225
264
  topics: [], // V2 doesn't use topics
226
265
  ttl: this.options.ttl
227
266
  });
228
- // Close the PC immediately - we only needed the SDP
229
- pc.close();
267
+ // Keep peer connection alive - DO NOT CLOSE
268
+ peerConnections.push(pc);
230
269
  }
231
270
  // Batch create offers
232
271
  const createdOffers = await this.offersApi.create(offerRequests);
233
272
  offers.push(...createdOffers);
273
+ // Set up ICE candidate handlers AFTER we have offer IDs
274
+ for (let i = 0; i < peerConnections.length; i++) {
275
+ const pc = peerConnections[i];
276
+ const offerId = createdOffers[i].id;
277
+ pc.onicecandidate = async (event) => {
278
+ if (event.candidate) {
279
+ const candidateData = event.candidate.toJSON();
280
+ if (candidateData.candidate && candidateData.candidate !== '') {
281
+ try {
282
+ await this.offersApi.addIceCandidates(offerId, [candidateData]);
283
+ }
284
+ catch (err) {
285
+ console.error('Error sending ICE candidate:', err);
286
+ }
287
+ }
288
+ }
289
+ };
290
+ }
234
291
  }
235
292
  catch (error) {
293
+ // Close any created peer connections on error
294
+ peerConnections.forEach(pc => pc.close());
236
295
  this.status.failedOfferCreations++;
237
296
  this.handleError(error, 'offer-creation');
238
297
  throw error;
239
298
  }
240
- return offers;
299
+ return { offers, peerConnections, dataChannels };
241
300
  }
242
301
  /**
243
302
  * Publish the initial service (creates first offer)
@@ -248,7 +307,7 @@ export class ServicePool {
248
307
  const pc = new RTCPeerConnection(rtcConfig || {
249
308
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
250
309
  });
251
- pc.createDataChannel('rondevu-service');
310
+ const dataChannel = pc.createDataChannel('rondevu-service');
252
311
  // Create offer
253
312
  const offer = await pc.createOffer();
254
313
  await pc.setLocalDescription(offer);
@@ -256,6 +315,8 @@ export class ServicePool {
256
315
  pc.close();
257
316
  throw new Error('Failed to generate SDP');
258
317
  }
318
+ // Store the SDP
319
+ const offerSdp = offer.sdp;
259
320
  // Create signature
260
321
  const timestamp = Date.now();
261
322
  const message = `publish:${username}:${serviceFqn}:${timestamp}`;
@@ -270,7 +331,7 @@ export class ServicePool {
270
331
  body: JSON.stringify({
271
332
  username,
272
333
  serviceFqn,
273
- sdp: offer.sdp,
334
+ sdp: offerSdp,
274
335
  ttl,
275
336
  isPublic,
276
337
  metadata,
@@ -278,17 +339,34 @@ export class ServicePool {
278
339
  message
279
340
  })
280
341
  });
281
- pc.close();
282
342
  if (!response.ok) {
343
+ pc.close();
283
344
  const error = await response.json();
284
345
  throw new Error(error.error || 'Failed to publish service');
285
346
  }
286
347
  const data = await response.json();
348
+ // Set up ICE candidate handler now that we have the offer ID
349
+ pc.onicecandidate = async (event) => {
350
+ if (event.candidate) {
351
+ const candidateData = event.candidate.toJSON();
352
+ if (candidateData.candidate && candidateData.candidate !== '') {
353
+ try {
354
+ await this.offersApi.addIceCandidates(data.offerId, [candidateData]);
355
+ }
356
+ catch (err) {
357
+ console.error('Error sending ICE candidate:', err);
358
+ }
359
+ }
360
+ }
361
+ };
287
362
  return {
288
363
  serviceId: data.serviceId,
289
364
  uuid: data.uuid,
290
365
  offerId: data.offerId,
291
- expiresAt: data.expiresAt
366
+ offerSdp,
367
+ expiresAt: data.expiresAt,
368
+ peerConnection: pc, // Keep peer connection alive
369
+ dataChannel // Keep data channel alive
292
370
  };
293
371
  }
294
372
  /**
@@ -298,8 +376,8 @@ export class ServicePool {
298
376
  if (!this.offerPool) {
299
377
  throw new Error('Pool not started');
300
378
  }
301
- const offers = await this.createOffers(count);
302
- await this.offerPool.addOffers(offers);
379
+ const result = await this.createOffers(count);
380
+ await this.offerPool.addOffers(result.offers, result.peerConnections, result.dataChannels);
303
381
  this.updateStatus();
304
382
  }
305
383
  /**
package/dist/usernames.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as ed25519 from '@noble/ed25519';
2
2
  // Set SHA-512 hash function for ed25519 (required in @noble/ed25519 v3+)
3
- // Uses built-in WebCrypto API
3
+ // Uses built-in WebCrypto API which only provides async digest
4
+ // We use the async ed25519 functions (signAsync, verifyAsync, getPublicKeyAsync)
4
5
  ed25519.hashes.sha512Async = async (message) => {
5
6
  return new Uint8Array(await crypto.subtle.digest('SHA-512', message));
6
7
  };
@@ -31,7 +32,7 @@ export class RondevuUsername {
31
32
  */
32
33
  async generateKeypair() {
33
34
  const privateKey = ed25519.utils.randomSecretKey();
34
- const publicKey = await ed25519.getPublicKey(privateKey);
35
+ const publicKey = await ed25519.getPublicKeyAsync(privateKey);
35
36
  return {
36
37
  publicKey: bytesToBase64(publicKey),
37
38
  privateKey: bytesToBase64(privateKey)
@@ -44,7 +45,7 @@ export class RondevuUsername {
44
45
  const privateKey = base64ToBytes(privateKeyBase64);
45
46
  const encoder = new TextEncoder();
46
47
  const messageBytes = encoder.encode(message);
47
- const signature = await ed25519.sign(messageBytes, privateKey);
48
+ const signature = await ed25519.signAsync(messageBytes, privateKey);
48
49
  return bytesToBase64(signature);
49
50
  }
50
51
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.8.2",
4
- "description": "TypeScript client for Rondevu DNS-like WebRTC with username claiming and service discovery",
3
+ "version": "0.9.1",
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",
7
7
  "types": "dist/index.d.ts",
package/dist/bloom.d.ts DELETED
@@ -1,30 +0,0 @@
1
- /**
2
- * Simple bloom filter implementation for peer ID exclusion
3
- * Uses multiple hash functions for better distribution
4
- */
5
- export declare class BloomFilter {
6
- private bits;
7
- private size;
8
- private numHashes;
9
- constructor(size?: number, numHashes?: number);
10
- /**
11
- * Add a peer ID to the filter
12
- */
13
- add(peerId: string): void;
14
- /**
15
- * Test if peer ID might be in the filter
16
- */
17
- test(peerId: string): boolean;
18
- /**
19
- * Get raw bits for transmission
20
- */
21
- toBytes(): Uint8Array;
22
- /**
23
- * Convert to base64 for URL parameters
24
- */
25
- toBase64(): string;
26
- /**
27
- * Simple hash function (FNV-1a variant)
28
- */
29
- private hash;
30
- }
package/dist/bloom.js DELETED
@@ -1,73 +0,0 @@
1
- /**
2
- * Simple bloom filter implementation for peer ID exclusion
3
- * Uses multiple hash functions for better distribution
4
- */
5
- export class BloomFilter {
6
- constructor(size = 1024, numHashes = 3) {
7
- this.size = size;
8
- this.numHashes = numHashes;
9
- this.bits = new Uint8Array(Math.ceil(size / 8));
10
- }
11
- /**
12
- * Add a peer ID to the filter
13
- */
14
- add(peerId) {
15
- for (let i = 0; i < this.numHashes; i++) {
16
- const hash = this.hash(peerId, i);
17
- const index = hash % this.size;
18
- const byteIndex = Math.floor(index / 8);
19
- const bitIndex = index % 8;
20
- this.bits[byteIndex] |= 1 << bitIndex;
21
- }
22
- }
23
- /**
24
- * Test if peer ID might be in the filter
25
- */
26
- test(peerId) {
27
- for (let i = 0; i < this.numHashes; i++) {
28
- const hash = this.hash(peerId, i);
29
- const index = hash % this.size;
30
- const byteIndex = Math.floor(index / 8);
31
- const bitIndex = index % 8;
32
- if (!(this.bits[byteIndex] & (1 << bitIndex))) {
33
- return false;
34
- }
35
- }
36
- return true;
37
- }
38
- /**
39
- * Get raw bits for transmission
40
- */
41
- toBytes() {
42
- return this.bits;
43
- }
44
- /**
45
- * Convert to base64 for URL parameters
46
- */
47
- toBase64() {
48
- // Convert Uint8Array to regular array then to string
49
- const binaryString = String.fromCharCode(...Array.from(this.bits));
50
- // Use btoa for browser, or Buffer for Node.js
51
- if (typeof btoa !== 'undefined') {
52
- return btoa(binaryString);
53
- }
54
- else if (typeof Buffer !== 'undefined') {
55
- return Buffer.from(this.bits).toString('base64');
56
- }
57
- else {
58
- // Fallback: manual base64 encoding
59
- throw new Error('No base64 encoding available');
60
- }
61
- }
62
- /**
63
- * Simple hash function (FNV-1a variant)
64
- */
65
- hash(str, seed) {
66
- let hash = 2166136261 ^ seed;
67
- for (let i = 0; i < str.length; i++) {
68
- hash ^= str.charCodeAt(i);
69
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
70
- }
71
- return hash >>> 0;
72
- }
73
- }
package/dist/client.d.ts DELETED
@@ -1,126 +0,0 @@
1
- import { RondevuClientOptions, CreateOfferRequest, CreateOfferResponse, AnswerRequest, AnswerResponse, PollOffererResponse, PollAnswererResponse, VersionResponse, HealthResponse, Side } from './types.js';
2
- /**
3
- * HTTP API client for Rondevu peer signaling server
4
- */
5
- export declare class RondevuAPI {
6
- private readonly baseUrl;
7
- private readonly fetchImpl;
8
- /**
9
- * Creates a new Rondevu API client instance
10
- * @param options - Client configuration options
11
- */
12
- constructor(options: RondevuClientOptions);
13
- /**
14
- * Makes an HTTP request to the Rondevu server
15
- */
16
- private request;
17
- /**
18
- * Gets server version information
19
- *
20
- * @returns Server version
21
- *
22
- * @example
23
- * ```typescript
24
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
25
- * const { version } = await api.getVersion();
26
- * console.log('Server version:', version);
27
- * ```
28
- */
29
- getVersion(): Promise<VersionResponse>;
30
- /**
31
- * Creates a new offer
32
- *
33
- * @param request - Offer details including peer ID, signaling data, and optional custom code
34
- * @returns Unique offer code (UUID or custom code)
35
- *
36
- * @example
37
- * ```typescript
38
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
39
- * const { code } = await api.createOffer({
40
- * peerId: 'peer-123',
41
- * offer: signalingData,
42
- * code: 'my-custom-code' // optional
43
- * });
44
- * console.log('Offer code:', code);
45
- * ```
46
- */
47
- createOffer(request: CreateOfferRequest): Promise<CreateOfferResponse>;
48
- /**
49
- * Sends an answer or candidate to an existing offer
50
- *
51
- * @param request - Answer details including offer code and signaling data
52
- * @returns Success confirmation
53
- *
54
- * @example
55
- * ```typescript
56
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
57
- *
58
- * // Send answer
59
- * await api.sendAnswer({
60
- * code: offerCode,
61
- * answer: answerData,
62
- * side: 'answerer'
63
- * });
64
- *
65
- * // Send candidate
66
- * await api.sendAnswer({
67
- * code: offerCode,
68
- * candidate: candidateData,
69
- * side: 'offerer'
70
- * });
71
- * ```
72
- */
73
- sendAnswer(request: AnswerRequest): Promise<AnswerResponse>;
74
- /**
75
- * Polls for offer data from the other peer
76
- *
77
- * @param code - Offer code
78
- * @param side - Which side is polling ('offerer' or 'answerer')
79
- * @returns Offer data including offers, answers, and candidates
80
- *
81
- * @example
82
- * ```typescript
83
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
84
- *
85
- * // Offerer polls for answer
86
- * const offererData = await api.poll(offerCode, 'offerer');
87
- * if (offererData.answer) {
88
- * console.log('Received answer:', offererData.answer);
89
- * }
90
- *
91
- * // Answerer polls for offer
92
- * const answererData = await api.poll(offerCode, 'answerer');
93
- * console.log('Received offer:', answererData.offer);
94
- * ```
95
- */
96
- poll(code: string, side: Side): Promise<PollOffererResponse | PollAnswererResponse>;
97
- /**
98
- * Checks server health and version
99
- *
100
- * @returns Health status, timestamp, and version
101
- *
102
- * @example
103
- * ```typescript
104
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
105
- * const health = await api.health();
106
- * console.log('Server status:', health.status);
107
- * console.log('Server version:', health.version);
108
- * ```
109
- */
110
- health(): Promise<HealthResponse>;
111
- /**
112
- * Ends a session by deleting the offer from the server
113
- *
114
- * @param code - The offer code
115
- * @returns Success confirmation
116
- *
117
- * @example
118
- * ```typescript
119
- * const api = new RondevuAPI({ baseUrl: 'https://example.com' });
120
- * await api.leave('my-offer-code');
121
- * ```
122
- */
123
- leave(code: string): Promise<{
124
- success: boolean;
125
- }>;
126
- }