From 4fdd596cbc4c22be3f7971cf398fbcdf8ea545ee Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 18 Feb 2026 00:11:28 -0600 Subject: [PATCH] Stop opening duplicate WebSocket connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real problem: nostr.relay() and nostr.group() each open brand new WebSocket connections outside the pool. Every hook that called these was creating duplicate connections to relays the pool already had open. Fixed by routing all queries and subscriptions through the pool (nostr) directly, which reuses its internal connections: - useStreamPosts: nostr.relay('wss://relay.ditto.pub') → nostr - useStreamKind: nostr.relay('wss://relay.ditto.pub') → nostr - useSearchProfiles: nostr.relay('wss://relay.ditto.pub') → nostr - useFollowActions: nostr.group([...]) → nostr - FollowPackDetailContent: nostr.group([...]) → nostr Also reverted the hacky relay cleanup code in NostrProvider since it was trying to solve the wrong problem. Co-authored-by: shakespeare.diy --- src/components/FollowPackDetailContent.tsx | 18 ++--------- src/components/NostrProvider.tsx | 27 ----------------- src/hooks/useFollowActions.ts | 35 ++++------------------ src/hooks/useSearchProfiles.ts | 6 ++-- src/hooks/useStreamKind.ts | 9 +++--- src/hooks/useStreamPosts.ts | 10 +++---- 6 files changed, 18 insertions(+), 87 deletions(-) diff --git a/src/components/FollowPackDetailContent.tsx b/src/components/FollowPackDetailContent.tsx index 092a7adb..15e8351f 100644 --- a/src/components/FollowPackDetailContent.tsx +++ b/src/components/FollowPackDetailContent.tsx @@ -15,7 +15,6 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFollowList, useFollowActions } from '@/hooks/useFollowActions'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useNostr } from '@nostrify/react'; -import { useAppContext } from '@/hooks/useAppContext'; import { genUserName } from '@/lib/genUserName'; /** Parse a follow pack / starter pack event into structured data. */ @@ -37,7 +36,6 @@ export function FollowPackDetailContent({ event }: { event: NostrEvent }) { const { toast } = useToast(); const { nostr } = useNostr(); const { user } = useCurrentUser(); - const { config } = useAppContext(); const { data: followList } = useFollowList(); const { mutateAsync: publishEvent } = useNostrPublish(); @@ -71,21 +69,9 @@ export function FollowPackDetailContent({ event }: { event: NostrEvent }) { setIsFollowingAll(true); try { - const discoveryRelays = [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.primal.net', - 'wss://purplepag.es', - ]; - const userRelays = config.relayMetadata.relays - .filter((r) => r.read) - .map((r) => r.url); - const allRelayUrls = [...new Set([...discoveryRelays, ...userRelays])]; - const signal = AbortSignal.timeout(10_000); - const relayGroup = nostr.group(allRelayUrls); - const followEvents = await relayGroup.query( + const followEvents = await nostr.query( [{ kinds: [3], authors: [user.pubkey], limit: 1 }], { signal }, ); @@ -123,7 +109,7 @@ export function FollowPackDetailContent({ event }: { event: NostrEvent }) { } finally { setIsFollowingAll(false); } - }, [user, pubkeys, nostr, config, publishEvent, toast]); + }, [user, pubkeys, nostr, publishEvent, toast]); const handleCopyLink = useCallback(() => { const dTag = event.tags.find(([n]) => n === 'd')?.[1] ?? ''; diff --git a/src/components/NostrProvider.tsx b/src/components/NostrProvider.tsx index 2a28f5b5..0a21d7d8 100644 --- a/src/components/NostrProvider.tsx +++ b/src/components/NostrProvider.tsx @@ -21,9 +21,6 @@ const NostrProvider: React.FC = (props) => { // Use refs so the pool always has the latest data const effectiveRelays = useRef(getEffectiveRelays(config.relayMetadata, config.useAppRelays)); - // Track previous relay URLs to detect changes - const prevRelayUrlsRef = useRef(''); - // Update effective relays ref and invalidate all queries when relays change, // since any cached query may have been fetched from a different set of relays. useEffect(() => { @@ -33,31 +30,7 @@ const NostrProvider: React.FC = (props) => { // Only invalidate if the relay URLs actually changed const prevUrls = prev.relays.map(r => r.url).sort().join(','); const nextUrls = effectiveRelays.current.relays.map(r => r.url).sort().join(','); - if (prevUrls !== nextUrls) { - // Close connections to relays that are no longer in the list - if (pool.current && prevRelayUrlsRef.current) { - const prevSet = new Set(prevUrls.split(',').filter(Boolean)); - const nextSet = new Set(nextUrls.split(',').filter(Boolean)); - - // Find relays that were removed - const removedRelays = [...prevSet].filter(url => !nextSet.has(url)); - - // Close removed relay connections - for (const url of removedRelays) { - try { - const relay = (pool.current as any).relays?.get?.(url); - if (relay) { - relay.close(); - (pool.current as any).relays?.delete?.(url); - } - } catch (e) { - // Ignore errors - } - } - } - - prevRelayUrlsRef.current = nextUrls; queryClient.invalidateQueries(); } }, [config.relayMetadata, config.useAppRelays, queryClient]); diff --git a/src/hooks/useFollowActions.ts b/src/hooks/useFollowActions.ts index e74e0e0a..ae18df11 100644 --- a/src/hooks/useFollowActions.ts +++ b/src/hooks/useFollowActions.ts @@ -2,37 +2,20 @@ import { useCallback, useState } from 'react'; import { useNostr } from '@nostrify/react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCurrentUser } from './useCurrentUser'; -import { useAppContext } from './useAppContext'; import { useNostrPublish } from './useNostrPublish'; import type { NostrEvent } from '@nostrify/nostrify'; /** - * Discovery relays used to find the user's latest kind 3 event. - * We cast a wide net to avoid acting on stale data. - */ -const DISCOVERY_RELAYS = [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.primal.net', - 'wss://purplepag.es', - 'wss://relay.snort.social', -]; - -/** - * Fetches the absolute freshest kind 3 follow list from multiple relays. - * This prevents stale-data mutations that could accidentally wipe follows. + * Fetches the absolute freshest kind 3 follow list via the pool. + * The pool already routes to all configured read relays. */ async function fetchFreshFollowEvent( nostr: ReturnType['nostr'], pubkey: string, - userRelayUrls: string[], ): Promise { - const allRelayUrls = [...new Set([...DISCOVERY_RELAYS, ...userRelayUrls])]; - const signal = AbortSignal.timeout(10_000); - const relayGroup = nostr.group(allRelayUrls); - const followEvents = await relayGroup.query( + const followEvents = await nostr.query( [{ kinds: [3], authors: [pubkey], limit: 1 }], { signal }, ); @@ -109,25 +92,19 @@ export interface UseFollowActionsReturn { export function useFollowActions(): UseFollowActionsReturn { const { nostr } = useNostr(); const { user } = useCurrentUser(); - const { config } = useAppContext(); const { mutateAsync: publishEvent } = useNostrPublish(); const queryClient = useQueryClient(); const [isPending, setIsPending] = useState(false); - /** User's read-enabled relay URLs (from NIP-65 config). */ - const userRelayUrls = config.relayMetadata.relays - .filter((r) => r.read) - .map((r) => r.url); - const mutateFollowList = useCallback( async (targetPubkey: string, action: 'follow' | 'unfollow') => { if (!user) throw new Error('Not logged in'); setIsPending(true); try { - // ① Fetch the freshest kind 3 event from a wide relay set - const latestEvent = await fetchFreshFollowEvent(nostr, user.pubkey, userRelayUrls); + // ① Fetch the freshest kind 3 event via pool + const latestEvent = await fetchFreshFollowEvent(nostr, user.pubkey); // ② Separate tags into `p` tags (follow entries) and everything else const existingTags = latestEvent?.tags ?? []; @@ -163,7 +140,7 @@ export function useFollowActions(): UseFollowActionsReturn { setIsPending(false); } }, - [nostr, user, userRelayUrls, publishEvent, queryClient], + [nostr, user, publishEvent, queryClient], ); const follow = useCallback( diff --git a/src/hooks/useSearchProfiles.ts b/src/hooks/useSearchProfiles.ts index 3c216467..47efe7d5 100644 --- a/src/hooks/useSearchProfiles.ts +++ b/src/hooks/useSearchProfiles.ts @@ -18,10 +18,8 @@ export function useSearchProfiles(query: string) { queryFn: async ({ signal }) => { if (!query.trim()) return []; - // Use relay.ditto.pub specifically for NIP-50 profile search - const relay = nostr.relay('wss://relay.ditto.pub'); - - const events = await relay.query( + // NIP-50 profile search (uses pool, reuses existing connections) + const events = await nostr.query( [{ kinds: [0], search: query.trim(), limit: 10 }], { signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) }, ); diff --git a/src/hooks/useStreamKind.ts b/src/hooks/useStreamKind.ts index 7d5a3267..3cbec9f3 100644 --- a/src/hooks/useStreamKind.ts +++ b/src/hooks/useStreamKind.ts @@ -67,13 +67,12 @@ export function useStreamKind(kind: number | number[]) { setEvents(Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at)); } - const relay = nostr.relay('wss://relay.ditto.pub'); const filter = { kinds }; - // 1. Fetch initial batch + // 1. Fetch initial batch (uses pool, reuses existing connections) (async () => { try { - const results = await relay.query( + const results = await nostr.query( [{ ...filter, limit: 40 }], { signal: ac.signal }, ); @@ -86,11 +85,11 @@ export function useStreamKind(kind: number | number[]) { if (alive) setIsLoading(false); })(); - // 2. Stream new events + // 2. Stream new events (uses pool, reuses existing connections) (async () => { try { const now = Math.floor(Date.now() / 1000); - for await (const msg of relay.req( + for await (const msg of nostr.req( [{ ...filter, since: now, limit: 100 }], { signal: ac.signal }, )) { diff --git a/src/hooks/useStreamPosts.ts b/src/hooks/useStreamPosts.ts index 8badbf2e..ef41b7d8 100644 --- a/src/hooks/useStreamPosts.ts +++ b/src/hooks/useStreamPosts.ts @@ -95,8 +95,6 @@ export function useStreamPosts(query: string, options: StreamPostsOptions) { setAllEvents(Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at)); } - const relay = nostr.relay('wss://relay.ditto.pub'); - // Build the kinds list: either vines-only or kind 1 + enabled extras const kinds: number[] = isVines ? [34236] @@ -108,10 +106,10 @@ export function useStreamPosts(query: string, options: StreamPostsOptions) { baseFilter.search = query.trim(); } - // 1. Fetch initial batch + // 1. Fetch initial batch (uses pool, reuses existing connections) (async () => { try { - const events = await relay.query( + const events = await nostr.query( [{ ...baseFilter, limit: 40 }], { signal: ac.signal }, ); @@ -124,11 +122,11 @@ export function useStreamPosts(query: string, options: StreamPostsOptions) { if (alive) setIsLoading(false); })(); - // 2. Stream new events + // 2. Stream new events (uses pool, reuses existing connections) (async () => { try { const now = Math.floor(Date.now() / 1000); - for await (const msg of relay.req( + for await (const msg of nostr.req( [{ ...baseFilter, since: now, limit: 100 }], { signal: ac.signal }, )) {