From 20f06d960840739f8054048f31dfee3e3e1a1338 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 30 Oct 2025 11:42:04 -0300 Subject: [PATCH] eventstore: SortedMerge() helper for combining results from different eventstores. --- eventstore/combine.go | 61 ++++++++++++++++++++++++++++++++++++++ eventstore/combine_test.go | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 eventstore/combine.go create mode 100644 eventstore/combine_test.go diff --git a/eventstore/combine.go b/eventstore/combine.go new file mode 100644 index 0000000..f10dec9 --- /dev/null +++ b/eventstore/combine.go @@ -0,0 +1,61 @@ +package eventstore + +import ( + "iter" + + "fiatjaf.com/nostr" +) + +func SortedMerge(it1, it2 iter.Seq[nostr.Event]) iter.Seq[nostr.Event] { + next1, done1 := iter.Pull(it1) + next2, done2 := iter.Pull(it2) + + return func(yield func(nostr.Event) bool) { + defer done1() + defer done2() + + evt1, ok1 := next1() + evt2, ok2 := next2() + + both: + if ok1 && ok2 { + if evt2.CreatedAt > evt1.CreatedAt { + if !yield(evt2) { + return + } + evt2, ok2 = next2() + goto both + } else { + if !yield(evt1) { + return + } + evt1, ok1 = next1() + goto both + } + } + + if !ok2 { + only1: + if ok1 { + if !yield(evt1) { + return + } + evt1, ok1 = next1() + goto only1 + } + } + + if !ok1 { + only2: + if ok2 { + if !yield(evt2) { + return + } + evt2, ok2 = next2() + goto only2 + } + } + + return + } +} diff --git a/eventstore/combine_test.go b/eventstore/combine_test.go new file mode 100644 index 0000000..78f7584 --- /dev/null +++ b/eventstore/combine_test.go @@ -0,0 +1,52 @@ +package eventstore + +import ( + "cmp" + "slices" + "testing" + + "fiatjaf.com/nostr" +) + +func FuzzSortedMerge(f *testing.F) { + f.Add(uint(4), uint(4), uint(3), uint(7), uint8(2), uint8(1)) + f.Add(uint(0), uint(4), uint(3), uint(7), uint8(2), uint8(1)) + f.Fuzz(func(t *testing.T, len1, len2 uint, start1, start2 uint, diff1, diff2 uint8) { + maxxx := max(len1*uint(diff1), len2*uint(diff2)) + start1 += maxxx + start2 += maxxx + + merged := SortedMerge( + func(yield func(nostr.Event) bool) { + for range len1 { + if !yield(nostr.Event{CreatedAt: nostr.Timestamp(start1)}) { + return + } + start1 -= uint(diff1) + } + }, + func(yield func(nostr.Event) bool) { + for range len2 { + if !yield(nostr.Event{CreatedAt: nostr.Timestamp(start2)}) { + return + } + start2 -= uint(diff2) + } + }, + ) + result := slices.Collect(merged) + + // assert length + if len(result) != int(len1+len2) { + t.Fatalf("expected %d events, got %d", len1+len2, len(result)) + } + + // assert sorted descending + slices.IsSortedFunc(result, func(a, b nostr.Event) int { return -1 * cmp.Compare(a.CreatedAt, b.CreatedAt) }) + for i := 1; i < len(result); i++ { + if result[i].CreatedAt > result[i-1].CreatedAt { + t.Fatalf("events not sorted descending at index %d: %d > %d", i, result[i].CreatedAt, result[i-1].CreatedAt) + } + } + }) +}