//go:build !(js && wasm) package database import ( "lol.mleku.dev/log" "next.orly.dev/pkg/database/indexes/types" "git.mleku.dev/mleku/nostr/encoders/hex" ) // TraverseFollows performs BFS traversal of the follow graph starting from a seed pubkey. // Returns pubkeys grouped by first-discovered depth (no duplicates across depths). // // The traversal works by: // 1. Starting with the seed pubkey at depth 0 (not included in results) // 2. For each pubkey at the current depth, find their kind-3 contact list // 3. Extract p-tags from the contact list to get follows // 4. Add new (unseen) follows to the next depth // 5. Continue until maxDepth is reached or no new pubkeys are found // // Early termination occurs if two consecutive depths yield no new pubkeys. func (d *D) TraverseFollows(seedPubkey []byte, maxDepth int) (*GraphResult, error) { result := NewGraphResult() if len(seedPubkey) != 32 { return result, ErrPubkeyNotFound } // Get seed pubkey serial seedSerial, err := d.GetPubkeySerial(seedPubkey) if err != nil { log.D.F("TraverseFollows: seed pubkey not in database: %s", hex.Enc(seedPubkey)) return result, nil // Not an error - just no results } // Track visited pubkeys by serial to avoid cycles visited := make(map[uint64]bool) visited[seedSerial.Get()] = true // Mark seed as visited but don't add to results // Current frontier (pubkeys to process at this depth) currentFrontier := []*types.Uint40{seedSerial} // Track consecutive empty depths for early termination consecutiveEmptyDepths := 0 for currentDepth := 1; currentDepth <= maxDepth; currentDepth++ { var nextFrontier []*types.Uint40 newPubkeysAtDepth := 0 for _, pubkeySerial := range currentFrontier { // Get follows for this pubkey follows, err := d.GetFollowsFromPubkeySerial(pubkeySerial) if err != nil { log.D.F("TraverseFollows: error getting follows for serial %d: %v", pubkeySerial.Get(), err) continue } for _, followSerial := range follows { // Skip if already visited if visited[followSerial.Get()] { continue } visited[followSerial.Get()] = true // Get pubkey hex for result pubkeyHex, err := d.GetPubkeyHexFromSerial(followSerial) if err != nil { log.D.F("TraverseFollows: error getting pubkey hex for serial %d: %v", followSerial.Get(), err) continue } // Add to results at this depth result.AddPubkeyAtDepth(pubkeyHex, currentDepth) newPubkeysAtDepth++ // Add to next frontier for further traversal nextFrontier = append(nextFrontier, followSerial) } } log.T.F("TraverseFollows: depth %d found %d new pubkeys", currentDepth, newPubkeysAtDepth) // Check for early termination if newPubkeysAtDepth == 0 { consecutiveEmptyDepths++ if consecutiveEmptyDepths >= 2 { log.T.F("TraverseFollows: early termination at depth %d (2 consecutive empty depths)", currentDepth) break } } else { consecutiveEmptyDepths = 0 } // Move to next depth currentFrontier = nextFrontier } log.D.F("TraverseFollows: completed with %d total pubkeys across %d depths", result.TotalPubkeys, len(result.PubkeysByDepth)) return result, nil } // TraverseFollowers performs BFS traversal to find who follows the seed pubkey. // This is the reverse of TraverseFollows - it finds users whose kind-3 lists // contain the target pubkey(s). // // At each depth: // - Depth 1: Users who directly follow the seed // - Depth 2: Users who follow anyone at depth 1 (followers of followers) // - etc. func (d *D) TraverseFollowers(seedPubkey []byte, maxDepth int) (*GraphResult, error) { result := NewGraphResult() if len(seedPubkey) != 32 { return result, ErrPubkeyNotFound } // Get seed pubkey serial seedSerial, err := d.GetPubkeySerial(seedPubkey) if err != nil { log.D.F("TraverseFollowers: seed pubkey not in database: %s", hex.Enc(seedPubkey)) return result, nil } // Track visited pubkeys visited := make(map[uint64]bool) visited[seedSerial.Get()] = true // Current frontier currentFrontier := []*types.Uint40{seedSerial} consecutiveEmptyDepths := 0 for currentDepth := 1; currentDepth <= maxDepth; currentDepth++ { var nextFrontier []*types.Uint40 newPubkeysAtDepth := 0 for _, targetSerial := range currentFrontier { // Get followers of this pubkey followers, err := d.GetFollowersOfPubkeySerial(targetSerial) if err != nil { log.D.F("TraverseFollowers: error getting followers for serial %d: %v", targetSerial.Get(), err) continue } for _, followerSerial := range followers { if visited[followerSerial.Get()] { continue } visited[followerSerial.Get()] = true pubkeyHex, err := d.GetPubkeyHexFromSerial(followerSerial) if err != nil { continue } result.AddPubkeyAtDepth(pubkeyHex, currentDepth) newPubkeysAtDepth++ nextFrontier = append(nextFrontier, followerSerial) } } log.T.F("TraverseFollowers: depth %d found %d new pubkeys", currentDepth, newPubkeysAtDepth) if newPubkeysAtDepth == 0 { consecutiveEmptyDepths++ if consecutiveEmptyDepths >= 2 { break } } else { consecutiveEmptyDepths = 0 } currentFrontier = nextFrontier } log.D.F("TraverseFollowers: completed with %d total pubkeys", result.TotalPubkeys) return result, nil } // TraverseFollowsFromHex is a convenience wrapper that accepts hex-encoded pubkey. func (d *D) TraverseFollowsFromHex(seedPubkeyHex string, maxDepth int) (*GraphResult, error) { seedPubkey, err := hex.Dec(seedPubkeyHex) if err != nil { return nil, err } return d.TraverseFollows(seedPubkey, maxDepth) } // TraverseFollowersFromHex is a convenience wrapper that accepts hex-encoded pubkey. func (d *D) TraverseFollowersFromHex(seedPubkeyHex string, maxDepth int) (*GraphResult, error) { seedPubkey, err := hex.Dec(seedPubkeyHex) if err != nil { return nil, err } return d.TraverseFollowers(seedPubkey, maxDepth) }