~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
321 lines · 6.6 KB Raw History
package cache

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"maps"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/atproto"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/models"
)

const (
	RecordValueFile  = "value.json"
	PolicyResultFile = "result.json"
	PolicyMetaFile   = "meta.json"
)

type Store struct {
	mu         sync.Mutex
	entries    map[string]models.CacheEntry
	order      []string
	maxEntries int
	maxBytes   int64
	bytes      int64
}

func NewStore(maxEntries int, maxBytes int64) *Store {
	return &Store{
		entries:    make(map[string]models.CacheEntry),
		maxEntries: maxEntries,
		maxBytes:   maxBytes,
	}
}

func (s *Store) Seed(value models.Cache) {
	if s == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	for key, entry := range value {
		s.setLocked(key, entry)
	}
}

func (s *Store) Snapshot() models.Cache {
	if s == nil {
		return nil
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	out := make(models.Cache, len(s.entries))
	for key, entry := range s.entries {
		copied := make(models.CacheEntry, len(entry))
		maps.Copy(copied, entry)
		out[key] = copied
	}
	return out
}

func (s *Store) Merge(value models.Cache) {
	if s == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	for key, entry := range value {
		s.setLocked(key, entry)
	}
}

func (s *Store) Get(key string) (models.CacheEntry, bool) {
	if s == nil {
		return nil, false
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	entry, ok := s.entries[key]
	if !ok {
		return nil, false
	}
	s.touchLocked(key)
	copied := make(models.CacheEntry, len(entry))
	maps.Copy(copied, entry)
	return copied, true
}

func (s *Store) Set(key string, entry models.CacheEntry) {
	if s == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.setLocked(key, entry)
}

func (s *Store) Delete(key string) {
	if s == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.deleteLocked(key)
}

func (s *Store) Len() int {
	if s == nil {
		return 0
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	return len(s.entries)
}

func (s *Store) setLocked(key string, entry models.CacheEntry) {
	if _, exists := s.entries[key]; exists {
		s.deleteLocked(key)
	}
	s.entries[key] = entry
	s.order = append(s.order, key)
	s.bytes += entryBytes(key, entry)
	s.evictLocked()
}

func (s *Store) deleteLocked(key string) {
	entry, ok := s.entries[key]
	if !ok {
		return
	}
	s.bytes -= entryBytes(key, entry)
	delete(s.entries, key)
	for i, existing := range s.order {
		if existing == key {
			s.order = append(s.order[:i], s.order[i+1:]...)
			break
		}
	}
}

func (s *Store) touchLocked(key string) {
	for i, existing := range s.order {
		if existing == key {
			s.order = append(s.order[:i], s.order[i+1:]...)
			s.order = append(s.order, key)
			return
		}
	}
}

func (s *Store) evictLocked() {
	for {
		if s.maxEntries > 0 && len(s.entries) > s.maxEntries {
			s.deleteLocked(s.order[0])
			continue
		}
		if s.maxBytes > 0 && s.bytes > s.maxBytes && len(s.order) > 0 {
			s.deleteLocked(s.order[0])
			continue
		}
		break
	}
}

func entryBytes(key string, entry models.CacheEntry) int64 {
	total := int64(len(key))
	for path, file := range entry {
		total += int64(len(path) + len(file.Data) + len(file.Encoding))
	}
	return total
}

type RecordSet struct {
	RFP    *atproto.StrongRef
	Bid    *atproto.StrongRef
	Accept *atproto.StrongRef
}

func RecordKey(ref atproto.StrongRef) string {
	return fmt.Sprintf("rec/%s/%s", ref.URI, ref.CID)
}

func ReadRecord(store *Store, ref atproto.StrongRef) (map[string]any, bool) {
	entry, ok := store.Get(RecordKey(ref))
	if !ok {
		return nil, false
	}
	file, ok := entry[RecordValueFile]
	if !ok {
		return nil, false
	}
	var value map[string]any
	if err := json.Unmarshal([]byte(file.Data), &value); err != nil {
		return nil, false
	}
	return value, true
}

func ReadRecordCache(seed models.Cache, ref atproto.StrongRef) (map[string]any, bool) {
	if len(seed) == 0 || ref.URI == "" {
		return nil, false
	}
	entry, ok := seed[RecordKey(ref)]
	if !ok {
		return nil, false
	}
	file, ok := entry[RecordValueFile]
	if !ok {
		return nil, false
	}
	data := []byte(file.Data)
	if file.Encoding == models.CacheEncodingBase64 {
		decoded, err := base64.StdEncoding.DecodeString(file.Data)
		if err != nil {
			return nil, false
		}
		data = decoded
	}
	var value map[string]any
	if err := json.Unmarshal(data, &value); err != nil {
		return nil, false
	}
	return value, true
}

func WriteRecord(store *Store, ref atproto.StrongRef, value map[string]any) {
	data, err := json.Marshal(value)
	if err != nil {
		return
	}
	store.Set(RecordKey(ref), models.CacheEntry{
		RecordValueFile: {Data: string(data), Encoding: models.CacheEncodingText},
	})
}

func policyRecordPart(ref *atproto.StrongRef) string {
	if ref == nil || ref.URI == "" {
		return ""
	}
	return ref.URI + "/" + ref.CID
}

func PolicyKey(name string, records RecordSet, args map[string]any) string {
	suffix := ""
	if len(args) > 0 {
		if data, err := json.Marshal(args); err == nil {
			suffix = ":" + HashString(string(data))
		}
	}
	return fmt.Sprintf("policy/%s/%s|%s|%s%s",
		name,
		policyRecordPart(records.RFP),
		policyRecordPart(records.Bid),
		policyRecordPart(records.Accept),
		suffix,
	)
}

type PolicyMeta struct {
	CachedAt int64 `json:"cachedAt"`
	TTLMs    int64 `json:"ttlMs"`
}

func ReadVerdict(store *Store, key string) ([]byte, bool) {
	entry, ok := store.Get(key)
	if !ok {
		return nil, false
	}
	if meta, ok := entry[PolicyMetaFile]; ok {
		var value PolicyMeta
		if err := json.Unmarshal([]byte(meta.Data), &value); err == nil {
			if time.Now().UnixMilli()-value.CachedAt > value.TTLMs {
				store.Delete(key)
				return nil, false
			}
		}
	}
	file, ok := entry[PolicyResultFile]
	if !ok {
		return nil, false
	}
	return []byte(file.Data), true
}

func WriteVerdict(store *Store, key string, data []byte, ttl time.Duration) {
	ttl = opt.Positive(ttl, 30*time.Second)
	meta, err := json.Marshal(PolicyMeta{CachedAt: time.Now().UnixMilli(), TTLMs: ttl.Milliseconds()})
	if err != nil {
		return
	}
	store.Set(key, models.CacheEntry{
		PolicyResultFile: {Data: string(data), Encoding: models.CacheEncodingText},
		PolicyMetaFile:   {Data: string(meta), Encoding: models.CacheEncodingText},
	})
}

func HashString(value string) string {
	var hash uint32 = 5381
	for i := 0; i < len(value); i++ {
		hash = (hash << 5) + hash + uint32(value[i])
	}
	return encodeBase36(uint64(hash))
}

func encodeBase36(value uint64) string {
	if value == 0 {
		return "0"
	}
	const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
	var buffer [16]byte
	index := len(buffer)
	for value > 0 {
		index--
		buffer[index] = digits[value%36]
		value /= 36
	}
	return string(buffer[index:])
}