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.
191 lines
5.0 KiB
191 lines
5.0 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" |
|
) |
|
|
|
// 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 |
|
}
|
|
|