You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
5.2 KiB
238 lines
5.2 KiB
package archive |
|
|
|
import ( |
|
"container/list" |
|
"crypto/sha256" |
|
"encoding/binary" |
|
"encoding/hex" |
|
"sort" |
|
"sync" |
|
"time" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/filter" |
|
) |
|
|
|
// QueryCache tracks which filters have been queried recently to avoid |
|
// repeated requests to archive relays for the same filter. |
|
type QueryCache struct { |
|
mu sync.RWMutex |
|
entries map[string]*list.Element |
|
order *list.List |
|
maxSize int |
|
ttl time.Duration |
|
} |
|
|
|
// queryCacheEntry holds a cached query fingerprint and timestamp. |
|
type queryCacheEntry struct { |
|
fingerprint string |
|
queriedAt time.Time |
|
} |
|
|
|
// NewQueryCache creates a new query cache. |
|
func NewQueryCache(ttl time.Duration, maxSize int) *QueryCache { |
|
if maxSize <= 0 { |
|
maxSize = 100000 |
|
} |
|
if ttl <= 0 { |
|
ttl = 24 * time.Hour |
|
} |
|
|
|
return &QueryCache{ |
|
entries: make(map[string]*list.Element), |
|
order: list.New(), |
|
maxSize: maxSize, |
|
ttl: ttl, |
|
} |
|
} |
|
|
|
// HasQueried returns true if the filter was queried within the TTL. |
|
func (qc *QueryCache) HasQueried(f *filter.F) bool { |
|
fingerprint := qc.normalizeAndHash(f) |
|
|
|
qc.mu.RLock() |
|
elem, exists := qc.entries[fingerprint] |
|
qc.mu.RUnlock() |
|
|
|
if !exists { |
|
return false |
|
} |
|
|
|
entry := elem.Value.(*queryCacheEntry) |
|
|
|
// Check if still within TTL |
|
if time.Since(entry.queriedAt) > qc.ttl { |
|
// Expired - remove it |
|
qc.mu.Lock() |
|
if elem, exists := qc.entries[fingerprint]; exists { |
|
delete(qc.entries, fingerprint) |
|
qc.order.Remove(elem) |
|
} |
|
qc.mu.Unlock() |
|
return false |
|
} |
|
|
|
return true |
|
} |
|
|
|
// MarkQueried marks a filter as having been queried. |
|
func (qc *QueryCache) MarkQueried(f *filter.F) { |
|
fingerprint := qc.normalizeAndHash(f) |
|
|
|
qc.mu.Lock() |
|
defer qc.mu.Unlock() |
|
|
|
// Update existing entry |
|
if elem, exists := qc.entries[fingerprint]; exists { |
|
qc.order.MoveToFront(elem) |
|
elem.Value.(*queryCacheEntry).queriedAt = time.Now() |
|
return |
|
} |
|
|
|
// Evict oldest if at capacity |
|
if len(qc.entries) >= qc.maxSize { |
|
oldest := qc.order.Back() |
|
if oldest != nil { |
|
entry := oldest.Value.(*queryCacheEntry) |
|
delete(qc.entries, entry.fingerprint) |
|
qc.order.Remove(oldest) |
|
} |
|
} |
|
|
|
// Add new entry |
|
entry := &queryCacheEntry{ |
|
fingerprint: fingerprint, |
|
queriedAt: time.Now(), |
|
} |
|
elem := qc.order.PushFront(entry) |
|
qc.entries[fingerprint] = elem |
|
} |
|
|
|
// normalizeAndHash creates a canonical fingerprint for a filter. |
|
// This ensures that differently-ordered filters with the same content |
|
// produce identical fingerprints. |
|
func (qc *QueryCache) normalizeAndHash(f *filter.F) string { |
|
h := sha256.New() |
|
|
|
// Normalize and hash IDs (sorted) |
|
if f.Ids != nil && f.Ids.Len() > 0 { |
|
ids := make([]string, 0, f.Ids.Len()) |
|
for _, id := range f.Ids.T { |
|
ids = append(ids, string(id)) |
|
} |
|
sort.Strings(ids) |
|
h.Write([]byte("ids:")) |
|
for _, id := range ids { |
|
h.Write([]byte(id)) |
|
} |
|
} |
|
|
|
// Normalize and hash Authors (sorted) |
|
if f.Authors != nil && f.Authors.Len() > 0 { |
|
authors := make([]string, 0, f.Authors.Len()) |
|
for _, author := range f.Authors.T { |
|
authors = append(authors, string(author)) |
|
} |
|
sort.Strings(authors) |
|
h.Write([]byte("authors:")) |
|
for _, a := range authors { |
|
h.Write([]byte(a)) |
|
} |
|
} |
|
|
|
// Normalize and hash Kinds (sorted) |
|
if f.Kinds != nil && f.Kinds.Len() > 0 { |
|
kinds := f.Kinds.ToUint16() |
|
sort.Slice(kinds, func(i, j int) bool { return kinds[i] < kinds[j] }) |
|
h.Write([]byte("kinds:")) |
|
for _, k := range kinds { |
|
var buf [2]byte |
|
binary.BigEndian.PutUint16(buf[:], k) |
|
h.Write(buf[:]) |
|
} |
|
} |
|
|
|
// Normalize and hash Tags (sorted by key, then values) |
|
if f.Tags != nil && f.Tags.Len() > 0 { |
|
// Collect all tag keys and sort them |
|
tagMap := make(map[string][]string) |
|
for _, t := range *f.Tags { |
|
if t.Len() > 0 { |
|
key := string(t.Key()) |
|
values := make([]string, 0, t.Len()-1) |
|
for j := 1; j < t.Len(); j++ { |
|
values = append(values, string(t.T[j])) |
|
} |
|
sort.Strings(values) |
|
tagMap[key] = values |
|
} |
|
} |
|
|
|
// Sort keys and hash |
|
keys := make([]string, 0, len(tagMap)) |
|
for k := range tagMap { |
|
keys = append(keys, k) |
|
} |
|
sort.Strings(keys) |
|
|
|
h.Write([]byte("tags:")) |
|
for _, k := range keys { |
|
h.Write([]byte(k)) |
|
h.Write([]byte(":")) |
|
for _, v := range tagMap[k] { |
|
h.Write([]byte(v)) |
|
} |
|
} |
|
} |
|
|
|
// Hash Since timestamp |
|
if f.Since != nil { |
|
h.Write([]byte("since:")) |
|
var buf [8]byte |
|
binary.BigEndian.PutUint64(buf[:], uint64(f.Since.V)) |
|
h.Write(buf[:]) |
|
} |
|
|
|
// Hash Until timestamp |
|
if f.Until != nil { |
|
h.Write([]byte("until:")) |
|
var buf [8]byte |
|
binary.BigEndian.PutUint64(buf[:], uint64(f.Until.V)) |
|
h.Write(buf[:]) |
|
} |
|
|
|
// Hash Limit |
|
if f.Limit != nil && *f.Limit > 0 { |
|
h.Write([]byte("limit:")) |
|
var buf [4]byte |
|
binary.BigEndian.PutUint32(buf[:], uint32(*f.Limit)) |
|
h.Write(buf[:]) |
|
} |
|
|
|
// Hash Search (NIP-50) |
|
if len(f.Search) > 0 { |
|
h.Write([]byte("search:")) |
|
h.Write(f.Search) |
|
} |
|
|
|
return hex.EncodeToString(h.Sum(nil)) |
|
} |
|
|
|
// Len returns the number of cached queries. |
|
func (qc *QueryCache) Len() int { |
|
qc.mu.RLock() |
|
defer qc.mu.RUnlock() |
|
return len(qc.entries) |
|
} |
|
|
|
// MaxSize returns the maximum cache size. |
|
func (qc *QueryCache) MaxSize() int { |
|
return qc.maxSize |
|
} |
|
|
|
// Clear removes all entries from the cache. |
|
func (qc *QueryCache) Clear() { |
|
qc.mu.Lock() |
|
defer qc.mu.Unlock() |
|
qc.entries = make(map[string]*list.Element) |
|
qc.order.Init() |
|
}
|
|
|