From a87abb6d97e9b1793dd579a6dd0d0c2e59daffec Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:37:41 +0200 Subject: [PATCH] Fix thread OOM: cap fetchThreadEvents at 300 events, cap profile cache at 500 entries --- src/hooks/useProfile.ts | 14 ++++++++++++++ src/lib/nostr/notes.ts | 21 +++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/hooks/useProfile.ts b/src/hooks/useProfile.ts index 3327ab9..e6b566f 100644 --- a/src/hooks/useProfile.ts +++ b/src/hooks/useProfile.ts @@ -2,9 +2,22 @@ import { useEffect, useState } from "react"; import { fetchProfile } from "../lib/nostr"; import { dbLoadProfile, dbSaveProfile } from "../lib/db"; +const PROFILE_CACHE_MAX = 500; const profileCache = new Map(); const pendingRequests = new Map>(); +function pruneProfileCache() { + if (profileCache.size > PROFILE_CACHE_MAX) { + // Drop oldest entries (Map preserves insertion order) + const toDelete = profileCache.size - PROFILE_CACHE_MAX; + let i = 0; + for (const key of profileCache.keys()) { + if (i++ >= toDelete) break; + profileCache.delete(key); + } + } +} + export function invalidateProfileCache(pubkey: string) { profileCache.delete(pubkey); pendingRequests.delete(pubkey); @@ -25,6 +38,7 @@ export function useProfile(pubkey: string) { .then((p) => { const result = p ?? null; profileCache.set(pubkey, result); + pruneProfileCache(); pendingRequests.delete(pubkey); if (result) dbSaveProfile(pubkey, JSON.stringify(result)); return result; diff --git a/src/lib/nostr/notes.ts b/src/lib/nostr/notes.ts index b162f8d..1f7016d 100644 --- a/src/lib/nostr/notes.ts +++ b/src/lib/nostr/notes.ts @@ -136,22 +136,27 @@ export async function publishQuote(content: string, quotedEvent: NDKEvent): Prom await note.publish(); } +const THREAD_EVENT_LIMIT = 300; // hard cap to prevent OOM on viral threads + export async function fetchThreadEvents(rootId: string): Promise { const instance = getNDK(); - // Round-trip 1: all events tagging the root - const directFilter: NDKFilter = { kinds: [NDKKind.Text], "#e": [rootId] }; + // Round-trip 1: all events tagging the root (capped) + const directFilter: NDKFilter = { kinds: [NDKKind.Text], "#e": [rootId], limit: THREAD_EVENT_LIMIT }; const directEvents = await fetchWithTimeout(instance, directFilter, THREAD_TIMEOUT); const allEvents = new Map(); for (const e of directEvents) allEvents.set(e.id, e); - // Round-trip 2: replies to any event in the thread - const knownIds = Array.from(allEvents.keys()); - if (knownIds.length > 0) { - const deepFilter: NDKFilter = { kinds: [NDKKind.Text], "#e": knownIds }; - const deepEvents = await fetchWithTimeout(instance, deepFilter, THREAD_TIMEOUT); - for (const e of deepEvents) allEvents.set(e.id, e); + // Round-trip 2: replies to events in the thread — only if round 1 returned < limit + // Skip deep fetch on large threads to avoid OOM + if (allEvents.size < THREAD_EVENT_LIMIT) { + const knownIds = Array.from(allEvents.keys()).slice(0, 50); // cap #e filter size + if (knownIds.length > 0) { + const deepFilter: NDKFilter = { kinds: [NDKKind.Text], "#e": knownIds, limit: THREAD_EVENT_LIMIT - allEvents.size }; + const deepEvents = await fetchWithTimeout(instance, deepFilter, THREAD_TIMEOUT); + for (const e of deepEvents) allEvents.set(e.id, e); + } } return Array.from(allEvents.values());