@xtr-dev/rondevu-client 0.21.9 → 0.21.11

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.
@@ -18,6 +18,13 @@ export interface DiscoverRequest {
18
18
  limit?: number;
19
19
  offset?: number;
20
20
  }
21
+ export interface CountOffersByTagsRequest {
22
+ tags: string[];
23
+ unique?: boolean;
24
+ }
25
+ export interface CountOffersByTagsResponse {
26
+ counts: Record<string, number>;
27
+ }
21
28
  export interface TaggedOffer {
22
29
  offerId: string;
23
30
  publicKey: string;
@@ -127,6 +134,13 @@ export declare class RondevuAPI {
127
134
  * @returns Paginated response if limit provided, single offer if not
128
135
  */
129
136
  discover(request: DiscoverRequest): Promise<DiscoverResponse | TaggedOffer>;
137
+ /**
138
+ * Count available offers by tags
139
+ * Returns the count of available (unanswered, non-expired) offers for each tag
140
+ * @param request - Request with tags to count and optional unique flag
141
+ * @returns Object mapping each tag to its count
142
+ */
143
+ countOffersByTags(request: CountOffersByTagsRequest): Promise<CountOffersByTagsResponse>;
130
144
  /**
131
145
  * Delete an offer by ID
132
146
  */
@@ -211,6 +211,23 @@ export class RondevuAPI {
211
211
  const authHeaders = await this.generateAuthHeaders(rpcRequest);
212
212
  return await this.rpc(rpcRequest, authHeaders);
213
213
  }
214
+ /**
215
+ * Count available offers by tags
216
+ * Returns the count of available (unanswered, non-expired) offers for each tag
217
+ * @param request - Request with tags to count and optional unique flag
218
+ * @returns Object mapping each tag to its count
219
+ */
220
+ async countOffersByTags(request) {
221
+ const rpcRequest = {
222
+ method: 'countOffersByTags',
223
+ params: {
224
+ tags: request.tags,
225
+ ...(request.unique !== undefined && { unique: request.unique }),
226
+ },
227
+ };
228
+ const authHeaders = await this.generateAuthHeaders(rpcRequest);
229
+ return await this.rpc(rpcRequest, authHeaders);
230
+ }
214
231
  /**
215
232
  * Delete an offer by ID
216
233
  */
@@ -123,6 +123,9 @@ export class RondevuConnection extends EventEmitter {
123
123
  if (this.state === ConnectionState.SIGNALING) {
124
124
  this.transitionTo(ConnectionState.CHECKING, 'ICE checking started');
125
125
  }
126
+ // ICE is progressing - clear the connection timeout to give ICE time to complete
127
+ // The timeout was for signaling, ICE checking shows progress is being made
128
+ this.clearConnectionTimeout();
126
129
  // Note: ICE candidate polling is handled by PollingManager
127
130
  // Candidates are received via handleRemoteIceCandidates()
128
131
  break;
@@ -6,7 +6,7 @@
6
6
  * Advanced options use sensible defaults.
7
7
  */
8
8
  export interface ConnectionOptions {
9
- /** Maximum time to wait for connection (ms). Default: 10000 */
9
+ /** Maximum time to wait for connection (ms). Default: 30000 */
10
10
  timeout?: number;
11
11
  /** Enable automatic reconnection on failures. Default: true */
12
12
  reconnect?: boolean;
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const DEFAULT_CONNECTION_CONFIG = {
5
5
  // Timeouts
6
- connectionTimeout: 10000, // 10 seconds
6
+ connectionTimeout: 30000, // 30 seconds (should be less than server offer TTL)
7
7
  iceGatheringTimeout: 10000, // 10 seconds
8
8
  // Reconnection
9
9
  reconnectEnabled: true,
@@ -233,11 +233,15 @@ export class OfferPool extends EventEmitter {
233
233
  return;
234
234
  }
235
235
  this.debug(`Proceeding with rotation for offer ${currentOfferId}`);
236
+ // Track new RTCPeerConnection for cleanup if rotation fails
237
+ let newPcForCleanup = null;
236
238
  try {
237
239
  // Create new offer and rebind existing connection
238
240
  const { newOfferId, pc, dc } = await this.createNewOfferForRotation();
239
- // Rebind the connection to new offer
241
+ newPcForCleanup = pc; // Track for cleanup if rebind fails
242
+ // Rebind the connection to new offer (this closes the old pc)
240
243
  await connection.rebindToOffer(newOfferId, pc, dc);
244
+ newPcForCleanup = null; // Rebind succeeded, pc is now managed by connection
241
245
  // Update map: remove old offerId, add new offerId with same connection
242
246
  this.activeConnections.delete(currentOfferId);
243
247
  this.activeConnections.set(newOfferId, connection);
@@ -247,8 +251,19 @@ export class OfferPool extends EventEmitter {
247
251
  this.debug(`Connection rotated: ${currentOfferId} → ${newOfferId}`);
248
252
  }
249
253
  catch (rotationError) {
250
- // If rotation fails, fall back to destroying connection
254
+ // If rotation fails, clean up all RTCPeerConnections to prevent resource leak
251
255
  this.debug(`Rotation failed for ${currentOfferId}:`, rotationError);
256
+ // Close the new pc if it was created but rebind failed
257
+ if (newPcForCleanup) {
258
+ try {
259
+ newPcForCleanup.close();
260
+ }
261
+ catch {
262
+ // Ignore close errors
263
+ }
264
+ }
265
+ // Close the old connection (closes its RTCPeerConnection)
266
+ connection.close();
252
267
  this.activeConnections.delete(currentOfferId);
253
268
  this.emit('offer:failed', currentOfferId, error);
254
269
  this.fillOffers(); // Create replacement
@@ -30,6 +30,7 @@ export class PollingManager extends EventEmitter {
30
30
  this.debug('Already running');
31
31
  return;
32
32
  }
33
+ console.log('[PollingManager] Starting polling manager');
33
34
  this.debug('Starting polling manager');
34
35
  this.running = true;
35
36
  // Poll immediately
@@ -74,6 +75,11 @@ export class PollingManager extends EventEmitter {
74
75
  return;
75
76
  try {
76
77
  const result = await this.api.poll(this.lastPollTimestamp);
78
+ // Log poll results for debugging (only when there are results)
79
+ const iceCount = Object.values(result.iceCandidates).reduce((sum, candidates) => sum + candidates.length, 0);
80
+ if (result.answers.length > 0 || iceCount > 0) {
81
+ console.log(`[PollingManager] Poll: ${result.answers.length} answers, ${iceCount} ICE (since: ${this.lastPollTimestamp})`);
82
+ }
77
83
  // Emit answer events
78
84
  for (const answer of result.answers) {
79
85
  this.debug(`Poll: answer for ${answer.offerId}`);
@@ -255,6 +255,26 @@ export declare class Rondevu extends EventEmitter {
255
255
  * ```
256
256
  */
257
257
  discover(tags: string[], options?: DiscoverOptions): Promise<DiscoverResult>;
258
+ /**
259
+ * Count available offers by tags
260
+ * Returns the count of available (unanswered, non-expired) offers for each tag
261
+ *
262
+ * @param tags - Tags to count offers for
263
+ * @param unique - If true, count unique public keys instead of total offers
264
+ * @returns Object mapping each tag to its count
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * const counts = await rondevu.countOffersByTags(['chat', 'video'])
269
+ * console.log(counts.counts) // { chat: 5, video: 3 }
270
+ *
271
+ * // Count unique peers instead of total offers
272
+ * const uniquePeers = await rondevu.countOffersByTags(['chat'], true)
273
+ * ```
274
+ */
275
+ countOffersByTags(tags: string[], unique?: boolean): Promise<{
276
+ counts: Record<string, number>;
277
+ }>;
258
278
  /**
259
279
  * Post answer SDP to specific offer
260
280
  */
@@ -446,6 +446,26 @@ export class Rondevu extends EventEmitter {
446
446
  // Always pass limit to ensure we get DiscoverResponse (paginated mode)
447
447
  return (await this.api.discover({ tags, limit, offset }));
448
448
  }
449
+ /**
450
+ * Count available offers by tags
451
+ * Returns the count of available (unanswered, non-expired) offers for each tag
452
+ *
453
+ * @param tags - Tags to count offers for
454
+ * @param unique - If true, count unique public keys instead of total offers
455
+ * @returns Object mapping each tag to its count
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * const counts = await rondevu.countOffersByTags(['chat', 'video'])
460
+ * console.log(counts.counts) // { chat: 5, video: 3 }
461
+ *
462
+ * // Count unique peers instead of total offers
463
+ * const uniquePeers = await rondevu.countOffersByTags(['chat'], true)
464
+ * ```
465
+ */
466
+ async countOffersByTags(tags, unique) {
467
+ return await this.api.countOffersByTags({ tags, unique });
468
+ }
449
469
  // ============================================
450
470
  // WebRTC Signaling
451
471
  // ============================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.21.9",
3
+ "version": "0.21.11",
4
4
  "description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",