Compare commits

..

4 Commits

Author SHA1 Message Date
3314a2a892 Refactor event replacement logic into WouldReplaceEvent and bump version to v0.10.5
Some checks failed
Go / build (push) Has been cancelled
2025-10-07 18:18:36 +01:00
7c14c72e9d Remove error return from ToSliceOfSliceOfStrings method in tag encoder and bump version to v0.10.4
Some checks failed
Go / build (push) Has been cancelled
2025-10-07 15:38:46 +01:00
dbdc5d703e Add ToSliceOfSliceOfStrings method to tag encoder and bump version to v0.10.3
Some checks failed
Go / build (push) Has been cancelled
2025-10-07 15:36:26 +01:00
c1acf0deaa Add ToSliceOfStrings method to tag encoder and bump version to v0.10.2
Some checks failed
Go / build (push) Has been cancelled
2025-10-07 15:32:07 +01:00
4 changed files with 127 additions and 102 deletions

View File

@@ -19,6 +19,13 @@ import (
"next.orly.dev/pkg/encoders/tag" "next.orly.dev/pkg/encoders/tag"
) )
var (
// ErrOlderThanExisting is returned when a candidate event is older than an existing replaceable/addressable event.
ErrOlderThanExisting = errors.New("older than existing event")
// ErrMissingDTag is returned when a parameterized replaceable event lacks the required 'd' tag.
ErrMissingDTag = errors.New("event is missing a d tag identifier")
)
func (d *D) GetSerialsFromFilter(f *filter.F) ( func (d *D) GetSerialsFromFilter(f *filter.F) (
sers types.Uint40s, err error, sers types.Uint40s, err error,
) { ) {
@@ -36,6 +43,65 @@ func (d *D) GetSerialsFromFilter(f *filter.F) (
return return
} }
// WouldReplaceEvent checks if the provided event would replace existing events
// based on Nostr's replaceable or parameterized replaceable semantics. It
// returns true along with the serials of events that should be replaced if the
// candidate is newer-or-equal. If an existing event is newer, it returns
// (false, serials, ErrOlderThanExisting). If no conflicts exist, it returns
// (false, nil, nil).
func (d *D) WouldReplaceEvent(ev *event.E) (bool, types.Uint40s, error) {
// Only relevant for replaceable or parameterized replaceable kinds
if !(kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind)) {
return false, nil, nil
}
var f *filter.F
if kind.IsReplaceable(ev.Kind) {
f = &filter.F{
Authors: tag.NewFromBytesSlice(ev.Pubkey),
Kinds: kind.NewS(kind.New(ev.Kind)),
}
} else {
// parameterized replaceable requires 'd' tag
dTag := ev.Tags.GetFirst([]byte("d"))
if dTag == nil {
return false, nil, ErrMissingDTag
}
f = &filter.F{
Authors: tag.NewFromBytesSlice(ev.Pubkey),
Kinds: kind.NewS(kind.New(ev.Kind)),
Tags: tag.NewS(
tag.NewFromAny("d", dTag.Value()),
),
}
}
sers, err := d.GetSerialsFromFilter(f)
if chk.E(err) {
return false, nil, err
}
if len(sers) == 0 {
return false, nil, nil
}
// Determine if any existing event is newer than the candidate
shouldReplace := true
for _, s := range sers {
oldEv, ferr := d.FetchEventBySerial(s)
if chk.E(ferr) {
continue
}
if ev.CreatedAt < oldEv.CreatedAt {
shouldReplace = false
break
}
}
if shouldReplace {
return true, sers, nil
}
return false, sers, ErrOlderThanExisting
}
// SaveEvent saves an event to the database, generating all the necessary indexes. // SaveEvent saves an event to the database, generating all the necessary indexes.
func (d *D) SaveEvent(c context.Context, ev *event.E) (kc, vc int, err error) { func (d *D) SaveEvent(c context.Context, ev *event.E) (kc, vc int, err error) {
if ev == nil { if ev == nil {
@@ -68,117 +134,37 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (kc, vc int, err error) {
err = fmt.Errorf("blocked: %s", err.Error()) err = fmt.Errorf("blocked: %s", err.Error())
return return
} }
// check for replacement // check for replacement (separated check vs deletion)
if kind.IsReplaceable(ev.Kind) { if kind.IsReplaceable(ev.Kind) || kind.IsParameterizedReplaceable(ev.Kind) {
// find the events and check timestamps before deleting var wouldReplace bool
f := &filter.F{
Authors: tag.NewFromBytesSlice(ev.Pubkey),
Kinds: kind.NewS(kind.New(ev.Kind)),
}
var sers types.Uint40s var sers types.Uint40s
if sers, err = d.GetSerialsFromFilter(f); chk.E(err) { var werr error
if wouldReplace, sers, werr = d.WouldReplaceEvent(ev); werr != nil {
if errors.Is(werr, ErrOlderThanExisting) {
if kind.IsReplaceable(ev.Kind) {
err = errors.New("blocked: event is older than existing replaceable event")
} else {
err = errors.New("blocked: event is older than existing addressable event")
}
return
}
if errors.Is(werr, ErrMissingDTag) {
// keep behavior consistent with previous implementation
err = ErrMissingDTag
return
}
// any other error
return return
} }
// if found, check timestamps before deleting if wouldReplace {
if len(sers) > 0 {
var shouldReplace bool = true
for _, s := range sers { for _, s := range sers {
var oldEv *event.E var oldEv *event.E
if oldEv, err = d.FetchEventBySerial(s); chk.E(err) { if oldEv, err = d.FetchEventBySerial(s); chk.E(err) {
continue continue
} }
// Only replace if the new event is newer or same timestamp if err = d.DeleteEventBySerial(c, s, oldEv); chk.E(err) {
if ev.CreatedAt < oldEv.CreatedAt {
// log.I.F(
// "SaveEvent: rejecting older replaceable event ID=%s (created_at=%d) - existing event ID=%s (created_at=%d)",
// hex.Enc(ev.ID), ev.CreatedAt, hex.Enc(oldEv.ID),
// oldEv.CreatedAt,
// )
shouldReplace = false
break
}
}
if shouldReplace {
for _, s := range sers {
var oldEv *event.E
if oldEv, err = d.FetchEventBySerial(s); chk.E(err) {
continue
}
// log.I.F(
// "SaveEvent: replacing older replaceable event ID=%s (created_at=%d) with newer event ID=%s (created_at=%d)",
// hex.Enc(oldEv.ID), oldEv.CreatedAt, hex.Enc(ev.ID),
// ev.CreatedAt,
// )
if err = d.DeleteEventBySerial(
c, s, oldEv,
); chk.E(err) {
continue
}
}
} else {
// Don't save the older event - return an error
err = errors.New("blocked: event is older than existing replaceable event")
return
}
}
} else if kind.IsParameterizedReplaceable(ev.Kind) {
// find the events and check timestamps before deleting
dTag := ev.Tags.GetFirst([]byte("d"))
if dTag == nil {
err = errors.New("event is missing a d tag identifier")
return
}
f := &filter.F{
Authors: tag.NewFromBytesSlice(ev.Pubkey),
Kinds: kind.NewS(kind.New(ev.Kind)),
Tags: tag.NewS(
tag.NewFromAny("d", dTag.Value()),
),
}
var sers types.Uint40s
if sers, err = d.GetSerialsFromFilter(f); chk.E(err) {
return
}
// if found, check timestamps before deleting
if len(sers) > 0 {
var shouldReplace bool = true
for _, s := range sers {
var oldEv *event.E
if oldEv, err = d.FetchEventBySerial(s); chk.E(err) {
continue continue
} }
// Only replace if the new event is newer or same timestamp
if ev.CreatedAt < oldEv.CreatedAt {
// log.I.F(
// "SaveEvent: rejecting older addressable event ID=%s (created_at=%d) - existing event ID=%s (created_at=%d)",
// hex.Enc(ev.ID), ev.CreatedAt, hex.Enc(oldEv.ID),
// oldEv.CreatedAt,
// )
shouldReplace = false
break
}
}
if shouldReplace {
for _, s := range sers {
var oldEv *event.E
if oldEv, err = d.FetchEventBySerial(s); chk.E(err) {
continue
}
// log.I.F(
// "SaveEvent: replacing older addressable event ID=%s (created_at=%d) with newer event ID=%s (created_at=%d)",
// hex.Enc(oldEv.ID), oldEv.CreatedAt, hex.Enc(ev.ID),
// ev.CreatedAt,
// )
if err = d.DeleteEventBySerial(
c, s, oldEv,
); chk.E(err) {
continue
}
}
} else {
// Don't save the older event - return an error
err = errors.New("blocked: event is older than existing addressable event")
return
} }
} }
} }

View File

@@ -156,3 +156,21 @@ func (t *T) Relay() (key []byte) {
} }
return return
} }
// ToSliceOfStrings returns the tag's bytes slices as a slice of strings. This
// method provides a convenient way to access the tag's contents in string format.
//
// # Return Values
//
// - s ([]string): A slice containing all tag elements converted to strings.
//
// # Expected Behaviour
//
// Returns an empty slice if the tag is empty, otherwise returns a new slice with
// each byte slice element converted to a string.
func (t *T) ToSliceOfStrings() (s []string) {
for _, v := range t.T {
s = append(s, string(v))
}
return
}

View File

@@ -188,3 +188,24 @@ func (s *S) GetTagElement(i int) (t *T) {
t = (*s)[i] t = (*s)[i]
return return
} }
// ToSliceOfSliceOfStrings converts the tag collection into a two-dimensional
// slice of strings, maintaining the structure of tags and their elements.
//
// # Return Values
//
// - ss ([][]string): A slice of string slices where each inner slice represents
// a tag's elements converted from bytes to strings.
//
// - err (error): Currently unused but maintained for interface consistency.
//
// # Expected Behaviour
//
// Iterates through each tag in the collection and converts its byte elements
// to strings, preserving the tag structure in the resulting nested slice.
func (s *S) ToSliceOfSliceOfStrings() (ss [][]string) {
for _, v := range *s {
ss = append(ss, v.ToSliceOfStrings())
}
return
}

View File

@@ -1 +1 @@
v0.10.1 v0.10.5