192 lines
5.0 KiB
Go
192 lines
5.0 KiB
Go
//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"
|
|
)
|
|
|
|
// TraverseThread performs BFS traversal of thread structure via e-tags.
|
|
// Starting from a seed event, it finds all replies/references at each depth.
|
|
//
|
|
// The traversal works bidirectionally:
|
|
// - Forward: Events that the seed references (parents, quoted posts)
|
|
// - Backward: Events that reference the seed (replies, reactions, reposts)
|
|
//
|
|
// Parameters:
|
|
// - seedEventID: The event ID to start traversal from
|
|
// - maxDepth: Maximum depth to traverse
|
|
// - direction: "both" (default), "inbound" (replies to seed), "outbound" (seed's references)
|
|
func (d *D) TraverseThread(seedEventID []byte, maxDepth int, direction string) (*GraphResult, error) {
|
|
result := NewGraphResult()
|
|
|
|
if len(seedEventID) != 32 {
|
|
return result, ErrEventNotFound
|
|
}
|
|
|
|
// Get seed event serial
|
|
seedSerial, err := d.GetSerialById(seedEventID)
|
|
if err != nil {
|
|
log.D.F("TraverseThread: seed event not in database: %s", hex.Enc(seedEventID))
|
|
return result, nil
|
|
}
|
|
|
|
// Normalize direction
|
|
if direction == "" {
|
|
direction = "both"
|
|
}
|
|
|
|
// Track visited events
|
|
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
|
|
newEventsAtDepth := 0
|
|
|
|
for _, eventSerial := range currentFrontier {
|
|
// Get inbound references (events that reference this event)
|
|
if direction == "both" || direction == "inbound" {
|
|
inboundSerials, err := d.GetReferencingEvents(eventSerial, nil)
|
|
if err != nil {
|
|
log.D.F("TraverseThread: error getting inbound refs for serial %d: %v", eventSerial.Get(), err)
|
|
} else {
|
|
for _, refSerial := range inboundSerials {
|
|
if visited[refSerial.Get()] {
|
|
continue
|
|
}
|
|
visited[refSerial.Get()] = true
|
|
|
|
eventIDHex, err := d.GetEventIDFromSerial(refSerial)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
result.AddEventAtDepth(eventIDHex, currentDepth)
|
|
newEventsAtDepth++
|
|
nextFrontier = append(nextFrontier, refSerial)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get outbound references (events this event references)
|
|
if direction == "both" || direction == "outbound" {
|
|
outboundSerials, err := d.GetETagsFromEventSerial(eventSerial)
|
|
if err != nil {
|
|
log.D.F("TraverseThread: error getting outbound refs for serial %d: %v", eventSerial.Get(), err)
|
|
} else {
|
|
for _, refSerial := range outboundSerials {
|
|
if visited[refSerial.Get()] {
|
|
continue
|
|
}
|
|
visited[refSerial.Get()] = true
|
|
|
|
eventIDHex, err := d.GetEventIDFromSerial(refSerial)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
result.AddEventAtDepth(eventIDHex, currentDepth)
|
|
newEventsAtDepth++
|
|
nextFrontier = append(nextFrontier, refSerial)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log.T.F("TraverseThread: depth %d found %d new events", currentDepth, newEventsAtDepth)
|
|
|
|
if newEventsAtDepth == 0 {
|
|
consecutiveEmptyDepths++
|
|
if consecutiveEmptyDepths >= 2 {
|
|
break
|
|
}
|
|
} else {
|
|
consecutiveEmptyDepths = 0
|
|
}
|
|
|
|
currentFrontier = nextFrontier
|
|
}
|
|
|
|
log.D.F("TraverseThread: completed with %d total events", result.TotalEvents)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// TraverseThreadFromHex is a convenience wrapper that accepts hex-encoded event ID.
|
|
func (d *D) TraverseThreadFromHex(seedEventIDHex string, maxDepth int, direction string) (*GraphResult, error) {
|
|
seedEventID, err := hex.Dec(seedEventIDHex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d.TraverseThread(seedEventID, maxDepth, direction)
|
|
}
|
|
|
|
// GetThreadReplies finds all direct replies to an event.
|
|
// This is a convenience method that returns events at depth 1 with inbound direction.
|
|
func (d *D) GetThreadReplies(eventID []byte, kinds []uint16) (*GraphResult, error) {
|
|
result := NewGraphResult()
|
|
|
|
if len(eventID) != 32 {
|
|
return result, ErrEventNotFound
|
|
}
|
|
|
|
eventSerial, err := d.GetSerialById(eventID)
|
|
if err != nil {
|
|
return result, nil
|
|
}
|
|
|
|
// Get events that reference this event
|
|
replySerials, err := d.GetReferencingEvents(eventSerial, kinds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, replySerial := range replySerials {
|
|
eventIDHex, err := d.GetEventIDFromSerial(replySerial)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
result.AddEventAtDepth(eventIDHex, 1)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetThreadParents finds events that a given event references (its parents/quotes).
|
|
func (d *D) GetThreadParents(eventID []byte) (*GraphResult, error) {
|
|
result := NewGraphResult()
|
|
|
|
if len(eventID) != 32 {
|
|
return result, ErrEventNotFound
|
|
}
|
|
|
|
eventSerial, err := d.GetSerialById(eventID)
|
|
if err != nil {
|
|
return result, nil
|
|
}
|
|
|
|
// Get events that this event references
|
|
parentSerials, err := d.GetETagsFromEventSerial(eventSerial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, parentSerial := range parentSerials {
|
|
eventIDHex, err := d.GetEventIDFromSerial(parentSerial)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
result.AddEventAtDepth(eventIDHex, 1)
|
|
}
|
|
|
|
return result, nil
|
|
}
|