From ba9fb109f6e74864e47beab823612344e5ad408d Mon Sep 17 00:00:00 2001 From: Gigi Date: Thu, 16 Oct 2025 14:31:39 +0200 Subject: [PATCH] refactor(api): DRY improvements for article OG endpoint - Extract fetchEventsFromRelays helper to eliminate duplication - Add setCacheHeaders helper for consistent header setting - Parallelize article and profile fetching for faster response - Move relayPool.close() to finally block to prevent leaks - Remove redundant cacheKey variable and sorting --- api/article-og.ts | 122 ++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 68 deletions(-) diff --git a/api/article-og.ts b/api/article-og.ts index 38a104fb..29ae92bb 100644 --- a/api/article-og.ts +++ b/api/article-og.ts @@ -38,6 +38,11 @@ function escapeHtml(text: string): string { .replace(/'/g, ''') } +function setCacheHeaders(res: VercelResponse, maxAge: number = 86400): void { + res.setHeader('Cache-Control', `public, max-age=${maxAge}, s-maxage=604800`) + res.setHeader('Content-Type', 'text/html; charset=utf-8') +} + interface ArticleMetadata { title: string summary: string @@ -46,6 +51,35 @@ interface ArticleMetadata { published?: number } +async function fetchEventsFromRelays( + relayPool: RelayPool, + relayUrls: string[], + filter: any, + timeoutMs: number +): Promise { + const events: NostrEvent[] = [] + + await new Promise((resolve) => { + const timeout = setTimeout(() => resolve(), timeoutMs) + + relayPool.req(relayUrls, filter).subscribe({ + next: (msg) => { + if (msg.type === 'EVENT') { + events.push(msg.event) + } + }, + error: () => resolve(), + complete: () => { + clearTimeout(timeout) + resolve() + } + }) + }) + + // Sort by created_at and return most recent first + return events.sort((a, b) => b.created_at - a.created_at) +} + async function fetchArticleMetadata(naddr: string): Promise { const relayPool = new RelayPool() @@ -62,68 +96,25 @@ async function fetchArticleMetadata(naddr: string): Promise 0 ? pointer.relays : RELAYS relayUrls.forEach(url => relayPool.open(url)) - // Fetch article (kind:30023) - const articleFilter = { - kinds: [pointer.kind], - authors: [pointer.pubkey], - '#d': [pointer.identifier || ''] - } - - const articleEvents: NostrEvent[] = [] - - await new Promise((resolve) => { - const timeout = setTimeout(() => resolve(), 5000) - - relayPool.req(relayUrls, articleFilter).subscribe({ - next: (msg) => { - if (msg.type === 'EVENT') { - articleEvents.push(msg.event) - } - }, - error: () => resolve(), - complete: () => { - clearTimeout(timeout) - resolve() - } - }) - }) + // Fetch article and profile in parallel + const [articleEvents, profileEvents] = await Promise.all([ + fetchEventsFromRelays(relayPool, relayUrls, { + kinds: [pointer.kind], + authors: [pointer.pubkey], + '#d': [pointer.identifier || ''] + }, 5000), + fetchEventsFromRelays(relayPool, relayUrls, { + kinds: [0], + authors: [pointer.pubkey] + }, 3000) + ]) if (articleEvents.length === 0) { - relayPool.close() return null } - // Sort by created_at and take most recent - articleEvents.sort((a, b) => b.created_at - a.created_at) const article = articleEvents[0] - // Fetch author profile (kind:0) - const profileFilter = { - kinds: [0], - authors: [pointer.pubkey] - } - - const profileEvents: NostrEvent[] = [] - - await new Promise((resolve) => { - const timeout = setTimeout(() => resolve(), 3000) - - relayPool.req(relayUrls, profileFilter).subscribe({ - next: (msg) => { - if (msg.type === 'EVENT') { - profileEvents.push(msg.event) - } - }, - error: () => resolve(), - complete: () => { - clearTimeout(timeout) - resolve() - } - }) - }) - - relayPool.close() - // Extract article metadata const title = getArticleTitle(article) || 'Untitled Article' const summary = getArticleSummary(article) || 'Read this article on Boris' @@ -132,10 +123,8 @@ async function fetchArticleMetadata(naddr: string): Promise 0) { - profileEvents.sort((a, b) => b.created_at - a.created_at) - const profile = profileEvents[0] try { - const profileData = JSON.parse(profile.content) + const profileData = JSON.parse(profileEvents[0].content) authorName = profileData.display_name || profileData.name || authorName } catch { // Use fallback @@ -151,8 +140,9 @@ async function fetchArticleMetadata(naddr: string): Promise now) { - res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800') - res.setHeader('Content-Type', 'text/html; charset=utf-8') + setCacheHeaders(res) return res.status(200).send(cached.html) } @@ -234,19 +222,17 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { const html = generateHtml(naddr, meta) // Cache the result - memoryCache.set(cacheKey, { html, expires: now + WEEK_MS }) + memoryCache.set(naddr, { html, expires: now + WEEK_MS }) // Send response - res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800') - res.setHeader('Content-Type', 'text/html; charset=utf-8') + setCacheHeaders(res) return res.status(200).send(html) } catch (err) { console.error('Error generating article OG HTML:', err) // Fallback to basic HTML with SPA boot const html = generateHtml(naddr, null) - res.setHeader('Cache-Control', 'public, max-age=3600') - res.setHeader('Content-Type', 'text/html; charset=utf-8') + setCacheHeaders(res, 3600) return res.status(200).send(html) } }