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