Browse Source

Fix Neo4j parameterized replaceable event handling (v0.52.5)

- Add post-query filtering to return only latest version per (pubkey, kind, d-tag)
- Delete older versions on save for kinds 30000-39999 in Neo4j backend
- QueryAllVersions bypasses filtering for recovery UI compatibility
- Badger continues to keep old versions (filtered at query time)

Files modified:
- pkg/neo4j/query-events.go: Add replaceable event filtering logic
- pkg/neo4j/save-event.go: Add deleteOlderParameterizedReplaceable helper
- pkg/version/version: Bump to v0.52.5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main v0.52.5
woikos 4 months ago
parent
commit
138d5cbff9
No known key found for this signature in database
  1. 75
      pkg/neo4j/query-events.go
  2. 43
      pkg/neo4j/save-event.go
  3. 2
      pkg/version/version

75
pkg/neo4j/query-events.go

@ -3,12 +3,15 @@ package neo4j
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"strconv"
"strings" "strings"
"time" "time"
"git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/hex" "git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/encoders/tag" "git.mleku.dev/mleku/nostr/encoders/tag"
"lol.mleku.dev/log" "lol.mleku.dev/log"
"next.orly.dev/pkg/database/indexes/types" "next.orly.dev/pkg/database/indexes/types"
@ -41,11 +44,81 @@ func (n *N) QueryEventsWithOptions(
} }
// Parse response // Parse response
evs, err = n.parseEventsFromResult(result) allEvents, err := n.parseEventsFromResult(result)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse events: %w", err) return nil, fmt.Errorf("failed to parse events: %w", err)
} }
// Filter replaceable events to only return the latest version
// unless showAllVersions is true
if showAllVersions {
return allEvents, nil
}
// Separate events by type and filter replaceables
replaceableEvents := make(map[string]*event.E) // key: pubkey:kind
paramReplaceableEvents := make(map[string]map[string]*event.E) // key: pubkey:kind -> d-tag -> event
var regularEvents event.S
for _, ev := range allEvents {
if kind.IsReplaceable(ev.Kind) {
// For replaceable events, keep only the latest per pubkey:kind
key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind))
existing, exists := replaceableEvents[key]
if !exists || ev.CreatedAt > existing.CreatedAt {
replaceableEvents[key] = ev
}
} else if kind.IsParameterizedReplaceable(ev.Kind) {
// For parameterized replaceable events, keep only the latest per pubkey:kind:d-tag
key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind))
// Get the 'd' tag value
dTag := ev.Tags.GetFirst([]byte("d"))
var dValue string
if dTag != nil && dTag.Len() > 1 {
dValue = string(dTag.Value())
}
// Initialize inner map if needed
if _, exists := paramReplaceableEvents[key]; !exists {
paramReplaceableEvents[key] = make(map[string]*event.E)
}
// Keep only the newest version
existing, exists := paramReplaceableEvents[key][dValue]
if !exists || ev.CreatedAt > existing.CreatedAt {
paramReplaceableEvents[key][dValue] = ev
}
} else {
regularEvents = append(regularEvents, ev)
}
}
// Combine results
evs = make(event.S, 0, len(replaceableEvents)+len(paramReplaceableEvents)+len(regularEvents))
for _, ev := range replaceableEvents {
evs = append(evs, ev)
}
for _, innerMap := range paramReplaceableEvents {
for _, ev := range innerMap {
evs = append(evs, ev)
}
}
evs = append(evs, regularEvents...)
// Re-sort by timestamp (newest first)
sort.Slice(evs, func(i, j int) bool {
return evs[i].CreatedAt > evs[j].CreatedAt
})
// Re-apply limit after filtering
if f.Limit != nil && len(evs) > int(*f.Limit) {
evs = evs[:*f.Limit]
}
return evs, nil return evs, nil
} }

43
pkg/neo4j/save-event.go

@ -56,6 +56,15 @@ func (n *N) SaveEvent(c context.Context, ev *event.E) (exists bool, err error) {
return true, nil // Event already exists return true, nil // Event already exists
} }
// For parameterized replaceable events (kinds 30000-39999), delete older versions
// before saving the new one. This ensures Neo4j only stores the latest version.
if ev.Kind >= 30000 && ev.Kind < 40000 {
if err := n.deleteOlderParameterizedReplaceable(c, ev); err != nil {
n.Logger.Warningf("failed to delete older replaceable events: %v", err)
// Continue with save - older events will be filtered at query time
}
}
// Get next serial number // Get next serial number
serial, err := n.getNextSerial() serial, err := n.getNextSerial()
if err != nil { if err != nil {
@ -444,3 +453,37 @@ ORDER BY e.created_at DESC`
return wouldReplace, serials, nil return wouldReplace, serials, nil
} }
// deleteOlderParameterizedReplaceable deletes older versions of parameterized replaceable events
// (kinds 30000-39999) that have the same pubkey, kind, and d-tag value.
// This is called before saving a new event to ensure only the latest version is stored.
func (n *N) deleteOlderParameterizedReplaceable(c context.Context, ev *event.E) error {
authorPubkey := hex.Enc(ev.Pubkey[:])
// Get the d-tag value
dTag := ev.Tags.GetFirst([]byte{'d'})
dValue := ""
if dTag != nil && len(dTag.T) >= 2 {
dValue = string(dTag.T[1])
}
// Delete older events with same pubkey, kind, and d-tag
// Only delete if the existing event is older than the new one
cypher := `
MATCH (e:Event {kind: $kind, pubkey: $pubkey})-[:TAGGED_WITH]->(t:Tag {type: 'd', value: $dValue})
WHERE e.created_at < $createdAt
DETACH DELETE e`
params := map[string]any{
"pubkey": authorPubkey,
"kind": int64(ev.Kind),
"dValue": dValue,
"createdAt": ev.CreatedAt,
}
if _, err := n.ExecuteWrite(c, cypher, params); err != nil {
return fmt.Errorf("failed to delete older replaceable events: %w", err)
}
return nil
}

2
pkg/version/version

@ -1 +1 @@
v0.52.4 v0.52.5

Loading…
Cancel
Save