//go:build !(js && wasm) package database import ( "errors" "github.com/dgraph-io/badger/v4" "lol.mleku.dev/chk" "next.orly.dev/pkg/database/bufpool" "next.orly.dev/pkg/database/indexes" "next.orly.dev/pkg/database/indexes/types" ) // SerialCache provides LRU caching for pubkey and event ID serial lookups. // This is critical for compact event decoding performance since every event // requires looking up the author pubkey and potentially multiple tag references. // // The cache uses LRU eviction and starts empty, growing on demand up to the // configured limits. This provides better memory efficiency than pre-allocation // and better hit rates than random eviction. type SerialCache struct { // Pubkey serial -> full pubkey (for decoding) pubkeyBySerial *LRUCache[uint64, []byte] // Pubkey bytes -> serial (for encoding) // Uses [32]byte as key since []byte isn't comparable serialByPubkey *LRUCache[[32]byte, uint64] // Event serial -> full event ID (for decoding) eventIdBySerial *LRUCache[uint64, []byte] // Event ID bytes -> serial (for encoding) serialByEventId *LRUCache[[32]byte, uint64] // Limits (for stats reporting) maxPubkeys int maxEventIds int } // NewSerialCache creates a new serial cache with the specified maximum sizes. // The cache starts empty and grows on demand up to these limits. func NewSerialCache(maxPubkeys, maxEventIds int) *SerialCache { if maxPubkeys <= 0 { maxPubkeys = 100000 // Default 100k pubkeys } if maxEventIds <= 0 { maxEventIds = 500000 // Default 500k event IDs } return &SerialCache{ pubkeyBySerial: NewLRUCache[uint64, []byte](maxPubkeys), serialByPubkey: NewLRUCache[[32]byte, uint64](maxPubkeys), eventIdBySerial: NewLRUCache[uint64, []byte](maxEventIds), serialByEventId: NewLRUCache[[32]byte, uint64](maxEventIds), maxPubkeys: maxPubkeys, maxEventIds: maxEventIds, } } // CachePubkey adds a pubkey to the cache in both directions. func (c *SerialCache) CachePubkey(serial uint64, pubkey []byte) { if len(pubkey) != 32 { return } // Copy pubkey to avoid referencing external slice pk := make([]byte, 32) copy(pk, pubkey) // Cache serial -> pubkey (for decoding) c.pubkeyBySerial.Put(serial, pk) // Cache pubkey -> serial (for encoding) var key [32]byte copy(key[:], pubkey) c.serialByPubkey.Put(key, serial) } // GetPubkeyBySerial returns the pubkey for a serial from cache. func (c *SerialCache) GetPubkeyBySerial(serial uint64) (pubkey []byte, found bool) { return c.pubkeyBySerial.Get(serial) } // GetSerialByPubkey returns the serial for a pubkey from cache. func (c *SerialCache) GetSerialByPubkey(pubkey []byte) (serial uint64, found bool) { if len(pubkey) != 32 { return 0, false } var key [32]byte copy(key[:], pubkey) return c.serialByPubkey.Get(key) } // CacheEventId adds an event ID to the cache in both directions. func (c *SerialCache) CacheEventId(serial uint64, eventId []byte) { if len(eventId) != 32 { return } // Copy event ID to avoid referencing external slice eid := make([]byte, 32) copy(eid, eventId) // Cache serial -> event ID (for decoding) c.eventIdBySerial.Put(serial, eid) // Cache event ID -> serial (for encoding) var key [32]byte copy(key[:], eventId) c.serialByEventId.Put(key, serial) } // GetEventIdBySerial returns the event ID for a serial from cache. func (c *SerialCache) GetEventIdBySerial(serial uint64) (eventId []byte, found bool) { return c.eventIdBySerial.Get(serial) } // GetSerialByEventId returns the serial for an event ID from cache. func (c *SerialCache) GetSerialByEventId(eventId []byte) (serial uint64, found bool) { if len(eventId) != 32 { return 0, false } var key [32]byte copy(key[:], eventId) return c.serialByEventId.Get(key) } // DatabaseSerialResolver implements SerialResolver using the database and cache. type DatabaseSerialResolver struct { db *D cache *SerialCache } // NewDatabaseSerialResolver creates a new resolver. func NewDatabaseSerialResolver(db *D, cache *SerialCache) *DatabaseSerialResolver { return &DatabaseSerialResolver{db: db, cache: cache} } // GetOrCreatePubkeySerial implements SerialResolver. func (r *DatabaseSerialResolver) GetOrCreatePubkeySerial(pubkey []byte) (serial uint64, err error) { if len(pubkey) != 32 { return 0, errors.New("pubkey must be 32 bytes") } // Check cache first if s, found := r.cache.GetSerialByPubkey(pubkey); found { return s, nil } // Use existing function which handles creation ser, err := r.db.GetOrCreatePubkeySerial(pubkey) if err != nil { return 0, err } serial = ser.Get() // Cache it r.cache.CachePubkey(serial, pubkey) return serial, nil } // GetPubkeyBySerial implements SerialResolver. func (r *DatabaseSerialResolver) GetPubkeyBySerial(serial uint64) (pubkey []byte, err error) { // Check cache first if pk, found := r.cache.GetPubkeyBySerial(serial); found { return pk, nil } // Look up in database ser := new(types.Uint40) if err = ser.Set(serial); err != nil { return nil, err } pubkey, err = r.db.GetPubkeyBySerial(ser) if err != nil { return nil, err } // Cache it r.cache.CachePubkey(serial, pubkey) return pubkey, nil } // GetEventSerialById implements SerialResolver. func (r *DatabaseSerialResolver) GetEventSerialById(eventId []byte) (serial uint64, found bool, err error) { if len(eventId) != 32 { return 0, false, errors.New("event ID must be 32 bytes") } // Check cache first if s, ok := r.cache.GetSerialByEventId(eventId); ok { return s, true, nil } // Look up in database using existing GetSerialById ser, err := r.db.GetSerialById(eventId) if err != nil { // Not found is not an error - just return found=false return 0, false, nil } serial = ser.Get() // Cache it r.cache.CacheEventId(serial, eventId) return serial, true, nil } // GetEventIdBySerial implements SerialResolver. func (r *DatabaseSerialResolver) GetEventIdBySerial(serial uint64) (eventId []byte, err error) { // Check cache first if eid, found := r.cache.GetEventIdBySerial(serial); found { return eid, nil } // Look up in database - use SerialEventId index ser := new(types.Uint40) if err = ser.Set(serial); err != nil { return nil, err } eventId, err = r.db.GetEventIdBySerial(ser) if err != nil { return nil, err } // Cache it r.cache.CacheEventId(serial, eventId) return eventId, nil } // GetEventIdBySerial looks up an event ID by its serial number. // Uses the SerialEventId index (sei prefix). func (d *D) GetEventIdBySerial(ser *types.Uint40) (eventId []byte, err error) { keyBuf := bufpool.GetSmall() defer bufpool.PutSmall(keyBuf) if err = indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) { return nil, err } err = d.View(func(txn *badger.Txn) error { item, gerr := txn.Get(keyBuf.Bytes()) if chk.E(gerr) { return gerr } return item.Value(func(val []byte) error { eventId = make([]byte, len(val)) copy(eventId, val) return nil }) }) if err != nil { return nil, errors.New("event ID not found for serial") } return eventId, nil } // StoreEventIdSerial stores the mapping from event serial to full event ID. // This is called during event save to enable later reconstruction. func (d *D) StoreEventIdSerial(txn *badger.Txn, serial uint64, eventId []byte) error { if len(eventId) != 32 { return errors.New("event ID must be 32 bytes") } ser := new(types.Uint40) if err := ser.Set(serial); err != nil { return err } keyBuf := bufpool.GetSmall() defer bufpool.PutSmall(keyBuf) if err := indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) { return err } return txn.Set(bufpool.CopyBytes(keyBuf), eventId) } // SerialCacheStats holds statistics about the serial cache. type SerialCacheStats struct { PubkeysCached int // Number of pubkeys currently cached PubkeysMaxSize int // Maximum pubkey cache size EventIdsCached int // Number of event IDs currently cached EventIdsMaxSize int // Maximum event ID cache size PubkeyMemoryBytes int // Estimated memory usage for pubkey cache EventIdMemoryBytes int // Estimated memory usage for event ID cache TotalMemoryBytes int // Total estimated memory usage } // Stats returns statistics about the serial cache. func (c *SerialCache) Stats() SerialCacheStats { pubkeysCached := c.pubkeyBySerial.Len() eventIdsCached := c.eventIdBySerial.Len() // Memory estimation: // Each entry has: key + value + list.Element overhead + map entry overhead // - Pubkey by serial: 8 (key) + 32 (value) + ~80 (list) + ~16 (map) ≈ 136 bytes // - Serial by pubkey: 32 (key) + 8 (value) + ~80 (list) + ~16 (map) ≈ 136 bytes // Total per pubkey (both directions): ~272 bytes // Similarly for event IDs: ~272 bytes per entry (both directions) pubkeyMemory := pubkeysCached * 272 eventIdMemory := eventIdsCached * 272 return SerialCacheStats{ PubkeysCached: pubkeysCached, PubkeysMaxSize: c.maxPubkeys, EventIdsCached: eventIdsCached, EventIdsMaxSize: c.maxEventIds, PubkeyMemoryBytes: pubkeyMemory, EventIdMemoryBytes: eventIdMemory, TotalMemoryBytes: pubkeyMemory + eventIdMemory, } } // SerialCacheStats returns statistics about the serial cache. func (d *D) SerialCacheStats() SerialCacheStats { if d.serialCache == nil { return SerialCacheStats{} } return d.serialCache.Stats() }