query_cache.go raw

   1  package archive
   2  
   3  import (
   4  	"container/list"
   5  	"crypto/sha256"
   6  	"encoding/binary"
   7  	"encoding/hex"
   8  	"sort"
   9  	"sync"
  10  	"time"
  11  
  12  	"next.orly.dev/pkg/nostr/encoders/filter"
  13  )
  14  
  15  // QueryCache tracks which filters have been queried recently to avoid
  16  // repeated requests to archive relays for the same filter.
  17  type QueryCache struct {
  18  	mu       sync.RWMutex
  19  	entries  map[string]*list.Element
  20  	order    *list.List
  21  	maxSize  int
  22  	ttl      time.Duration
  23  }
  24  
  25  // queryCacheEntry holds a cached query fingerprint and timestamp.
  26  type queryCacheEntry struct {
  27  	fingerprint string
  28  	queriedAt   time.Time
  29  }
  30  
  31  // NewQueryCache creates a new query cache.
  32  func NewQueryCache(ttl time.Duration, maxSize int) *QueryCache {
  33  	if maxSize <= 0 {
  34  		maxSize = 100000
  35  	}
  36  	if ttl <= 0 {
  37  		ttl = 24 * time.Hour
  38  	}
  39  
  40  	return &QueryCache{
  41  		entries: make(map[string]*list.Element),
  42  		order:   list.New(),
  43  		maxSize: maxSize,
  44  		ttl:     ttl,
  45  	}
  46  }
  47  
  48  // HasQueried returns true if the filter was queried within the TTL.
  49  func (qc *QueryCache) HasQueried(f *filter.F) bool {
  50  	fingerprint := qc.normalizeAndHash(f)
  51  
  52  	qc.mu.RLock()
  53  	elem, exists := qc.entries[fingerprint]
  54  	qc.mu.RUnlock()
  55  
  56  	if !exists {
  57  		return false
  58  	}
  59  
  60  	entry := elem.Value.(*queryCacheEntry)
  61  
  62  	// Check if still within TTL
  63  	if time.Since(entry.queriedAt) > qc.ttl {
  64  		// Expired - remove it
  65  		qc.mu.Lock()
  66  		if elem, exists := qc.entries[fingerprint]; exists {
  67  			delete(qc.entries, fingerprint)
  68  			qc.order.Remove(elem)
  69  		}
  70  		qc.mu.Unlock()
  71  		return false
  72  	}
  73  
  74  	return true
  75  }
  76  
  77  // MarkQueried marks a filter as having been queried.
  78  func (qc *QueryCache) MarkQueried(f *filter.F) {
  79  	fingerprint := qc.normalizeAndHash(f)
  80  
  81  	qc.mu.Lock()
  82  	defer qc.mu.Unlock()
  83  
  84  	// Update existing entry
  85  	if elem, exists := qc.entries[fingerprint]; exists {
  86  		qc.order.MoveToFront(elem)
  87  		elem.Value.(*queryCacheEntry).queriedAt = time.Now()
  88  		return
  89  	}
  90  
  91  	// Evict oldest if at capacity
  92  	if len(qc.entries) >= qc.maxSize {
  93  		oldest := qc.order.Back()
  94  		if oldest != nil {
  95  			entry := oldest.Value.(*queryCacheEntry)
  96  			delete(qc.entries, entry.fingerprint)
  97  			qc.order.Remove(oldest)
  98  		}
  99  	}
 100  
 101  	// Add new entry
 102  	entry := &queryCacheEntry{
 103  		fingerprint: fingerprint,
 104  		queriedAt:   time.Now(),
 105  	}
 106  	elem := qc.order.PushFront(entry)
 107  	qc.entries[fingerprint] = elem
 108  }
 109  
 110  // normalizeAndHash creates a canonical fingerprint for a filter.
 111  // This ensures that differently-ordered filters with the same content
 112  // produce identical fingerprints.
 113  func (qc *QueryCache) normalizeAndHash(f *filter.F) string {
 114  	h := sha256.New()
 115  
 116  	// Normalize and hash IDs (sorted)
 117  	if f.Ids != nil && f.Ids.Len() > 0 {
 118  		ids := make([]string, 0, f.Ids.Len())
 119  		for _, id := range f.Ids.T {
 120  			ids = append(ids, string(id))
 121  		}
 122  		sort.Strings(ids)
 123  		h.Write([]byte("ids:"))
 124  		for _, id := range ids {
 125  			h.Write([]byte(id))
 126  		}
 127  	}
 128  
 129  	// Normalize and hash Authors (sorted)
 130  	if f.Authors != nil && f.Authors.Len() > 0 {
 131  		authors := make([]string, 0, f.Authors.Len())
 132  		for _, author := range f.Authors.T {
 133  			authors = append(authors, string(author))
 134  		}
 135  		sort.Strings(authors)
 136  		h.Write([]byte("authors:"))
 137  		for _, a := range authors {
 138  			h.Write([]byte(a))
 139  		}
 140  	}
 141  
 142  	// Normalize and hash Kinds (sorted)
 143  	if f.Kinds != nil && f.Kinds.Len() > 0 {
 144  		kinds := f.Kinds.ToUint16()
 145  		sort.Slice(kinds, func(i, j int) bool { return kinds[i] < kinds[j] })
 146  		h.Write([]byte("kinds:"))
 147  		for _, k := range kinds {
 148  			var buf [2]byte
 149  			binary.BigEndian.PutUint16(buf[:], k)
 150  			h.Write(buf[:])
 151  		}
 152  	}
 153  
 154  	// Normalize and hash Tags (sorted by key, then values)
 155  	if f.Tags != nil && f.Tags.Len() > 0 {
 156  		// Collect all tag keys and sort them
 157  		tagMap := make(map[string][]string)
 158  		for _, t := range *f.Tags {
 159  			if t.Len() > 0 {
 160  				key := string(t.Key())
 161  				values := make([]string, 0, t.Len()-1)
 162  				for j := 1; j < t.Len(); j++ {
 163  					values = append(values, string(t.T[j]))
 164  				}
 165  				sort.Strings(values)
 166  				tagMap[key] = values
 167  			}
 168  		}
 169  
 170  		// Sort keys and hash
 171  		keys := make([]string, 0, len(tagMap))
 172  		for k := range tagMap {
 173  			keys = append(keys, k)
 174  		}
 175  		sort.Strings(keys)
 176  
 177  		h.Write([]byte("tags:"))
 178  		for _, k := range keys {
 179  			h.Write([]byte(k))
 180  			h.Write([]byte(":"))
 181  			for _, v := range tagMap[k] {
 182  				h.Write([]byte(v))
 183  			}
 184  		}
 185  	}
 186  
 187  	// Hash Since timestamp
 188  	if f.Since != nil {
 189  		h.Write([]byte("since:"))
 190  		var buf [8]byte
 191  		binary.BigEndian.PutUint64(buf[:], uint64(f.Since.V))
 192  		h.Write(buf[:])
 193  	}
 194  
 195  	// Hash Until timestamp
 196  	if f.Until != nil {
 197  		h.Write([]byte("until:"))
 198  		var buf [8]byte
 199  		binary.BigEndian.PutUint64(buf[:], uint64(f.Until.V))
 200  		h.Write(buf[:])
 201  	}
 202  
 203  	// Hash Limit
 204  	if f.Limit != nil && *f.Limit > 0 {
 205  		h.Write([]byte("limit:"))
 206  		var buf [4]byte
 207  		binary.BigEndian.PutUint32(buf[:], uint32(*f.Limit))
 208  		h.Write(buf[:])
 209  	}
 210  
 211  	// Hash Search (NIP-50)
 212  	if len(f.Search) > 0 {
 213  		h.Write([]byte("search:"))
 214  		h.Write(f.Search)
 215  	}
 216  
 217  	return hex.EncodeToString(h.Sum(nil))
 218  }
 219  
 220  // Len returns the number of cached queries.
 221  func (qc *QueryCache) Len() int {
 222  	qc.mu.RLock()
 223  	defer qc.mu.RUnlock()
 224  	return len(qc.entries)
 225  }
 226  
 227  // MaxSize returns the maximum cache size.
 228  func (qc *QueryCache) MaxSize() int {
 229  	return qc.maxSize
 230  }
 231  
 232  // Clear removes all entries from the cache.
 233  func (qc *QueryCache) Clear() {
 234  	qc.mu.Lock()
 235  	defer qc.mu.Unlock()
 236  	qc.entries = make(map[string]*list.Element)
 237  	qc.order.Init()
 238  }
 239