unc_sdk/store/lazy/
mod.rs

1//! A persistent lazy storage value. Stores a value for a given key.
2//! Example:
3//! If the underlying value is large, e.g. the contract needs to store an image, but it doesn't need
4//! to have access to this image at regular calls, then the contract can wrap this image into
5//! [`Lazy`] and it will not be deserialized until requested.
6
7mod impls;
8
9use borsh::{to_vec, BorshDeserialize, BorshSerialize};
10use once_cell::unsync::OnceCell;
11
12use unc_sdk_macros::unc;
13
14use crate::env;
15use crate::store::ERR_INCONSISTENT_STATE;
16use crate::utils::{CacheEntry, EntryState};
17use crate::IntoStorageKey;
18
19const ERR_VALUE_SERIALIZATION: &str = "Cannot serialize value with Borsh";
20const ERR_VALUE_DESERIALIZATION: &str = "Cannot deserialize value with Borsh";
21const ERR_NOT_FOUND: &str = "No value found for the given key";
22
23fn expect_key_exists<T>(val: Option<T>) -> T {
24    val.unwrap_or_else(|| env::panic_str(ERR_NOT_FOUND))
25}
26
27fn expect_consistent_state<T>(val: Option<T>) -> T {
28    val.unwrap_or_else(|| env::panic_str(ERR_INCONSISTENT_STATE))
29}
30
31pub(crate) fn load_and_deserialize<T>(key: &[u8]) -> CacheEntry<T>
32where
33    T: BorshDeserialize,
34{
35    let bytes = expect_key_exists(env::storage_read(key));
36    let val =
37        T::try_from_slice(&bytes).unwrap_or_else(|_| env::panic_str(ERR_VALUE_DESERIALIZATION));
38    CacheEntry::new_cached(Some(val))
39}
40
41pub(crate) fn serialize_and_store<T>(key: &[u8], value: &T)
42where
43    T: BorshSerialize,
44{
45    let serialized = to_vec(value).unwrap_or_else(|_| env::panic_str(ERR_VALUE_SERIALIZATION));
46    env::storage_write(key, &serialized);
47}
48
49/// An persistent lazily loaded value, that stores a value in the storage.
50///
51/// This will only write to the underlying store if the value has changed, and will only read the
52/// existing value from storage once.
53///
54/// # Examples
55/// ```
56/// use unc_sdk::store::Lazy;
57///
58/// let mut a = Lazy::new(b"a", "test string".to_string());
59/// assert_eq!(*a, "test string");
60///
61/// *a = "new string".to_string();
62/// assert_eq!(a.get(), "new string");
63/// ```
64#[unc(inside_uncsdk)]
65pub struct Lazy<T>
66where
67    T: BorshSerialize,
68{
69    /// Key bytes to index the contract's storage.
70    storage_key: Box<[u8]>,
71    #[borsh(skip, bound(deserialize = ""))] // removes `core::default::Default` bound from T
72    /// Cached value which is lazily loaded and deserialized from storage.
73    cache: OnceCell<CacheEntry<T>>,
74}
75
76impl<T> Lazy<T>
77where
78    T: BorshSerialize,
79{
80    /// Initializes new lazily loaded value with a given storage prefix and the value to initialize
81    /// it with.
82    ///
83    /// This prefix can be anything that implements [`IntoStorageKey`]. The prefix is used when
84    /// storing and looking up values in storage to ensure no collisions with other collections.
85    pub fn new<S>(key: S, value: T) -> Self
86    where
87        S: IntoStorageKey,
88    {
89        Self {
90            storage_key: key.into_storage_key().into_boxed_slice(),
91            cache: OnceCell::from(CacheEntry::new_modified(Some(value))),
92        }
93    }
94
95    /// Updates the value with a new value. This does not load the current value from storage.
96    pub fn set(&mut self, value: T) {
97        if let Some(v) = self.cache.get_mut() {
98            *v.value_mut() = Some(value);
99        } else {
100            self.cache
101                .set(CacheEntry::new_modified(Some(value)))
102                .unwrap_or_else(|_| env::panic_str("cache is checked to not be filled above"))
103        }
104    }
105
106    /// Writes any changes to the value to storage. This will automatically be done when the
107    /// value is dropped through [`Drop`] so this should only be used when the changes need to be
108    /// reflected in the underlying storage before then.
109    pub fn flush(&mut self) {
110        if let Some(v) = self.cache.get_mut() {
111            if v.is_modified() {
112                // Value was modified, serialize and put the serialized bytes in storage.
113                let value = expect_consistent_state(v.value().as_ref());
114                serialize_and_store(&self.storage_key, value);
115
116                // Replaces cache entry state to cached because the value in memory matches the
117                // stored value. This avoids writing the same value twice.
118                v.replace_state(EntryState::Cached);
119            }
120        }
121    }
122}
123
124impl<T> Lazy<T>
125where
126    T: BorshSerialize + BorshDeserialize,
127{
128    /// Returns a reference to the lazily loaded storage value.
129    /// The load from storage only happens once, and if the value is already cached, it will not
130    /// be reloaded.
131    ///
132    /// This function will panic if the cache is not loaded and the value at the key does not exist.
133    pub fn get(&self) -> &T {
134        let entry = self.cache.get_or_init(|| load_and_deserialize(&self.storage_key));
135
136        expect_consistent_state(entry.value().as_ref())
137    }
138
139    /// Returns a reference to the lazily loaded storage value.
140    /// The load from storage only happens once, and if the value is already cached, it will not
141    /// be reloaded.
142    ///
143    /// This function will panic if the cache is not loaded and the value at the key does not exist.
144    pub fn get_mut(&mut self) -> &mut T {
145        self.cache.get_or_init(|| load_and_deserialize(&self.storage_key));
146        let entry = self.cache.get_mut().unwrap_or_else(|| env::abort());
147
148        expect_consistent_state(entry.value_mut().as_mut())
149    }
150}
151
152#[cfg(not(target_arch = "wasm32"))]
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    pub fn test_lazy() {
159        let mut a = Lazy::new(b"a", 8u32);
160        assert_eq!(a.get(), &8);
161
162        assert!(!env::storage_has_key(b"a"));
163        a.flush();
164        assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 8);
165
166        a.set(42);
167
168        // Value in storage will still be 8 until the value is flushed
169        assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 8);
170        assert_eq!(*a, 42);
171
172        *a = 30;
173        let serialized = to_vec(&a).unwrap();
174        drop(a);
175        assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 30);
176
177        let lazy_loaded = Lazy::<u32>::try_from_slice(&serialized).unwrap();
178        assert!(lazy_loaded.cache.get().is_none());
179
180        let b = Lazy::new(b"b", 30);
181        assert!(!env::storage_has_key(b"b"));
182
183        // A value that is not stored in storage yet and one that has not been loaded yet can
184        // be checked for equality.
185        assert_eq!(lazy_loaded, b);
186    }
187
188    #[test]
189    pub fn test_debug() {
190        let mut lazy = Lazy::new(b"m", 8u8);
191        if cfg!(feature = "expensive-debug") {
192            assert_eq!(format!("{:?}", lazy), "8");
193        } else {
194            assert_eq!(format!("{:?}", lazy), "Lazy { storage_key: [109], cache: Some(CacheEntry { value: Some(8), state: Modified }) }");
195        }
196
197        lazy.flush();
198        if !cfg!(feature = "expensive-debug") {
199            assert_eq!(format!("{:?}", lazy), "Lazy { storage_key: [109], cache: Some(CacheEntry { value: Some(8), state: Cached }) }");
200        }
201
202        // Serialize and deserialize to simulate storing and loading.
203        let serialized = borsh::to_vec(&lazy).unwrap();
204        drop(lazy);
205        let lazy = Lazy::<u8>::try_from_slice(&serialized).unwrap();
206        if cfg!(feature = "expensive-debug") {
207            assert_eq!(format!("{:?}", lazy), "8");
208        } else {
209            assert_eq!(format!("{:?}", lazy), "Lazy { storage_key: [109], cache: None }");
210        }
211    }
212}