ark-api 0.17.0-pre.15

Ark API
Documentation
//! # 💽 Storage API
//!
//! This API enables modules to retrieve and persist arbitrary arbitrary data in named _data stores_.

use crate::{ffi::storage_v1 as ffi, Error, ErrorCode};
use std::rc::Rc;

#[doc(hidden)]
pub use ffi::API as FFI_API;

pub use crate::user::UserId;

/// Storage realm, where a store exists.
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum StoreRealm {
    /// Store is not persisted and only available while open in the current module instance.
    ///
    /// This is mostly useful for testing purposes, or possibly to be able to "swap" out and
    /// use more than 2 GB of memory in a Wasm module.
    ModuleInstance,

    /// Store is specific to a single user, is persisted only on the local.
    ///
    /// This means that each user will get their own version of a store with the same name
    Device,

    /// Store is persisted only on the local device, store names are device-wide for any module
    /// to access.
    ///
    /// This is useful, for instance, for testing purposes before creating a global cache, as
    /// well as for keeping some local caches in modules.
    DeviceUser(UserId),

    /// Store is persisted on the local device as well as asynchronously automatically
    /// synchronized globally.
    Global,

    /// Store is specific to a single user, is persisted on the local device as well
    /// as asynchronously automatically synchronized globally.
    ///
    /// This means that each user will get their own version of a store with the same name
    GlobalUser(UserId),
}

/// Name of a store
///
/// This has to be a static string and has certain constraints of it such as the length and types of characters allowed in it (alphanumeric and '-', '_').
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct StoreName(&'static str);

impl std::fmt::Debug for StoreName {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self.0)
    }
}

impl StoreName {
    /// Maximum allowed length of name of a store
    ///
    /// # Internal
    ///
    /// TODO: This is currently too long, should reduce to 32 but currently can't as we have stores in use with longer names.
    /// The host has a limitation on 64 total for store name + user id (~20 chars), see `SyncStoreName`
    pub const MAX_LEN: usize = 54;

    /// Create a new store name as a constant variable, this is the preferred method
    pub const fn const_new(name: &'static str) -> Self {
        if name.len() > Self::MAX_LEN {
            // TODO: Add more descriptions here when Rust supports formatting in const functions
            panic!("Store name too long");
        }
        if name.is_empty() {
            panic!("Store name can't be empty");
        }
        // TODO: validate alphanumeric/dash/divider characters, but not available in const fn yet
        Self(name)
    }

    /// Create a new non-const store name
    pub fn new(name: &'static str) -> Self {
        if name.len() > Self::MAX_LEN {
            panic!("Store can't be named {name:?} as that name is too long ({len} characters, max is {max_len})", len = name.len(), max_len = Self::MAX_LEN);
        }
        if name.is_empty() {
            panic!("Store name can't be empty");
        }
        if !name
            .chars()
            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
        {
            panic!("Store name {name:?} does not contain just alphanumeric or dash or divider characters");
        }

        Self(name)
    }

    /// Access the name as a string
    pub fn as_str(&self) -> &'static str {
        self.0
    }
}

/// Simple key-value data store for which all values are loaded at startup.
///
/// Prefer using `AsyncStore` for large data stores.
#[derive(PartialEq, Clone)]
pub struct Store {
    handle: Rc<ffi::StoreHandle>,
}

impl Store {
    /// Open a store for a specific realm and name.
    ///
    /// If the store doesn't exist, locally or globally, it will be automatically created.
    ///
    /// Names of stores is limited to max 32 characters and can only be alphanumeric or dash or underscore.
    ///
    /// # Global stores
    ///
    /// Note that for [`StoreRealm::Global`] this can be a very expensive operation as it will
    /// synchronize and download latest data. Prefer to call this early in module initialization.
    ///
    /// Also the store name is global, so be precise and unique in your naming to not pollute or
    /// collide other stores.
    ///
    /// # Panics
    ///
    /// This function panics if a store could not be opened for the given name.
    pub fn open(realm: StoreRealm, name: StoreName) -> Self {
        let handle = store_open(realm, name, ffi::StoreMode::Sync);
        Self {
            handle: Rc::new(handle),
        }
    }

    /// Start opening a store for a specific realm and name.
    ///
    /// If the store doesn't exist, locally or globally, it will be automatically created.
    ///
    /// Names of stores is limited to max 32 characters and can only be alphanumeric or dash or underscore.
    ///
    /// # Global stores
    ///
    /// The store name is global, so be precise and unique in your naming to not pollute or collide
    /// other stores.
    ///
    /// # Panics
    ///
    /// This function panics if a store could not be opened for the given name.
    pub fn async_open(realm: StoreRealm, name: StoreName) -> OpenSyncStorePromise {
        let (ffi_realm, ffi_user_id) = as_ffi_args(&realm);
        let open_handle = ffi::store_async_open(
            ffi_realm,
            ffi_user_id.unwrap_or(&[]),
            ffi::StoreMode::Sync,
            name.0,
        );
        OpenSyncStorePromise {
            handle: open_handle,
        }
    }

    /// Insert a key
    ///
    /// If the key already exists in the store, it will be overwritten
    pub fn insert<K: AsRef<[u8]>, V: AsRef<[u8]>>(&mut self, key: K, value: V) {
        ffi::immediate_insert(*self.handle, key.as_ref(), value.as_ref());
    }

    /// Remove a key
    pub fn remove<K: AsRef<[u8]>>(&mut self, key: K) {
        ffi::immediate_remove(*self.handle, key.as_ref());
    }

    /// Get a list of all keys currently in the store.
    ///
    /// Note that this is a snapshot from the time of the call, for stores using [`StoreRealm::Global`] there
    /// is a chance keys can be removed while one is iterating through this list, and new keys can also
    /// be added or the value of the keys changed
    ///
    /// This can be bit expensive operation for stores that contain a lot of keys
    pub fn keys(&self) -> Keys {
        let bytes = ffi::immediate_list(*self.handle);
        Keys::new(bytes)
    }

    /// Check if the store has a specific key
    pub fn contains<K: AsRef<[u8]>>(&self, key: K) -> bool {
        match ffi::immediate_get(*self.handle, key.as_ref()) {
            Ok(_) => true,
            Err(ErrorCode::NotFound) => false, // key not found
            Err(error) => panic!("Unexpected error: {}", Error::from(error)),
        }
    }

    /// Get the value of a key, if available
    pub fn get<K: AsRef<[u8]>>(&self, key: K) -> Option<Vec<u8>> {
        match ffi::immediate_get(*self.handle, key.as_ref()) {
            Ok(bytes) => Some(bytes),
            Err(ErrorCode::NotFound) => None, // key not found
            Err(error) => panic!("Unexpected error: {}", Error::from(error)),
        }
    }
}

impl Drop for Store {
    fn drop(&mut self) {
        if Rc::strong_count(&self.handle) == 1 {
            ffi::store_close(*self.handle);
        }
    }
}

/// Simple key-value data store which loads values in a lazy way.
#[derive(PartialEq, Clone)]
pub struct AsyncStore {
    handle: Rc<ffi::StoreHandle>,
}

impl AsyncStore {
    /// Open an async store for a specific realm and name.
    ///
    /// If the store doesn't exist, locally or globally, it will be automatically created.
    ///
    /// Names of stores is limited to max 32 characters and can only be alphanumeric or dash or underscore.
    ///
    /// # Global stores
    ///
    /// Note that for [`StoreRealm::Global`] this will synchronize all the keys during the initial
    /// opening. Prefer to call this early in module initialization.
    ///
    /// Also the store name is global, so be precise and unique in your naming to not pollute or
    /// collide other stores.
    ///
    /// # Panics
    ///
    /// This function panics if a store could not be opened for the given name.
    pub fn open(realm: StoreRealm, name: StoreName) -> Self {
        let handle = store_open(realm, name, ffi::StoreMode::Async);
        Self {
            handle: Rc::new(handle),
        }
    }

    /// Start opening an async store for a specific realm and name.
    ///
    /// If the store doesn't exist, locally or globally, it will be automatically created.
    ///
    /// Names of stores is limited to max 32 characters and can only be alphanumeric or dash or underscore.
    ///
    /// # Global stores
    ///
    /// The store name is global, so be precise and unique in your naming to not pollute or collide
    /// other stores.
    ///
    /// # Panics
    ///
    /// This function panics if a store could not be opened for the given name.
    pub fn async_open(realm: StoreRealm, name: StoreName) -> OpenAsyncStorePromise {
        let (ffi_realm, ffi_user_id) = as_ffi_args(&realm);
        let open_handle = ffi::store_async_open(
            ffi_realm,
            ffi_user_id.unwrap_or(&[]),
            ffi::StoreMode::Async,
            name.0,
        );
        OpenAsyncStorePromise {
            handle: open_handle,
        }
    }

    /// Get the value of a key, if available; returns a handle that will later resolve to this
    /// value, if present.
    pub fn get<K: AsRef<[u8]>>(&self, key: K) -> LazyGetHandle {
        let handle = ffi::async_get(*self.handle, key.as_ref());
        LazyGetHandle::new(handle)
    }

    /// Insert a key
    ///
    /// If the key already exists in the store, it will be overwritten
    pub fn insert<K: AsRef<[u8]>, V: AsRef<[u8]>>(&mut self, key: K, value: V) {
        ffi::immediate_insert(*self.handle, key.as_ref(), value.as_ref());
    }

    /// Remove a key
    pub fn remove<K: AsRef<[u8]>>(&mut self, key: K) {
        ffi::immediate_remove(*self.handle, key.as_ref());
    }

    /// Get a list of all keys currently in the store.
    ///
    /// Note that this is a snapshot from the time of the call, for stores using [`StoreRealm::Global`] there
    /// is a chance keys can be removed while one is iterating through this list, and new keys can also
    /// be added or the value of the keys changed
    ///
    /// This can be bit expensive operation for stores that contain a lot of keys
    pub fn keys(&self) -> Keys {
        let bytes = ffi::immediate_list(*self.handle);
        Keys::new(bytes)
    }

    /// Check if the store has a specific key
    pub fn contains<K: AsRef<[u8]>>(&self, key: K) -> bool {
        match ffi::immediate_get(*self.handle, key.as_ref()) {
            Ok(_) => true,
            Err(ErrorCode::NotFound) => false, // key not found
            Err(error) => panic!("Unexpected error: {}", Error::from(error)),
        }
    }
}

impl Drop for AsyncStore {
    fn drop(&mut self) {
        if Rc::strong_count(&self.handle) == 1 {
            ffi::store_close(*self.handle);
        }
    }
}

enum AsyncGetStatus {
    NotFinished,
    Found(Vec<u8>),
    NotFound,
}

/// Handle to a lazy `get` request for `AsyncStore`s.
///
/// Can be consumed as a `Future` or manually with the `is_ready` and `take` methods.
pub struct LazyGetHandle {
    handle: ffi::AsyncGetHandle,
    result: AsyncGetStatus,
}

impl LazyGetHandle {
    fn new(handle: ffi::AsyncGetHandle) -> Self {
        Self {
            handle,
            result: AsyncGetStatus::NotFinished,
        }
    }

    /// Checks if the request is done or not.
    ///
    /// Polls in the background, if necessary.
    pub fn is_ready(&mut self) -> bool {
        if !matches!(self.result, AsyncGetStatus::NotFinished) {
            return true;
        }
        match ffi::async_get_ready(self.handle) {
            Ok(data) => {
                self.result = AsyncGetStatus::Found(data);
                true
            }
            Err(ErrorCode::Unavailable) => false,
            Err(ErrorCode::NotFound) => {
                self.result = AsyncGetStatus::NotFound;
                true
            }
            Err(err) => panic!("Unexpected error: {}", Error::from(err)),
        }
    }

    /// Consumes the handle and returns the underlying `get` result.
    ///
    /// # Panics
    ///
    /// This will panic if `is_ready()` hasn't returned true yet.
    pub fn take(self) -> Option<Vec<u8>> {
        match self.result {
            AsyncGetStatus::NotFinished => {
                panic!("take() must be called after `is_ready` returned true")
            }
            AsyncGetStatus::NotFound => None,
            AsyncGetStatus::Found(data) => Some(data),
        }
    }
}

// Allow using `await` on `LazyGetHandle`.
impl std::future::Future for LazyGetHandle {
    type Output = Option<Vec<u8>>;

    fn poll(
        self: std::pin::Pin<&mut Self>,
        _cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Self::Output> {
        match ffi::async_get_ready(self.handle) {
            Ok(data) => std::task::Poll::Ready(Some(data)),
            Err(ErrorCode::Unavailable) => std::task::Poll::Pending,
            Err(ErrorCode::NotFound) => std::task::Poll::Ready(None),
            Err(err) => panic!("Unexpected error: {}", Error::from(err)),
        }
    }
}

/// Collection of keys in a store
///
/// This is created with [`Store::keys()`].
///
/// The individual keys can be iterated through with [`Keys::iter()`] which will return a `&[u8]` byte slice for each key
pub struct Keys {
    len: usize,
    bytes: Vec<u8>,
}

/// Iterator for keys in a store
///
/// This can only be created with [`Keys::iter()`]
pub struct KeysIterator<'a> {
    keys: &'a Keys,
    offset: usize,
}

impl Keys {
    pub(crate) fn new(bytes: Vec<u8>) -> Self {
        let mut offset: usize = 0;
        let len = Self::read_u32_len(&bytes, &mut offset).unwrap_or_default();

        Self { len, bytes }
    }

    /// Returns an iterator over the keys
    pub fn iter(&self) -> KeysIterator<'_> {
        KeysIterator {
            keys: self,
            offset: 4, // the keys start at this offset
        }
    }

    /// Returns the number of keys in the list
    pub fn len(&self) -> usize {
        self.len
    }

    /// Returns `true` if the list contains no keys
    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// Read a u32 value from our byte stream, but return as usize as we use it for length
    fn read_u32_len(bytes: &[u8], offset: &mut usize) -> Option<usize> {
        let result = bytes
            .get((*offset)..(*offset + 4))
            .map(|slice| u32::from_le_bytes(slice.try_into().unwrap()) as usize);
        *offset += 4;
        result
    }

    fn read_key<'a>(bytes: &'a [u8], offset: &mut usize) -> Option<&'a [u8]> {
        Self::read_u32_len(bytes, offset).map(|key_len| {
            let slice = bytes.get((*offset)..((*offset) + key_len)).unwrap();
            *offset += key_len;
            slice
        })
    }
}

impl<'a> Iterator for KeysIterator<'a> {
    type Item = &'a [u8];

    fn next(&mut self) -> Option<&'a [u8]> {
        Keys::read_key(&self.keys.bytes, &mut self.offset)
    }
}

impl<'a> ExactSizeIterator for KeysIterator<'a> {
    fn len(&self) -> usize {
        self.keys.len()
    }
}

fn as_ffi_args(realm: &StoreRealm) -> (ffi::StoreRealm, Option<&[u8]>) {
    match &realm {
        StoreRealm::ModuleInstance => (ffi::StoreRealm::ModuleInstance, None),
        StoreRealm::Device => (ffi::StoreRealm::DeviceShared, None),
        StoreRealm::DeviceUser(user_id) => (ffi::StoreRealm::DeviceUser, Some(user_id.as_ref())),
        StoreRealm::Global => (ffi::StoreRealm::GlobalShared, None),
        StoreRealm::GlobalUser(user_id) => (ffi::StoreRealm::GlobalUser, Some(user_id.as_ref())),
    }
}

fn store_open(realm: StoreRealm, name: StoreName, mode: ffi::StoreMode) -> ffi::StoreHandle {
    let (ffi_realm, ffi_user_id) = as_ffi_args(&realm);
    ffi::store_open3(ffi_realm, ffi_user_id.unwrap_or(&[]), mode, name.0)
}

/// A handle to an async-open sync-store operation.
pub struct OpenSyncStorePromise {
    handle: ffi::AsyncOpenHandle,
}

impl OpenSyncStorePromise {
    /// Try to consume the handle into the final store, once it's done loading.
    pub fn try_take(self) -> Result<Store, Self> {
        match ffi::store_async_poll_open(self.handle) {
            Ok(store_handle) => Ok(Store {
                handle: Rc::new(store_handle),
            }),
            Err(ErrorCode::Unavailable) => Err(self),
            Err(err) => panic!("Unexpected error: {}", Error::from(err)),
        }
    }
}

/// A handle to an async-open sync-store operation.
pub struct OpenAsyncStorePromise {
    handle: ffi::AsyncOpenHandle,
}

impl OpenAsyncStorePromise {
    /// Try to consume the handle into the final store, once it's done loading.
    pub fn try_take(self) -> Result<AsyncStore, Self> {
        match ffi::store_async_poll_open(self.handle) {
            Ok(store_handle) => Ok(AsyncStore {
                handle: Rc::new(store_handle),
            }),
            Err(ErrorCode::Unavailable) => Err(self),
            Err(err) => panic!("Unexpected error: {}", Error::from(err)),
        }
    }
}