@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/.github/workflows/docker-publish.yml +1 -0
- package/dist/index.js +106 -4
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/src/config.ts +1 -1
- package/src/rpc.ts +5 -2
- package/src/storage/d1.ts +49 -42
- package/src/storage/memory.ts +20 -7
- package/src/storage/mysql.ts +3 -2
- package/src/storage/postgres.ts +3 -2
- package/src/storage/sqlite.ts +3 -2
- package/src/storage/types.ts +3 -2
package/package.json
CHANGED
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:
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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 > ?
|
package/src/storage/memory.ts
CHANGED
|
@@ -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
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
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;
|
package/src/storage/mysql.ts
CHANGED
|
@@ -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
|
|
489
|
+
`SELECT ${countColumn} as count
|
|
489
490
|
FROM offers
|
|
490
491
|
WHERE JSON_CONTAINS(tags, ?)
|
|
491
492
|
AND expires_at > ?
|
package/src/storage/postgres.ts
CHANGED
|
@@ -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
|
|
497
|
+
`SELECT ${countColumn} as count
|
|
497
498
|
FROM offers
|
|
498
499
|
WHERE tags ? $1
|
|
499
500
|
AND expires_at > $2
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -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
|
|
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 > ?
|
package/src/storage/types.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
}
|