memo.gno

3.86 Kb ยท 134 lines
  1// Package memo provides a simple memoization utility to cache function results.
  2//
  3// The package offers a Memoizer type that can cache function results based on keys,
  4// with optional validation of cached values. This is useful for expensive computations
  5// that need to be cached and potentially invalidated based on custom conditions.
  6//
  7// /!\ Important Warning for Gno Usage:
  8// In Gno, storage updates only persist during transactions. This means:
  9//   - Cache entries created during queries will NOT persist
 10//   - Creating cache entries during queries will actually decrease performance
 11//     as it wastes resources trying to save data that won't be saved
 12//
 13// Best Practices:
 14// - Use this pattern in transaction-driven contexts rather than query/render scenarios
 15// - Consider controlled cache updates, e.g., by specific accounts (like oracles)
 16// - Ideal for cases where cache updates happen every N blocks or on specific events
 17// - Carefully evaluate if caching will actually improve performance in your use case
 18//
 19// Basic usage example:
 20//
 21//	m := memo.New()
 22//
 23//	// Cache expensive computation
 24//	result := m.Memoize("key", func() any {
 25//	    // expensive operation
 26//	    return "computed-value"
 27//	})
 28//
 29//	// Subsequent calls with same key return cached result
 30//	result = m.Memoize("key", func() any {
 31//	    // function won't be called, cached value is returned
 32//	    return "computed-value"
 33//	})
 34//
 35// Example with validation:
 36//
 37//	type TimestampedValue struct {
 38//	    Value     string
 39//	    Timestamp time.Time
 40//	}
 41//
 42//	m := memo.New()
 43//
 44//	// Cache value with timestamp
 45//	result := m.MemoizeWithValidator(
 46//	    "key",
 47//	    func() any {
 48//	        return TimestampedValue{
 49//	            Value:     "data",
 50//	            Timestamp: time.Now(),
 51//	        }
 52//	    },
 53//	    func(cached any) bool {
 54//	        // Validate that the cached value is not older than 1 hour
 55//	        if tv, ok := cached.(TimestampedValue); ok {
 56//	            return time.Since(tv.Timestamp) < time.Hour
 57//	        }
 58//	        return false
 59//	    },
 60//	)
 61package memo
 62
 63import (
 64	"gno.land/p/demo/btree"
 65	"gno.land/p/demo/ufmt"
 66)
 67
 68// Record implements the btree.Record interface for our cache entries
 69type cacheEntry struct {
 70	key   any
 71	value any
 72}
 73
 74// Less implements btree.Record interface
 75func (e cacheEntry) Less(than btree.Record) bool {
 76	// Convert the other record to cacheEntry
 77	other := than.(cacheEntry)
 78	// Compare string representations of keys for consistent ordering
 79	return ufmt.Sprintf("%v", e.key) < ufmt.Sprintf("%v", other.key)
 80}
 81
 82// Memoizer is a structure to handle memoization of function results.
 83type Memoizer struct {
 84	cache *btree.BTree
 85}
 86
 87// New creates a new Memoizer instance.
 88func New() *Memoizer {
 89	return &Memoizer{
 90		cache: btree.New(),
 91	}
 92}
 93
 94// Memoize ensures the result of the given function is cached for the specified key.
 95func (m *Memoizer) Memoize(key any, fn func() any) any {
 96	entry := cacheEntry{key: key}
 97	if found := m.cache.Get(entry); found != nil {
 98		return found.(cacheEntry).value
 99	}
100
101	value := fn()
102	m.cache.Insert(cacheEntry{key: key, value: value})
103	return value
104}
105
106// MemoizeWithValidator ensures the result is cached and valid according to the validator function.
107func (m *Memoizer) MemoizeWithValidator(key any, fn func() any, isValid func(any) bool) any {
108	entry := cacheEntry{key: key}
109	if found := m.cache.Get(entry); found != nil {
110		cachedEntry := found.(cacheEntry)
111		if isValid(cachedEntry.value) {
112			return cachedEntry.value
113		}
114	}
115
116	value := fn()
117	m.cache.Insert(cacheEntry{key: key, value: value})
118	return value
119}
120
121// Invalidate removes the cached value for the specified key.
122func (m *Memoizer) Invalidate(key any) {
123	m.cache.Delete(cacheEntry{key: key})
124}
125
126// Clear clears all cached values.
127func (m *Memoizer) Clear() {
128	m.cache.Clear(true)
129}
130
131// Size returns the number of items currently in the cache.
132func (m *Memoizer) Size() int {
133	return m.cache.Len()
134}