1package syncprotocol23import (4 "fmt"56 wire "github.com/FelineStateMachine/tinyrelay/protocol/nostr"78 "fiatjaf.com/nostr"9 "fiatjaf.com/nostr/nip77/negentropy"10 "fiatjaf.com/nostr/nip77/negentropy/storage/vector"11)1213// Item identifies one event in a reconciliation index.14type Item = wire.EventRef1516// Result contains one negentropy response and the IDs discovered by the17// initiator when reconciliation completes.18type Result struct {19 Response string20 Have []string21 Need []string22 Done bool23}2425// Session holds one stateful negentropy exchange. A session is used by one26// initiator or responder for the lifetime of that exchange.27type Session struct {28 neg *negentropy.Negentropy29 initiator bool30}3132// NewSession builds a sealed negentropy index from the relay's event IDs and33// timestamps. The initiator flag selects the side that starts the exchange34// and receives Have and Need IDs.35func NewSession(items []Item, initiator bool) (*Session, error) {36 vec := vector.New()37 for _, item := range items {38 id, err := nostr.IDFromHex(item.ID)39 if err != nil {40 return nil, fmt.Errorf("invalid sync item id: %w", err)41 }42 vec.Insert(nostr.Timestamp(item.Timestamp), id)43 }44 vec.Seal()45 return &Session{neg: negentropy.New(vec, 60_000, initiator, initiator), initiator: initiator}, nil46}4748// Start begins an initiator session and returns its protocol message.49func (s *Session) Start() (string, error) {50 if !s.initiator {51 return "", fmt.Errorf("only the initiator can start a negentropy session")52 }53 return s.neg.Start(), nil54}5556// Reconcile consumes one peer message and returns the next response. An57// initiator receives IDs present on each side once the exchange is complete.58func (s *Session) Reconcile(message string) (Result, error) {59 if message == "" {60 return Result{}, fmt.Errorf("empty negentropy message")61 }62 response, err := s.neg.Reconcile(message)63 if err != nil {64 return Result{}, fmt.Errorf("reconcile negentropy message: %w", err)65 }66 result := Result{Response: response, Done: s.initiator && response == ""}67 if s.initiator {68 result.Have = drainIDs(s.neg.Haves)69 result.Need = drainIDs(s.neg.HaveNots)70 }71 return result, nil72}7374func drainIDs(ids <-chan nostr.ID) []string {75 if ids == nil {76 return nil77 }78 values := make([]string, 0)79 for id := range ids {80 values = append(values, id.Hex())81 }82 return values83}