0fa3e58a83
The app was firing an excessive number of concurrent REQs, causing relays to return "too many concurrent REQs" errors. Root causes and fixes: 1. **Batch event stats** (biggest win): Created `useBatchEventStats` hook that fetches interaction stats for ALL visible feed items in a single relay query instead of 2 queries per NoteCard. For a 15-item feed this reduces ~30 concurrent REQs to just 1. Results are seeded into the individual `['event-stats', id]` cache for instant resolution. 2. **Merged dual queries**: Both `useEventStats` and `useEventInteractions` were firing 2 parallel queries (e-tag + q-tag). Merged each into a single query using multiple filter objects (relay handles as OR). 3. **Reduced limits across the board**: - useEventStats: limit 200+50 → 50+20 (single query) - useEventInteractions: limit 500+100 → 50+20 (single query) - useTrendingTags: limit 200 → 50 - useReplies: limit 100 → 50 - useProfileFeed PAGE_SIZE: 30 → 20 - useStreamPosts/useStreamKind subscription limit: 100 → 0 Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
import { useNostr } from '@nostrify/react';
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
|
|
/**
|
|
* Generic streaming hook that fetches an initial batch of events for the given
|
|
* kind(s) and then streams new ones in real-time.
|
|
*
|
|
* Handles deduplication for both regular events (by id) and addressable
|
|
* events (by pubkey+kind+d).
|
|
*
|
|
* Accepts a single kind number or an array of kinds.
|
|
*/
|
|
export function useStreamKind(kind: number | number[]) {
|
|
const { nostr } = useNostr();
|
|
const [events, setEvents] = useState<NostrEvent[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// Normalise to a stable array
|
|
const kinds = useMemo(
|
|
() => (Array.isArray(kind) ? kind : [kind]),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[JSON.stringify(Array.isArray(kind) ? kind.slice().sort() : [kind])],
|
|
);
|
|
|
|
const kindsSet = useMemo(() => new Set(kinds), [kinds]);
|
|
|
|
useEffect(() => {
|
|
if (kinds.length === 0) {
|
|
setEvents([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const ac = new AbortController();
|
|
let alive = true;
|
|
|
|
setEvents([]);
|
|
setIsLoading(true);
|
|
|
|
const eventMap = new Map<string, NostrEvent>();
|
|
|
|
function isAddressable(k: number): boolean {
|
|
return k >= 30000 && k < 40000;
|
|
}
|
|
|
|
function dedupeKey(event: NostrEvent): string {
|
|
if (isAddressable(event.kind)) {
|
|
const dTag = event.tags.find(([name]) => name === 'd')?.[1] ?? '';
|
|
return `${event.pubkey}:${event.kind}:${dTag}`;
|
|
}
|
|
return event.id;
|
|
}
|
|
|
|
function addEvent(event: NostrEvent) {
|
|
if (!alive) return;
|
|
if (!kindsSet.has(event.kind)) return;
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
if (event.created_at > now) return;
|
|
|
|
const key = dedupeKey(event);
|
|
const existing = eventMap.get(key);
|
|
if (existing && existing.created_at >= event.created_at) return;
|
|
|
|
eventMap.set(key, event);
|
|
setEvents(Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at));
|
|
}
|
|
|
|
const filter = { kinds };
|
|
|
|
// 1. Fetch initial batch (uses pool, reuses existing connections)
|
|
(async () => {
|
|
try {
|
|
const results = await nostr.query(
|
|
[{ ...filter, limit: 40 }],
|
|
{ signal: ac.signal },
|
|
);
|
|
for (const event of results) {
|
|
addEvent(event);
|
|
}
|
|
} catch {
|
|
// abort expected
|
|
}
|
|
if (alive) setIsLoading(false);
|
|
})();
|
|
|
|
// 2. Stream new events (uses pool, reuses existing connections)
|
|
(async () => {
|
|
try {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
for await (const msg of nostr.req(
|
|
[{ ...filter, since: now, limit: 0 }],
|
|
{ signal: ac.signal },
|
|
)) {
|
|
if (!alive) break;
|
|
if (msg[0] === 'EVENT') {
|
|
addEvent(msg[2]);
|
|
} else if (msg[0] === 'CLOSED') {
|
|
break;
|
|
}
|
|
}
|
|
} catch {
|
|
// abort expected
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
alive = false;
|
|
ac.abort();
|
|
};
|
|
}, [nostr, kinds, kindsSet]);
|
|
|
|
return { events, isLoading };
|
|
}
|