You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
5.8 KiB
199 lines
5.8 KiB
//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) |
|
}
|
|
|