Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

memo.gno

4.23 Kb · 139 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// It is the B+ tree successor to [gno.land/p/moul/memo/v0] (which is backed by
  8// an AVL tree): a bump to v3 because the backing data structure — and thus the
  9// on-chain storage layout — changed. The exported API is identical to v2
 10// (New/Memoize/MemoizeWithValidator/Invalidate/Clear/Size). A B+ tree packs
 11// many entries per persisted node, so it costs materially less storage and gas
 12// per cached entry — prefer v3 when the cache is part of persisted realm state.
 13//
 14// Because the B+ tree backing mutates in place (the AVL backing was
 15// copy-on-write), do NOT invalidate or add entries to the same Memoizer from
 16// inside a callback that is iterating it, and do NOT copy a non-zero Memoizer
 17// by value.
 18//
 19// /!\ Important Warning for Gno Usage:
 20// In Gno, storage updates only persist during transactions. This means:
 21//   - Cache entries created during queries will NOT persist
 22//   - Creating cache entries during queries will actually decrease performance
 23//     as it wastes resources trying to save data that won't be saved
 24//
 25// Best Practices:
 26// - Use this pattern in transaction-driven contexts rather than query/render scenarios
 27// - Consider controlled cache updates, e.g., by specific accounts (like oracles)
 28// - Ideal for cases where cache updates happen every N blocks or on specific events
 29// - Carefully evaluate if caching will actually improve performance in your use case
 30//
 31// Basic usage example:
 32//
 33//	m := memo.New()
 34//
 35//	// Cache expensive computation
 36//	result := m.Memoize("key", func() any {
 37//	    // expensive operation
 38//	    return "computed-value"
 39//	})
 40//
 41//	// Subsequent calls with same key return cached result
 42//	result = m.Memoize("key", func() any {
 43//	    // function won't be called, cached value is returned
 44//	    return "computed-value"
 45//	})
 46//
 47// Example with validation:
 48//
 49//	type TimestampedValue struct {
 50//	    Value     string
 51//	    Timestamp time.Time
 52//	}
 53//
 54//	m := memo.New()
 55//
 56//	// Cache value with timestamp
 57//	result := m.MemoizeWithValidator(
 58//	    "key",
 59//	    func() any {
 60//	        return TimestampedValue{
 61//	            Value:     "data",
 62//	            Timestamp: time.Now(),
 63//	        }
 64//	    },
 65//	    func(cached any) bool {
 66//	        // Validate that the cached value is not older than 1 hour
 67//	        if tv, ok := cached.(TimestampedValue); ok {
 68//	            return time.Since(tv.Timestamp) < time.Hour
 69//	        }
 70//	        return false
 71//	    },
 72//	)
 73package memo
 74
 75import (
 76	"gno.land/p/nt/bptree/v0"
 77	"gno.land/p/nt/ufmt/v0"
 78)
 79
 80// keyString derives a stable string key from an arbitrary value. Keys are
 81// compared by their string representation, mirroring the previous ordering
 82// behavior.
 83func keyString(key any) string {
 84	return ufmt.Sprintf("%v", key)
 85}
 86
 87// Memoizer is a structure to handle memoization of function results.
 88type Memoizer struct {
 89	cache *bptree.BPTree
 90}
 91
 92// New creates a new Memoizer instance.
 93func New() *Memoizer {
 94	return &Memoizer{
 95		cache: bptree.NewBPTree32(),
 96	}
 97}
 98
 99// Memoize ensures the result of the given function is cached for the specified key.
100func (m *Memoizer) Memoize(key any, fn func() any) any {
101	k := keyString(key)
102	if m.cache.Has(k) {
103		return m.cache.Get(k)
104	}
105
106	value := fn()
107	m.cache.Set(k, value)
108	return value
109}
110
111// MemoizeWithValidator ensures the result is cached and valid according to the validator function.
112func (m *Memoizer) MemoizeWithValidator(key any, fn func() any, isValid func(any) bool) any {
113	k := keyString(key)
114	if m.cache.Has(k) {
115		cached := m.cache.Get(k)
116		if isValid(cached) {
117			return cached
118		}
119	}
120
121	value := fn()
122	m.cache.Set(k, value)
123	return value
124}
125
126// Invalidate removes the cached value for the specified key.
127func (m *Memoizer) Invalidate(key any) {
128	m.cache.Remove(keyString(key))
129}
130
131// Clear clears all cached values.
132func (m *Memoizer) Clear() {
133	m.cache = bptree.NewBPTree32()
134}
135
136// Size returns the number of items currently in the cache.
137func (m *Memoizer) Size() int {
138	return m.cache.Size()
139}