diff --git a/package.json b/package.json index 7f65755..ecdf58f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/rondevu-server", - "version": "0.1.0", + "version": "0.1.1", "description": "Topic-based peer discovery and signaling server for distributed P2P applications", "main": "dist/index.js", "scripts": { diff --git a/src/app.ts b/src/app.ts index cce9984..f950480 100644 --- a/src/app.ts +++ b/src/app.ts @@ -231,22 +231,29 @@ export function createApp(storage: Storage, config: Config) { * GET /topics * List all topics with active peer counts (paginated) * Public endpoint (no auth required) + * Query params: + * - limit: Max topics to return (default 50, max 200) + * - offset: Number of topics to skip (default 0) + * - startsWith: Filter topics starting with this prefix (optional) */ app.get('/topics', async (c) => { try { const limitParam = c.req.query('limit'); const offsetParam = c.req.query('offset'); + const startsWithParam = c.req.query('startsWith'); const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50; const offset = offsetParam ? parseInt(offsetParam, 10) : 0; + const startsWith = startsWithParam || undefined; - const result = await storage.getTopics(limit, offset); + const result = await storage.getTopics(limit, offset, startsWith); return c.json({ topics: result.topics, total: result.total, limit, - offset + offset, + ...(startsWith && { startsWith }) }, 200); } catch (err) { console.error('Error fetching topics:', err); diff --git a/src/storage/d1.ts b/src/storage/d1.ts index 2ec9a60..6ed6647 100644 --- a/src/storage/d1.ts +++ b/src/storage/d1.ts @@ -296,32 +296,51 @@ export class D1Storage implements Storage { return candidates; } - async getTopics(limit: number, offset: number): Promise<{ + async getTopics(limit: number, offset: number, startsWith?: string): Promise<{ topics: TopicInfo[]; total: number; }> { + const now = Date.now(); + + // Build WHERE clause for startsWith filter + const whereClause = startsWith + ? 'o.expires_at > ? AND ot.topic LIKE ?' + : 'o.expires_at > ?'; + + const startsWithPattern = startsWith ? `${startsWith}%` : null; + // Get total count of topics with active offers - const countResult = await this.db.prepare(` + const countQuery = ` SELECT COUNT(DISTINCT ot.topic) as count FROM offer_topics ot INNER JOIN offers o ON ot.offer_id = o.id - WHERE o.expires_at > ? - `).bind(Date.now()).first(); + WHERE ${whereClause} + `; + + const countStmt = this.db.prepare(countQuery); + const countResult = startsWith + ? await countStmt.bind(now, startsWithPattern).first() + : await countStmt.bind(now).first(); const total = (countResult as any)?.count || 0; // Get topics with peer counts (paginated) - const topicsResult = await this.db.prepare(` + const topicsQuery = ` SELECT ot.topic, COUNT(DISTINCT o.peer_id) as active_peers FROM offer_topics ot INNER JOIN offers o ON ot.offer_id = o.id - WHERE o.expires_at > ? + WHERE ${whereClause} GROUP BY ot.topic ORDER BY active_peers DESC, ot.topic ASC LIMIT ? OFFSET ? - `).bind(Date.now(), limit, offset).all(); + `; + + const topicsStmt = this.db.prepare(topicsQuery); + const topicsResult = startsWith + ? await topicsStmt.bind(now, startsWithPattern, limit, offset).all() + : await topicsStmt.bind(now, limit, offset).all(); const topics = (topicsResult.results || []).map((row: any) => ({ topic: row.topic, diff --git a/src/storage/sqlite.ts b/src/storage/sqlite.ts index 5c91ea8..63851be 100644 --- a/src/storage/sqlite.ts +++ b/src/storage/sqlite.ts @@ -305,35 +305,50 @@ export class SQLiteStorage implements Storage { })); } - async getTopics(limit: number, offset: number): Promise<{ + async getTopics(limit: number, offset: number, startsWith?: string): Promise<{ topics: TopicInfo[]; total: number; }> { + const now = Date.now(); + + // Build WHERE clause for startsWith filter + const whereClause = startsWith + ? 'o.expires_at > ? AND ot.topic LIKE ?' + : 'o.expires_at > ?'; + + const startsWithPattern = startsWith ? `${startsWith}%` : null; + // Get total count of topics with active offers - const countStmt = this.db.prepare(` + const countQuery = ` SELECT COUNT(DISTINCT ot.topic) as count FROM offer_topics ot INNER JOIN offers o ON ot.offer_id = o.id - WHERE o.expires_at > ? - `); + WHERE ${whereClause} + `; - const countRow = countStmt.get(Date.now()) as any; + const countStmt = this.db.prepare(countQuery); + const countParams = startsWith ? [now, startsWithPattern] : [now]; + const countRow = countStmt.get(...countParams) as any; const total = countRow.count; // Get topics with peer counts (paginated) - const topicsStmt = this.db.prepare(` + const topicsQuery = ` SELECT ot.topic, COUNT(DISTINCT o.peer_id) as active_peers FROM offer_topics ot INNER JOIN offers o ON ot.offer_id = o.id - WHERE o.expires_at > ? + WHERE ${whereClause} GROUP BY ot.topic ORDER BY active_peers DESC, ot.topic ASC LIMIT ? OFFSET ? - `); + `; - const rows = topicsStmt.all(Date.now(), limit, offset) as any[]; + const topicsStmt = this.db.prepare(topicsQuery); + const topicsParams = startsWith + ? [now, startsWithPattern, limit, offset] + : [now, limit, offset]; + const rows = topicsStmt.all(...topicsParams) as any[]; const topics = rows.map(row => ({ topic: row.topic, diff --git a/src/storage/types.ts b/src/storage/types.ts index 8fb75b8..f609470 100644 --- a/src/storage/types.ts +++ b/src/storage/types.ts @@ -146,9 +146,10 @@ export interface Storage { * Retrieves topics with active peer counts (paginated) * @param limit Maximum number of topics to return * @param offset Number of topics to skip + * @param startsWith Optional prefix filter - only return topics starting with this string * @returns Object with topics array and total count */ - getTopics(limit: number, offset: number): Promise<{ + getTopics(limit: number, offset: number, startsWith?: string): Promise<{ topics: TopicInfo[]; total: number; }>;