//! # 💽 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)),
}
}
}