bring in khatru and eventstore.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
mergesortedslices "fiatjaf.com/lib/merge-sorted-slices"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func IsOlder(previous, next *nostr.Event) bool {
|
||||
return previous.CreatedAt < next.CreatedAt ||
|
||||
(previous.CreatedAt == next.CreatedAt && previous.ID > next.ID)
|
||||
}
|
||||
|
||||
func ChooseNarrowestTag(filter nostr.Filter) (key string, values []string, goodness int) {
|
||||
var tagKey string
|
||||
var tagValues []string
|
||||
for key, values := range filter.Tags {
|
||||
switch key {
|
||||
case "e", "E", "q":
|
||||
// 'e' and 'q' are the narrowest possible, so if we have that we will use it and that's it
|
||||
tagKey = key
|
||||
tagValues = values
|
||||
goodness = 9
|
||||
break
|
||||
case "a", "A", "i", "I", "g", "r":
|
||||
// these are second-best as they refer to relatively static things
|
||||
goodness = 8
|
||||
tagKey = key
|
||||
tagValues = values
|
||||
case "d":
|
||||
// this is as good as long as we have an "authors"
|
||||
if len(filter.Authors) != 0 && goodness < 7 {
|
||||
goodness = 7
|
||||
tagKey = key
|
||||
tagValues = values
|
||||
} else if goodness < 4 {
|
||||
goodness = 4
|
||||
tagKey = key
|
||||
tagValues = values
|
||||
}
|
||||
case "h", "t", "l", "k", "K":
|
||||
// these things denote "categories", so they are a little more broad
|
||||
if goodness < 6 {
|
||||
goodness = 6
|
||||
tagKey = key
|
||||
tagValues = values
|
||||
}
|
||||
case "p":
|
||||
// this is broad and useless for a pure tag search, but we will still prefer it over others
|
||||
// for secondary filtering
|
||||
if goodness < 2 {
|
||||
goodness = 2
|
||||
tagKey = key
|
||||
tagValues = values
|
||||
}
|
||||
default:
|
||||
// all the other tags are probably too broad and useless
|
||||
if goodness == 0 {
|
||||
tagKey = key
|
||||
tagValues = values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagKey, tagValues, goodness
|
||||
}
|
||||
|
||||
func CopyMapWithoutKey[K comparable, V any](originalMap map[K]V, key K) map[K]V {
|
||||
newMap := make(map[K]V, len(originalMap)-1)
|
||||
for k, v := range originalMap {
|
||||
if k != key {
|
||||
newMap[k] = v
|
||||
}
|
||||
}
|
||||
return newMap
|
||||
}
|
||||
|
||||
type IterEvent struct {
|
||||
*nostr.Event
|
||||
Q int
|
||||
}
|
||||
|
||||
// MergeSortMultipleBatches takes the results of multiple iterators, which are already sorted,
|
||||
// and merges them into a single big sorted slice
|
||||
func MergeSortMultiple(batches [][]IterEvent, limit int, dst []IterEvent) []IterEvent {
|
||||
// clear up empty lists here while simultaneously computing the total count.
|
||||
// this helps because if there are a bunch of empty lists then this pre-clean
|
||||
// step will get us in the faster 'merge' branch otherwise we would go to the other.
|
||||
// we would have to do the cleaning anyway inside it.
|
||||
// and even if we still go on the other we save one iteration by already computing the
|
||||
// total count.
|
||||
total := 0
|
||||
for i := len(batches) - 1; i >= 0; i-- {
|
||||
if len(batches[i]) == 0 {
|
||||
batches = SwapDelete(batches, i)
|
||||
} else {
|
||||
total += len(batches[i])
|
||||
}
|
||||
}
|
||||
|
||||
if limit == -1 {
|
||||
limit = total
|
||||
}
|
||||
|
||||
// this amazing equation will ensure that if one of the two sides goes very small (like 1 or 2)
|
||||
// the other can go very high (like 500) and we're still in the 'merge' branch.
|
||||
// if values go somewhere in the middle then they may match the 'merge' branch (batches=20,limit=70)
|
||||
// or not (batches=25, limit=60)
|
||||
if math.Log(float64(len(batches)*2))+math.Log(float64(limit)) < 8 {
|
||||
if dst == nil {
|
||||
dst = make([]IterEvent, limit)
|
||||
} else if cap(dst) < limit {
|
||||
dst = slices.Grow(dst, limit-len(dst))
|
||||
}
|
||||
dst = dst[0:limit]
|
||||
return mergesortedslices.MergeFuncNoEmptyListsIntoSlice(dst, batches, compareIterEvent)
|
||||
} else {
|
||||
if dst == nil {
|
||||
dst = make([]IterEvent, total)
|
||||
} else if cap(dst) < total {
|
||||
dst = slices.Grow(dst, total-len(dst))
|
||||
}
|
||||
dst = dst[0:total]
|
||||
|
||||
// use quicksort in a dumb way that will still be fast because it's cheated
|
||||
lastIndex := 0
|
||||
for _, batch := range batches {
|
||||
copy(dst[lastIndex:], batch)
|
||||
lastIndex += len(batch)
|
||||
}
|
||||
|
||||
slices.SortFunc(dst, compareIterEvent)
|
||||
|
||||
for i, j := 0, total-1; i < j; i, j = i+1, j-1 {
|
||||
dst[i], dst[j] = dst[j], dst[i]
|
||||
}
|
||||
|
||||
if limit < len(dst) {
|
||||
return dst[0:limit]
|
||||
}
|
||||
return dst
|
||||
}
|
||||
}
|
||||
|
||||
// BatchSizePerNumberOfQueries tries to make an educated guess for the batch size given the total filter limit and
|
||||
// the number of abstract queries we'll be conducting at the same time
|
||||
func BatchSizePerNumberOfQueries(totalFilterLimit int, numberOfQueries int) int {
|
||||
if numberOfQueries == 1 || totalFilterLimit*numberOfQueries < 50 {
|
||||
return totalFilterLimit
|
||||
}
|
||||
|
||||
return int(
|
||||
math.Ceil(
|
||||
math.Pow(float64(totalFilterLimit), 0.80) / math.Pow(float64(numberOfQueries), 0.71),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func SwapDelete[A any](arr []A, i int) []A {
|
||||
arr[i] = arr[len(arr)-1]
|
||||
return arr[:len(arr)-1]
|
||||
}
|
||||
|
||||
func compareIterEvent(a, b IterEvent) int {
|
||||
if a.Event == nil {
|
||||
if b.Event == nil {
|
||||
return 0
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
} else if b.Event == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
if a.CreatedAt == b.CreatedAt {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
}
|
||||
return cmp.Compare(a.CreatedAt, b.CreatedAt)
|
||||
}
|
||||
Reference in New Issue
Block a user