@xtr-dev/rondevu-client 0.21.8 → 0.21.10
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.
- package/dist/api/client.d.ts +13 -0
- package/dist/api/client.js +16 -0
- package/dist/connections/config.d.ts +1 -1
- package/dist/connections/config.js +1 -1
- package/dist/core/offer-pool.d.ts +6 -0
- package/dist/core/offer-pool.js +69 -7
- package/dist/core/rondevu.d.ts +16 -0
- package/dist/core/rondevu.js +16 -0
- package/package.json +1 -1
package/dist/api/client.d.ts
CHANGED
|
@@ -18,6 +18,12 @@ export interface DiscoverRequest {
|
|
|
18
18
|
limit?: number;
|
|
19
19
|
offset?: number;
|
|
20
20
|
}
|
|
21
|
+
export interface CountOffersByTagsRequest {
|
|
22
|
+
tags: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface CountOffersByTagsResponse {
|
|
25
|
+
counts: Record<string, number>;
|
|
26
|
+
}
|
|
21
27
|
export interface TaggedOffer {
|
|
22
28
|
offerId: string;
|
|
23
29
|
publicKey: string;
|
|
@@ -127,6 +133,13 @@ export declare class RondevuAPI {
|
|
|
127
133
|
* @returns Paginated response if limit provided, single offer if not
|
|
128
134
|
*/
|
|
129
135
|
discover(request: DiscoverRequest): Promise<DiscoverResponse | TaggedOffer>;
|
|
136
|
+
/**
|
|
137
|
+
* Count available offers by tags
|
|
138
|
+
* Returns the count of available (unanswered, non-expired) offers for each tag
|
|
139
|
+
* @param request - Request with tags to count
|
|
140
|
+
* @returns Object mapping each tag to its offer count
|
|
141
|
+
*/
|
|
142
|
+
countOffersByTags(request: CountOffersByTagsRequest): Promise<CountOffersByTagsResponse>;
|
|
130
143
|
/**
|
|
131
144
|
* Delete an offer by ID
|
|
132
145
|
*/
|
package/dist/api/client.js
CHANGED
|
@@ -211,6 +211,22 @@ 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
|
|
218
|
+
* @returns Object mapping each tag to its offer count
|
|
219
|
+
*/
|
|
220
|
+
async countOffersByTags(request) {
|
|
221
|
+
const rpcRequest = {
|
|
222
|
+
method: 'countOffersByTags',
|
|
223
|
+
params: {
|
|
224
|
+
tags: request.tags,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const authHeaders = await this.generateAuthHeaders(rpcRequest);
|
|
228
|
+
return await this.rpc(rpcRequest, authHeaders);
|
|
229
|
+
}
|
|
214
230
|
/**
|
|
215
231
|
* Delete an offer by ID
|
|
216
232
|
*/
|
|
@@ -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:
|
|
9
|
+
/** Maximum time to wait for connection (ms). Default: 10000 */
|
|
10
10
|
timeout?: number;
|
|
11
11
|
/** Enable automatic reconnection on failures. Default: true */
|
|
12
12
|
reconnect?: boolean;
|
|
@@ -52,6 +52,7 @@ export declare class OfferPool extends EventEmitter<OfferPoolEvents> {
|
|
|
52
52
|
private readonly debugEnabled;
|
|
53
53
|
private readonly offerCreationThrottleMs;
|
|
54
54
|
private readonly activeConnections;
|
|
55
|
+
private readonly rotatedOfferIds;
|
|
55
56
|
private readonly matchedTagsByOffer;
|
|
56
57
|
private readonly fillLock;
|
|
57
58
|
private running;
|
|
@@ -123,6 +124,11 @@ export declare class OfferPool extends EventEmitter<OfferPoolEvents> {
|
|
|
123
124
|
* Called by Rondevu when a poll:ice event is received
|
|
124
125
|
*/
|
|
125
126
|
handlePollIce(data: PollIceEvent): void;
|
|
127
|
+
/**
|
|
128
|
+
* Resolve an offerId through the rotation chain to find the current offerId
|
|
129
|
+
* Returns the final offerId or undefined if not found
|
|
130
|
+
*/
|
|
131
|
+
private resolveRotatedOfferId;
|
|
126
132
|
/**
|
|
127
133
|
* Debug logging (only if debug enabled)
|
|
128
134
|
*/
|
package/dist/core/offer-pool.js
CHANGED
|
@@ -11,6 +11,7 @@ export class OfferPool extends EventEmitter {
|
|
|
11
11
|
super();
|
|
12
12
|
// State
|
|
13
13
|
this.activeConnections = new Map();
|
|
14
|
+
this.rotatedOfferIds = new Map(); // Maps old offerId -> new offerId for late-arriving answers
|
|
14
15
|
this.matchedTagsByOffer = new Map(); // Track matchedTags from answers
|
|
15
16
|
this.fillLock = new AsyncLock();
|
|
16
17
|
this.running = false;
|
|
@@ -57,6 +58,8 @@ export class OfferPool extends EventEmitter {
|
|
|
57
58
|
connection.close();
|
|
58
59
|
}
|
|
59
60
|
this.activeConnections.clear();
|
|
61
|
+
this.rotatedOfferIds.clear();
|
|
62
|
+
this.matchedTagsByOffer.clear();
|
|
60
63
|
}
|
|
61
64
|
/**
|
|
62
65
|
* Get count of active offers
|
|
@@ -230,20 +233,37 @@ export class OfferPool extends EventEmitter {
|
|
|
230
233
|
return;
|
|
231
234
|
}
|
|
232
235
|
this.debug(`Proceeding with rotation for offer ${currentOfferId}`);
|
|
236
|
+
// Track new RTCPeerConnection for cleanup if rotation fails
|
|
237
|
+
let newPcForCleanup = null;
|
|
233
238
|
try {
|
|
234
239
|
// Create new offer and rebind existing connection
|
|
235
240
|
const { newOfferId, pc, dc } = await this.createNewOfferForRotation();
|
|
236
|
-
//
|
|
241
|
+
newPcForCleanup = pc; // Track for cleanup if rebind fails
|
|
242
|
+
// Rebind the connection to new offer (this closes the old pc)
|
|
237
243
|
await connection.rebindToOffer(newOfferId, pc, dc);
|
|
244
|
+
newPcForCleanup = null; // Rebind succeeded, pc is now managed by connection
|
|
238
245
|
// Update map: remove old offerId, add new offerId with same connection
|
|
239
246
|
this.activeConnections.delete(currentOfferId);
|
|
240
247
|
this.activeConnections.set(newOfferId, connection);
|
|
248
|
+
// Track rotation so late-arriving answers for old offerId can be forwarded
|
|
249
|
+
this.rotatedOfferIds.set(currentOfferId, newOfferId);
|
|
241
250
|
this.emit('connection:rotated', currentOfferId, newOfferId, connection);
|
|
242
251
|
this.debug(`Connection rotated: ${currentOfferId} → ${newOfferId}`);
|
|
243
252
|
}
|
|
244
253
|
catch (rotationError) {
|
|
245
|
-
// If rotation fails,
|
|
254
|
+
// If rotation fails, clean up all RTCPeerConnections to prevent resource leak
|
|
246
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();
|
|
247
267
|
this.activeConnections.delete(currentOfferId);
|
|
248
268
|
this.emit('offer:failed', currentOfferId, error);
|
|
249
269
|
this.fillOffers(); // Create replacement
|
|
@@ -268,12 +288,25 @@ export class OfferPool extends EventEmitter {
|
|
|
268
288
|
async handlePollAnswer(data) {
|
|
269
289
|
if (!this.running)
|
|
270
290
|
return;
|
|
271
|
-
|
|
291
|
+
// Find connection - check direct mapping first, then rotated offers
|
|
292
|
+
let connection = this.activeConnections.get(data.offerId);
|
|
293
|
+
let effectiveOfferId = data.offerId;
|
|
294
|
+
if (!connection) {
|
|
295
|
+
// Check if this is a late-arriving answer for a rotated offer
|
|
296
|
+
const newOfferId = this.resolveRotatedOfferId(data.offerId);
|
|
297
|
+
if (newOfferId && newOfferId !== data.offerId) {
|
|
298
|
+
connection = this.activeConnections.get(newOfferId);
|
|
299
|
+
effectiveOfferId = newOfferId;
|
|
300
|
+
if (connection) {
|
|
301
|
+
this.debug(`Late answer for rotated offer ${data.offerId} → forwarding to ${newOfferId}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
272
305
|
if (connection) {
|
|
273
|
-
this.debug(`Processing answer for offer ${
|
|
306
|
+
this.debug(`Processing answer for offer ${effectiveOfferId}`);
|
|
274
307
|
// Store matchedTags for when connection opens
|
|
275
308
|
if (data.matchedTags) {
|
|
276
|
-
this.matchedTagsByOffer.set(
|
|
309
|
+
this.matchedTagsByOffer.set(effectiveOfferId, data.matchedTags);
|
|
277
310
|
}
|
|
278
311
|
try {
|
|
279
312
|
await connection.processAnswer(data.sdp, data.answererPublicKey);
|
|
@@ -281,7 +314,7 @@ export class OfferPool extends EventEmitter {
|
|
|
281
314
|
this.fillOffers();
|
|
282
315
|
}
|
|
283
316
|
catch (err) {
|
|
284
|
-
this.debug(`Failed to process answer for offer ${
|
|
317
|
+
this.debug(`Failed to process answer for offer ${effectiveOfferId}:`, err);
|
|
285
318
|
}
|
|
286
319
|
}
|
|
287
320
|
// Silently ignore answers for offers we don't have - they may be for other connections
|
|
@@ -293,13 +326,42 @@ export class OfferPool extends EventEmitter {
|
|
|
293
326
|
handlePollIce(data) {
|
|
294
327
|
if (!this.running)
|
|
295
328
|
return;
|
|
296
|
-
|
|
329
|
+
// Find connection - check direct mapping first, then rotated offers
|
|
330
|
+
let connection = this.activeConnections.get(data.offerId);
|
|
331
|
+
if (!connection) {
|
|
332
|
+
// Check if this is late-arriving ICE for a rotated offer
|
|
333
|
+
const newOfferId = this.resolveRotatedOfferId(data.offerId);
|
|
334
|
+
if (newOfferId && newOfferId !== data.offerId) {
|
|
335
|
+
connection = this.activeConnections.get(newOfferId);
|
|
336
|
+
if (connection) {
|
|
337
|
+
this.debug(`Late ICE for rotated offer ${data.offerId} → forwarding to ${newOfferId}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
297
341
|
if (connection) {
|
|
298
342
|
this.debug(`Processing ${data.candidates.length} ICE candidates for offer ${data.offerId}`);
|
|
299
343
|
connection.handleRemoteIceCandidates(data.candidates);
|
|
300
344
|
}
|
|
301
345
|
// Silently ignore ICE candidates for offers we don't have
|
|
302
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Resolve an offerId through the rotation chain to find the current offerId
|
|
349
|
+
* Returns the final offerId or undefined if not found
|
|
350
|
+
*/
|
|
351
|
+
resolveRotatedOfferId(offerId) {
|
|
352
|
+
let currentId = offerId;
|
|
353
|
+
const visited = new Set();
|
|
354
|
+
while (this.rotatedOfferIds.has(currentId)) {
|
|
355
|
+
if (visited.has(currentId)) {
|
|
356
|
+
// Circular reference - shouldn't happen but protect against it
|
|
357
|
+
this.debug(`Circular rotation detected for ${offerId}`);
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
visited.add(currentId);
|
|
361
|
+
currentId = this.rotatedOfferIds.get(currentId);
|
|
362
|
+
}
|
|
363
|
+
return currentId !== offerId ? currentId : undefined;
|
|
364
|
+
}
|
|
303
365
|
/**
|
|
304
366
|
* Debug logging (only if debug enabled)
|
|
305
367
|
*/
|
package/dist/core/rondevu.d.ts
CHANGED
|
@@ -255,6 +255,22 @@ 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
|
+
* @returns Object mapping each tag to its offer count
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* const counts = await rondevu.countOffersByTags(['chat', 'video'])
|
|
268
|
+
* console.log(counts.counts) // { chat: 5, video: 3 }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
countOffersByTags(tags: string[]): Promise<{
|
|
272
|
+
counts: Record<string, number>;
|
|
273
|
+
}>;
|
|
258
274
|
/**
|
|
259
275
|
* Post answer SDP to specific offer
|
|
260
276
|
*/
|
package/dist/core/rondevu.js
CHANGED
|
@@ -446,6 +446,22 @@ 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
|
+
* @returns Object mapping each tag to its offer count
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```typescript
|
|
458
|
+
* const counts = await rondevu.countOffersByTags(['chat', 'video'])
|
|
459
|
+
* console.log(counts.counts) // { chat: 5, video: 3 }
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
async countOffersByTags(tags) {
|
|
463
|
+
return await this.api.countOffersByTags({ tags });
|
|
464
|
+
}
|
|
449
465
|
// ============================================
|
|
450
466
|
// WebRTC Signaling
|
|
451
467
|
// ============================================
|
package/package.json
CHANGED