useFetchFollowGraph.tsx raw
1 import client from '@/services/client.service'
2 import graphQueryService from '@/services/graph-query.service'
3 import { useEffect, useState } from 'react'
4
5 interface FollowGraphResult {
6 /** Pubkeys by depth (index 0 = direct follows, index 1 = follows of follows, etc.) */
7 pubkeysByDepth: string[][]
8 /** Whether graph query was used (vs traditional method) */
9 usedGraphQuery: boolean
10 /** Loading state */
11 isLoading: boolean
12 /** Error if any */
13 error: Error | null
14 }
15
16 /**
17 * Hook for fetching follow graph with automatic graph query optimization.
18 * Falls back to traditional method when graph queries are not available.
19 *
20 * @param pubkey - The seed pubkey to fetch follows for
21 * @param depth - How many levels deep to fetch (1 = direct follows, 2 = + follows of follows)
22 * @param relayUrls - Optional relay URLs to try for graph queries
23 */
24 export function useFetchFollowGraph(
25 pubkey: string | null,
26 depth: number = 1,
27 relayUrls?: string[]
28 ): FollowGraphResult {
29 const [pubkeysByDepth, setPubkeysByDepth] = useState<string[][]>([])
30 const [usedGraphQuery, setUsedGraphQuery] = useState(false)
31 const [isLoading, setIsLoading] = useState(true)
32 const [error, setError] = useState<Error | null>(null)
33
34 useEffect(() => {
35 if (!pubkey) {
36 setPubkeysByDepth([])
37 setIsLoading(false)
38 return
39 }
40
41 const fetchFollowGraph = async () => {
42 setIsLoading(true)
43 setError(null)
44 setUsedGraphQuery(false)
45
46 const urls = relayUrls ?? client.currentRelays
47
48 try {
49 // Try graph query first
50 const graphResult = await graphQueryService.queryFollowGraph(urls, pubkey, depth)
51
52 if (graphResult?.pubkeys_by_depth?.length) {
53 setPubkeysByDepth(graphResult.pubkeys_by_depth)
54 setUsedGraphQuery(true)
55 setIsLoading(false)
56 return
57 }
58
59 // Fallback to traditional method
60 const result = await fetchTraditionalFollowGraph(pubkey, depth)
61 setPubkeysByDepth(result)
62 } catch (e) {
63 console.error('Failed to fetch follow graph:', e)
64 setError(e instanceof Error ? e : new Error('Unknown error'))
65 } finally {
66 setIsLoading(false)
67 }
68 }
69
70 fetchFollowGraph()
71 }, [pubkey, depth, relayUrls?.join(',')])
72
73 return { pubkeysByDepth, usedGraphQuery, isLoading, error }
74 }
75
76 /**
77 * Traditional method for fetching follow graph (used as fallback)
78 */
79 async function fetchTraditionalFollowGraph(pubkey: string, depth: number): Promise<string[][]> {
80 const result: string[][] = []
81
82 // Depth 1: Direct follows
83 const directFollows = await client.fetchFollowings(pubkey)
84 result.push(directFollows)
85
86 if (depth < 2 || directFollows.length === 0) {
87 return result
88 }
89
90 // Depth 2: Follows of follows
91 // Note: This is expensive - N queries for N follows
92 const followsOfFollows = new Set<string>()
93 const directFollowsSet = new Set(directFollows)
94 directFollowsSet.add(pubkey) // Exclude self
95
96 // Fetch in batches to avoid overwhelming relays
97 const batchSize = 10
98 for (let i = 0; i < directFollows.length; i += batchSize) {
99 const batch = directFollows.slice(i, i + batchSize)
100 const batchResults = await Promise.all(batch.map((pk) => client.fetchFollowings(pk)))
101
102 for (const follows of batchResults) {
103 for (const pk of follows) {
104 if (!directFollowsSet.has(pk)) {
105 followsOfFollows.add(pk)
106 }
107 }
108 }
109 }
110
111 result.push(Array.from(followsOfFollows))
112
113 return result
114 }
115
116 /**
117 * Get all pubkeys from a follow graph result as a flat array
118 */
119 export function flattenFollowGraph(pubkeysByDepth: string[][]): string[] {
120 return pubkeysByDepth.flat()
121 }
122
123 /**
124 * Check if a pubkey is in the follow graph at a specific depth
125 */
126 export function isPubkeyAtDepth(
127 pubkeysByDepth: string[][],
128 pubkey: string,
129 depth: number
130 ): boolean {
131 const depthIndex = depth - 1
132 if (depthIndex < 0 || depthIndex >= pubkeysByDepth.length) {
133 return false
134 }
135 return pubkeysByDepth[depthIndex].includes(pubkey)
136 }
137
138 /**
139 * Check if a pubkey is anywhere in the follow graph
140 */
141 export function isPubkeyInGraph(pubkeysByDepth: string[][], pubkey: string): boolean {
142 return pubkeysByDepth.some((depth) => depth.includes(pubkey))
143 }
144