@xtr-dev/rondevu-server 0.5.22 → 0.5.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-server",
3
- "version": "0.5.22",
3
+ "version": "0.5.25",
4
4
  "description": "DNS-like WebRTC signaling server with credential-based authentication and service discovery",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/config.ts CHANGED
@@ -95,7 +95,7 @@ export const CONFIG_DEFAULTS = {
95
95
  offerDefaultTtl: 60000,
96
96
  offerMaxTtl: 86400000,
97
97
  offerMinTtl: 60000,
98
- answeredOfferTtl: 30000, // 30 seconds TTL after offer is answered
98
+ answeredOfferTtl: 60000, // 60 seconds TTL after offer is answered
99
99
  cleanupInterval: 60000,
100
100
  maxOffersPerRequest: 100,
101
101
  maxBatchSize: 100,
package/src/rpc.ts CHANGED
@@ -130,6 +130,7 @@ export interface DiscoverParams {
130
130
 
131
131
  export interface CountOffersByTagsParams {
132
132
  tags: string[];
133
+ unique?: boolean;
133
134
  }
134
135
 
135
136
  export interface PublishOfferParams {
@@ -308,14 +309,14 @@ const handlers: Record<string, RpcHandler> = {
308
309
  * Count available offers by tags
309
310
  */
310
311
  async countOffersByTags(params: CountOffersByTagsParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
311
- const { tags } = params;
312
+ const { tags, unique } = params;
312
313
 
313
314
  const tagsValidation = validateTags(tags);
314
315
  if (!tagsValidation.valid) {
315
316
  throw new RpcError(ErrorCodes.INVALID_TAG, tagsValidation.error || 'Invalid tags');
316
317
  }
317
318
 
318
- const counts = await storage.countOffersByTags(tags);
319
+ const counts = await storage.countOffersByTags(tags, unique === true);
319
320
 
320
321
  // Convert Map to object for JSON serialization
321
322
  const result: Record<string, number> = {};
@@ -482,6 +483,7 @@ const handlers: Record<string, RpcHandler> = {
482
483
 
483
484
  // Reduce TTL after answer for faster cleanup (answered offers no longer appear in discovery)
484
485
  const newExpiresAt = Date.now() + config.answeredOfferTtl;
486
+
485
487
  await storage.answerOffer(offerId, publicKey, sdp, matchedTags, newExpiresAt);
486
488
 
487
489
  return { success: true, offerId };
@@ -536,6 +538,7 @@ const handlers: Record<string, RpcHandler> = {
536
538
  const sinceTimestamp = since !== undefined ? since : 0;
537
539
 
538
540
  const answeredOffers = await storage.getAnsweredOffers(publicKey);
541
+
539
542
  const filteredAnswers = answeredOffers.filter(
540
543
  (offer) => offer.answeredAt && offer.answeredAt > sinceTimestamp
541
544
  );
package/src/storage/d1.ts CHANGED
@@ -378,48 +378,54 @@ export class D1Storage implements Storage {
378
378
  throw new Error('Too many offer IDs (max 1000)');
379
379
  }
380
380
 
381
- const placeholders = offerIds.map(() => '?').join(',');
382
-
383
- let query = `
384
- SELECT ic.*, o.public_key as offer_public_key
385
- FROM ice_candidates ic
386
- INNER JOIN offers o ON o.id = ic.offer_id
387
- WHERE ic.offer_id IN (${placeholders})
388
- AND (
389
- (o.public_key = ? AND ic.role = 'answerer')
390
- OR (o.answerer_public_key = ? AND ic.role = 'offerer')
391
- )
392
- `;
393
-
394
- const params: any[] = [...offerIds, publicKey, publicKey];
395
-
396
- if (since !== undefined) {
397
- query += ' AND ic.created_at > ?';
398
- params.push(since);
399
- }
400
-
401
- query += ' ORDER BY ic.created_at ASC';
402
-
403
- const queryResult = await this.db.prepare(query).bind(...params).all();
404
-
405
- if (!queryResult.results) {
406
- return result;
407
- }
381
+ // D1 has a limit of ~100 bind parameters per query, so batch queries
382
+ // Each query uses: offerIds.length + 2 (publicKey twice) + 1 (optional since) params
383
+ // Use batches of 90 offer IDs to stay safely under the limit
384
+ const BATCH_SIZE = 90;
385
+
386
+ for (let i = 0; i < offerIds.length; i += BATCH_SIZE) {
387
+ const batchOfferIds = offerIds.slice(i, i + BATCH_SIZE);
388
+ const placeholders = batchOfferIds.map(() => '?').join(',');
389
+
390
+ let query = `
391
+ SELECT ic.*, o.public_key as offer_public_key
392
+ FROM ice_candidates ic
393
+ INNER JOIN offers o ON o.id = ic.offer_id
394
+ WHERE ic.offer_id IN (${placeholders})
395
+ AND (
396
+ (o.public_key = ? AND ic.role = 'answerer')
397
+ OR (o.answerer_public_key = ? AND ic.role = 'offerer')
398
+ )
399
+ `;
400
+
401
+ const params: any[] = [...batchOfferIds, publicKey, publicKey];
402
+
403
+ if (since !== undefined) {
404
+ query += ' AND ic.created_at > ?';
405
+ params.push(since);
406
+ }
408
407
 
409
- for (const row of queryResult.results as any[]) {
410
- const candidate: IceCandidate = {
411
- id: row.id,
412
- offerId: row.offer_id,
413
- publicKey: row.public_key,
414
- role: row.role,
415
- candidate: JSON.parse(row.candidate),
416
- createdAt: row.created_at,
417
- };
418
-
419
- if (!result.has(row.offer_id)) {
420
- result.set(row.offer_id, []);
408
+ query += ' ORDER BY ic.created_at ASC';
409
+
410
+ const queryResult = await this.db.prepare(query).bind(...params).all();
411
+
412
+ if (queryResult.results) {
413
+ for (const row of queryResult.results as any[]) {
414
+ const candidate: IceCandidate = {
415
+ id: row.id,
416
+ offerId: row.offer_id,
417
+ publicKey: row.public_key,
418
+ role: row.role,
419
+ candidate: JSON.parse(row.candidate),
420
+ createdAt: row.created_at,
421
+ };
422
+
423
+ if (!result.has(row.offer_id)) {
424
+ result.set(row.offer_id, []);
425
+ }
426
+ result.get(row.offer_id)!.push(candidate);
427
+ }
421
428
  }
422
- result.get(row.offer_id)!.push(candidate);
423
429
  }
424
430
 
425
431
  return result;
@@ -508,7 +514,7 @@ export class D1Storage implements Storage {
508
514
  return result?.count ?? 0;
509
515
  }
510
516
 
511
- async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
517
+ async countOffersByTags(tags: string[], unique = false): Promise<Map<string, number>> {
512
518
  const result = new Map<string, number>();
513
519
  if (tags.length === 0) return result;
514
520
 
@@ -516,8 +522,9 @@ export class D1Storage implements Storage {
516
522
 
517
523
  // Query each tag individually using json_each
518
524
  for (const tag of tags) {
525
+ const countColumn = unique ? 'COUNT(DISTINCT o.public_key)' : 'COUNT(DISTINCT o.id)';
519
526
  const queryResult = await this.db.prepare(`
520
- SELECT COUNT(DISTINCT o.id) as count
527
+ SELECT ${countColumn} as count
521
528
  FROM offers o, json_each(o.tags) as t
522
529
  WHERE t.value = ?
523
530
  AND o.expires_at > ?
@@ -445,7 +445,7 @@ export class MemoryStorage implements Storage {
445
445
  return candidates ? candidates.length : 0;
446
446
  }
447
447
 
448
- async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
448
+ async countOffersByTags(tags: string[], unique = false): Promise<Map<string, number>> {
449
449
  const result = new Map<string, number>();
450
450
  if (tags.length === 0) return result;
451
451
 
@@ -458,14 +458,27 @@ export class MemoryStorage implements Storage {
458
458
  continue;
459
459
  }
460
460
 
461
- let count = 0;
462
- for (const offerId of offerIds) {
463
- const offer = this.offers.get(offerId);
464
- if (offer && offer.expiresAt > now && !offer.answererPublicKey) {
465
- count++;
461
+ if (unique) {
462
+ // Count unique public keys
463
+ const uniquePublicKeys = new Set<string>();
464
+ for (const offerId of offerIds) {
465
+ const offer = this.offers.get(offerId);
466
+ if (offer && offer.expiresAt > now && !offer.answererPublicKey) {
467
+ uniquePublicKeys.add(offer.publicKey);
468
+ }
466
469
  }
470
+ result.set(tag, uniquePublicKeys.size);
471
+ } else {
472
+ // Count total offers
473
+ let count = 0;
474
+ for (const offerId of offerIds) {
475
+ const offer = this.offers.get(offerId);
476
+ if (offer && offer.expiresAt > now && !offer.answererPublicKey) {
477
+ count++;
478
+ }
479
+ }
480
+ result.set(tag, count);
467
481
  }
468
- result.set(tag, count);
469
482
  }
470
483
 
471
484
  return result;
@@ -476,7 +476,7 @@ export class MySQLStorage implements Storage {
476
476
  return Number(rows[0].count);
477
477
  }
478
478
 
479
- async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
479
+ async countOffersByTags(tags: string[], unique = false): Promise<Map<string, number>> {
480
480
  const result = new Map<string, number>();
481
481
  if (tags.length === 0) return result;
482
482
 
@@ -484,8 +484,9 @@ export class MySQLStorage implements Storage {
484
484
 
485
485
  // Query each tag individually using JSON_CONTAINS
486
486
  for (const tag of tags) {
487
+ const countColumn = unique ? 'COUNT(DISTINCT public_key)' : 'COUNT(DISTINCT id)';
487
488
  const [rows] = await this.pool.query<RowDataPacket[]>(
488
- `SELECT COUNT(DISTINCT id) as count
489
+ `SELECT ${countColumn} as count
489
490
  FROM offers
490
491
  WHERE JSON_CONTAINS(tags, ?)
491
492
  AND expires_at > ?
@@ -484,7 +484,7 @@ export class PostgreSQLStorage implements Storage {
484
484
  return Number(result.rows[0].count);
485
485
  }
486
486
 
487
- async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
487
+ async countOffersByTags(tags: string[], unique = false): Promise<Map<string, number>> {
488
488
  const result = new Map<string, number>();
489
489
  if (tags.length === 0) return result;
490
490
 
@@ -492,8 +492,9 @@ export class PostgreSQLStorage implements Storage {
492
492
 
493
493
  // Query each tag individually using JSONB containment
494
494
  for (const tag of tags) {
495
+ const countColumn = unique ? 'COUNT(DISTINCT public_key)' : 'COUNT(DISTINCT id)';
495
496
  const queryResult = await this.pool.query(
496
- `SELECT COUNT(DISTINCT id) as count
497
+ `SELECT ${countColumn} as count
497
498
  FROM offers
498
499
  WHERE tags ? $1
499
500
  AND expires_at > $2
@@ -543,7 +543,7 @@ export class SQLiteStorage implements Storage {
543
543
  return result.count;
544
544
  }
545
545
 
546
- async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
546
+ async countOffersByTags(tags: string[], unique = false): Promise<Map<string, number>> {
547
547
  const result = new Map<string, number>();
548
548
  if (tags.length === 0) return result;
549
549
 
@@ -551,8 +551,9 @@ export class SQLiteStorage implements Storage {
551
551
 
552
552
  // Query counts for each tag individually for accuracy
553
553
  // (an offer with multiple matching tags should only count once per tag)
554
+ const countColumn = unique ? 'COUNT(DISTINCT o.public_key)' : 'COUNT(DISTINCT o.id)';
554
555
  const stmt = this.db.prepare(`
555
- SELECT COUNT(DISTINCT o.id) as count
556
+ SELECT ${countColumn} as count
556
557
  FROM offers o, json_each(o.tags) as t
557
558
  WHERE t.value = ?
558
559
  AND o.expires_at > ?
@@ -259,7 +259,8 @@ export interface Storage {
259
259
  /**
260
260
  * Counts available (unanswered, non-expired) offers for each of the given tags
261
261
  * @param tags Array of tags to count offers for
262
- * @returns Map of tag to offer count
262
+ * @param unique If true, count unique public keys instead of total offers
263
+ * @returns Map of tag to count
263
264
  */
264
- countOffersByTags(tags: string[]): Promise<Map<string, number>>;
265
+ countOffersByTags(tags: string[], unique?: boolean): Promise<Map<string, number>>;
265
266
  }