|
| 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() interface{} { |
| 25 | +// // expensive operation |
| 26 | +// return "computed-value" |
| 27 | +// }) |
| 28 | +// |
| 29 | +// // Subsequent calls with same key return cached result |
| 30 | +// result = m.Memoize("key", func() interface{} { |
| 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() interface{} { |
| 48 | +// return TimestampedValue{ |
| 49 | +// Value: "data", |
| 50 | +// Timestamp: time.Now(), |
| 51 | +// } |
| 52 | +// }, |
| 53 | +// func(cached interface{}) 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 | +// ) |
| 61 | +package memo |
| 62 | + |
| 63 | +import ( |
| 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 |
| 69 | +type cacheEntry struct { |
| 70 | + key interface{} |
| 71 | + value interface{} |
| 72 | +} |
| 73 | + |
| 74 | +// Less implements btree.Record interface |
| 75 | +func (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. |
| 83 | +type Memoizer struct { |
| 84 | + cache *btree.BTree |
| 85 | +} |
| 86 | + |
| 87 | +// New creates a new Memoizer instance. |
| 88 | +func 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. |
| 95 | +func (m *Memoizer) Memoize(key interface{}, fn func() interface{}) interface{} { |
| 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. |
| 107 | +func (m *Memoizer) MemoizeWithValidator(key interface{}, fn func() interface{}, isValid func(interface{}) bool) interface{} { |
| 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. |
| 122 | +func (m *Memoizer) Invalidate(key interface{}) { |
| 123 | + m.cache.Delete(cacheEntry{key: key}) |
| 124 | +} |
| 125 | + |
| 126 | +// Clear clears all cached values. |
| 127 | +func (m *Memoizer) Clear() { |
| 128 | + m.cache.Clear(true) |
| 129 | +} |
| 130 | + |
| 131 | +// Size returns the number of items currently in the cache. |
| 132 | +func (m *Memoizer) Size() int { |
| 133 | + return m.cache.Len() |
| 134 | +} |
0 commit comments