Files
vega/src/lib/nostr/notes.ts
Jure 5fe3554579 Fix feed OOM: cap follow feed at 30, WebKit software rendering, dedup notification fetches
- followNotes capped at 30 (was 80) — following feed was rendering 2.7x more
  notes than global, causing 4GB+ spike on media-heavy follow content
- fetchFollowFeed limit 80→30 to match
- WEBKIT_FORCE_SOFTWARE_RENDERING=1 replaces WEBKIT_DISABLE_COMPOSITING_MODE=1
  (compositing mode killed Wayland path → blank window on Hyprland)
- HardwareAccelerationPolicy::Never → OnDemand (Never also caused blank screen)
- set_enable_page_cache(false) — SPA never navigates, bfcache is pure waste
- Removed duplicate fetchNotifications calls on login (was firing 3x in 8s)
- First notification poll delayed 8s→90s to avoid competing with feed load
- Result: login 3600MB→453MB, following feed crash→737MB, plateau at ~950MB
2026-04-16 11:43:48 +02:00

199 lines
7.8 KiB
TypeScript

import { NDKEvent, NDKFilter, NDKKind, NDKRelaySet, nip19 } from "@nostr-dev-kit/ndk";
import { getNDK, getStoredRelayUrls, fetchWithTimeout, withTimeout, FEED_TIMEOUT, THREAD_TIMEOUT, SINGLE_TIMEOUT } from "./core";
import { fetchUserRelayList } from "./relays";
export async function fetchGlobalFeed(limit: number = 50): Promise<NDKEvent[]> {
const instance = getNDK();
// Ask for notes from the last 2 hours to ensure freshness
const since = Math.floor(Date.now() / 1000) - 2 * 3600;
const filter: NDKFilter = { kinds: [NDKKind.Text, 1068 as NDKKind], limit, since };
const events = await fetchWithTimeout(instance, filter, FEED_TIMEOUT);
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
export async function fetchMediaFeed(limit: number = 500): Promise<NDKEvent[]> {
const instance = getNDK();
// Wider window (24h) since media notes are sparse among text notes
const since = Math.floor(Date.now() / 1000) - 24 * 3600;
const filter: NDKFilter = { kinds: [NDKKind.Text], limit, since };
const events = await fetchWithTimeout(instance, filter, FEED_TIMEOUT);
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
export async function fetchFollowFeed(pubkeys: string[], limit = 30): Promise<NDKEvent[]> {
if (pubkeys.length === 0) return [];
const instance = getNDK();
const since = Math.floor(Date.now() / 1000) - 24 * 3600; // last 24h for follows
const filter: NDKFilter = { kinds: [NDKKind.Text, 1068 as NDKKind], authors: pubkeys, limit, since };
const events = await fetchWithTimeout(instance, filter, FEED_TIMEOUT);
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
export async function fetchUserNotes(pubkey: string, limit = 30): Promise<NDKEvent[]> {
const instance = getNDK();
const filter: NDKFilter = { kinds: [NDKKind.Text, 1068 as NDKKind], authors: [pubkey], limit };
const events = await fetchWithTimeout(instance, filter, FEED_TIMEOUT);
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
export async function fetchUserNotesNIP65(pubkey: string, limit = 30): Promise<NDKEvent[]> {
const instance = getNDK();
const filter: NDKFilter = { kinds: [NDKKind.Text, 1068 as NDKKind], authors: [pubkey], limit };
try {
const relayList = await withTimeout(fetchUserRelayList(pubkey), SINGLE_TIMEOUT, { read: [], write: [] });
if (relayList.write.length > 0) {
const merged = Array.from(new Set([...relayList.write, ...getStoredRelayUrls()]));
const relaySet = NDKRelaySet.fromRelayUrls(merged, instance);
const events = await fetchWithTimeout(instance, filter, FEED_TIMEOUT, relaySet);
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
} catch { /* fallthrough */ }
return fetchUserNotes(pubkey, limit);
}
export async function fetchNoteById(eventId: string): Promise<NDKEvent | null> {
const instance = getNDK();
const filter: NDKFilter = { ids: [eventId], limit: 1 };
const events = await fetchWithTimeout(instance, filter, SINGLE_TIMEOUT);
return Array.from(events)[0] ?? null;
}
export async function fetchReplies(eventId: string): Promise<NDKEvent[]> {
const instance = getNDK();
const filter: NDKFilter = { kinds: [NDKKind.Text], "#e": [eventId] };
const events = await fetchWithTimeout(instance, filter, FEED_TIMEOUT);
return Array.from(events).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
}
export async function publishNote(content: string): Promise<NDKEvent> {
const instance = getNDK();
if (!instance.signer) throw new Error("Not logged in");
const event = new NDKEvent(instance);
event.kind = NDKKind.Text;
event.content = content;
await event.publish();
return event;
}
export async function publishReply(
content: string,
replyTo: { id: string; pubkey: string },
rootEvent?: { id: string; pubkey: string },
): Promise<NDKEvent> {
const instance = getNDK();
if (!instance.signer) throw new Error("Not logged in");
const event = new NDKEvent(instance);
event.kind = NDKKind.Text;
event.content = content;
if (rootEvent && rootEvent.id !== replyTo.id) {
const pTags = new Set([rootEvent.pubkey, replyTo.pubkey]);
event.tags = [
["e", rootEvent.id, "", "root"],
["e", replyTo.id, "", "reply"],
...Array.from(pTags).map((p) => ["p", p]),
];
} else {
event.tags = [
["e", replyTo.id, "", "root"],
["p", replyTo.pubkey],
];
}
await event.publish();
return event;
}
export async function publishRepost(event: NDKEvent): Promise<void> {
const instance = getNDK();
if (!instance.signer) throw new Error("Not logged in");
const repost = new NDKEvent(instance);
repost.kind = NDKKind.Repost;
repost.content = JSON.stringify(event.rawEvent());
repost.tags = [
["e", event.id!, "", "mention"],
["p", event.pubkey],
];
await repost.publish();
}
export async function publishQuote(content: string, quotedEvent: NDKEvent): Promise<void> {
const instance = getNDK();
if (!instance.signer) throw new Error("Not logged in");
const nevent = nip19.neventEncode({ id: quotedEvent.id!, author: quotedEvent.pubkey });
const fullContent = content.trim() + "\n\nnostr:" + nevent;
const note = new NDKEvent(instance);
note.kind = NDKKind.Text;
note.content = fullContent;
note.tags = [
["q", quotedEvent.id!, ""],
["p", quotedEvent.pubkey],
];
await note.publish();
}
const THREAD_EVENT_LIMIT = 300; // hard cap to prevent OOM on viral threads
export async function fetchThreadEvents(rootId: string): Promise<NDKEvent[]> {
const instance = getNDK();
// 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<string, NDKEvent>();
// Hard-truncate: relays often ignore `limit` on #e filters, so we enforce it client-side
for (const e of [...directEvents].slice(0, THREAD_EVENT_LIMIT)) allEvents.set(e.id, e);
// Round-trip 2: only attempt if round 1 was small (skip entirely on big threads)
if (allEvents.size < 50) {
const knownIds = Array.from(allEvents.keys());
if (knownIds.length > 0) {
const deepFilter: NDKFilter = { kinds: [NDKKind.Text], "#e": knownIds, limit: THREAD_EVENT_LIMIT };
const deepEvents = await fetchWithTimeout(instance, deepFilter, THREAD_TIMEOUT);
for (const e of [...deepEvents].slice(0, THREAD_EVENT_LIMIT - allEvents.size)) allEvents.set(e.id, e);
}
}
return Array.from(allEvents.values());
}
const ANCESTOR_TIMEOUT = 2000; // 2s per parent — fail fast
export async function fetchAncestors(event: NDKEvent, maxDepth = 5): Promise<NDKEvent[]> {
const ancestors: NDKEvent[] = [];
let current = event;
for (let i = 0; i < maxDepth; i++) {
const eTags = current.tags.filter((t) => t[0] === "e");
if (eTags.length === 0) break;
const parentId =
eTags.find((t) => t[3] === "reply")?.[1] ??
eTags.find((t) => t[3] === "root")?.[1] ??
eTags[eTags.length - 1][1];
if (!parentId) break;
const instance = getNDK();
const filter: NDKFilter = { ids: [parentId], limit: 1 };
const events = await fetchWithTimeout(instance, filter, ANCESTOR_TIMEOUT);
const parent = Array.from(events)[0] ?? null;
if (!parent) break;
ancestors.unshift(parent);
current = parent;
}
return ancestors;
}
export async function fetchHashtagFeed(tag: string, limit = 100): Promise<NDKEvent[]> {
const instance = getNDK();
const filter: NDKFilter = { kinds: [NDKKind.Text], "#t": [tag.toLowerCase()], limit };
const events = await fetchWithTimeout(instance, filter, FEED_TIMEOUT);
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}