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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}