#[cfg(feature = "store-cosmosdb")]
mod cosmosdb;
#[cfg(feature = "store-dynamodb")]
mod dynamodb;
#[cfg(feature = "store-firestore")]
mod firestore;
#[cfg(feature = "store-fjall")]
mod fjall;
pub mod keyspaces;
#[cfg(feature = "store-redis")]
mod redis;
#[cfg(test)]
pub(crate) mod test_support;
pub use keyspaces::{
KS_ACL, KS_ASSIGNMENTS, KS_DIDS, KS_DOMAINS, KS_IDENTITY, KS_META, KS_OUTBOUND_QUEUE,
KS_PASSKEY_STEP_UP, KS_PENDING_PURGES, KS_REGISTRY, KS_SESSIONS, KS_STATS, KS_TIMESERIES,
KS_TSP_RELATIONSHIPS, KS_WITNESSES,
};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde::Serialize;
use serde::de::DeserializeOwned;
use super::config::{FjallTuning, StoreConfig};
use super::error::AppError;
pub mod key_prefix {
pub const SESSION: &str = "session:";
pub const REFRESH: &str = "refresh:";
pub const DID: &str = "did:";
pub const CONTENT: &str = "content:";
pub const OWNER: &str = "owner:";
pub const WATCHER_SYNC: &str = "watcher_sync:";
pub const PASSKEY: &str = "passkey:";
pub const ACL: &str = "acl:";
pub const STATS: &str = "stats:";
pub const TIMESERIES: &str = "ts:";
}
pub type RawKvPair = (Vec<u8>, Vec<u8>);
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait KeyspaceOps: Send + Sync {
fn insert_raw(&self, key: Vec<u8>, value: Vec<u8>) -> BoxFuture<'_, Result<(), AppError>>;
fn get_raw(&self, key: Vec<u8>) -> BoxFuture<'_, Result<Option<Vec<u8>>, AppError>>;
fn remove(&self, key: Vec<u8>) -> BoxFuture<'_, Result<(), AppError>>;
fn contains_key(&self, key: Vec<u8>) -> BoxFuture<'_, Result<bool, AppError>>;
fn prefix_iter_raw(&self, prefix: Vec<u8>) -> BoxFuture<'_, Result<Vec<RawKvPair>, AppError>>;
fn take_raw_atomic(&self, key: Vec<u8>) -> BoxFuture<'_, Result<Option<Vec<u8>>, AppError>>;
fn incr_raw(&self, key: Vec<u8>) -> BoxFuture<'_, Result<u64, AppError>>;
}
pub trait BatchOps: Send {
fn insert_raw(&mut self, keyspace: &str, key: Vec<u8>, value: Vec<u8>);
fn remove(&mut self, keyspace: &str, key: Vec<u8>);
fn commit(self: Box<Self>) -> BoxFuture<'static, Result<(), AppError>>;
}
pub trait StorageBackend: Send + Sync {
fn keyspace(&self, name: &str) -> Result<(String, Arc<dyn KeyspaceOps>), AppError>;
fn batch(&self) -> Box<dyn BatchOps>;
fn persist(&self) -> BoxFuture<'_, Result<(), AppError>>;
}
#[derive(Clone)]
pub struct Store {
inner: Arc<dyn StorageBackend>,
}
#[derive(Clone)]
pub struct KeyspaceHandle {
pub name: String,
inner: Arc<dyn KeyspaceOps>,
backend: Arc<dyn StorageBackend>,
}
pub struct WriteBatch {
inner: Box<dyn BatchOps>,
}
impl Store {
pub async fn open(config: &StoreConfig) -> Result<Self, AppError> {
Self::open_with(config, &FjallTuning::default()).await
}
pub async fn open_with(config: &StoreConfig, tuning: &FjallTuning) -> Result<Self, AppError> {
let backend = create_backend(config, tuning).await?;
Ok(Self {
inner: Arc::from(backend),
})
}
pub fn keyspace(&self, name: &str) -> Result<KeyspaceHandle, AppError> {
let (ks_name, ops) = self.inner.keyspace(name)?;
Ok(KeyspaceHandle {
name: ks_name,
inner: ops,
backend: self.inner.clone(),
})
}
pub fn batch(&self) -> WriteBatch {
WriteBatch {
inner: self.inner.batch(),
}
}
pub async fn persist(&self) -> Result<(), AppError> {
self.inner.persist().await
}
}
impl WriteBatch {
pub fn insert<V: Serialize>(
&mut self,
ks: &KeyspaceHandle,
key: impl Into<Vec<u8>>,
value: &V,
) -> Result<(), AppError> {
let bytes = serde_json::to_vec(value)?;
self.inner.insert_raw(&ks.name, key.into(), bytes);
Ok(())
}
pub fn insert_raw(
&mut self,
ks: &KeyspaceHandle,
key: impl Into<Vec<u8>>,
value: impl Into<Vec<u8>>,
) {
self.inner.insert_raw(&ks.name, key.into(), value.into());
}
pub fn remove(&mut self, ks: &KeyspaceHandle, key: impl Into<Vec<u8>>) {
self.inner.remove(&ks.name, key.into());
}
pub async fn commit(self) -> Result<(), AppError> {
self.inner.commit().await
}
}
impl KeyspaceHandle {
pub fn batch(&self) -> WriteBatch {
WriteBatch {
inner: self.backend.batch(),
}
}
pub async fn insert<V: Serialize>(
&self,
key: impl Into<Vec<u8>>,
value: &V,
) -> Result<(), AppError> {
let bytes = serde_json::to_vec(value)?;
self.inner.insert_raw(key.into(), bytes).await
}
pub async fn get<V: DeserializeOwned + Send + 'static>(
&self,
key: impl Into<Vec<u8>>,
) -> Result<Option<V>, AppError> {
match self.inner.get_raw(key.into()).await? {
Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
None => Ok(None),
}
}
pub async fn take<V: DeserializeOwned + Send + 'static>(
&self,
key: impl Into<Vec<u8>>,
) -> Result<Option<V>, AppError> {
match self.inner.take_raw_atomic(key.into()).await? {
Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
None => Ok(None),
}
}
pub async fn take_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
self.inner.take_raw_atomic(key.into()).await
}
pub async fn incr_raw(&self, key: impl Into<Vec<u8>>) -> Result<u64, AppError> {
self.inner.incr_raw(key.into()).await
}
pub async fn remove(&self, key: impl Into<Vec<u8>>) -> Result<(), AppError> {
self.inner.remove(key.into()).await
}
pub async fn insert_raw(
&self,
key: impl Into<Vec<u8>>,
value: impl Into<Vec<u8>>,
) -> Result<(), AppError> {
self.inner.insert_raw(key.into(), value.into()).await
}
pub async fn get_raw(&self, key: impl Into<Vec<u8>>) -> Result<Option<Vec<u8>>, AppError> {
self.inner.get_raw(key.into()).await
}
pub async fn contains_key(&self, key: impl Into<Vec<u8>>) -> Result<bool, AppError> {
self.inner.contains_key(key.into()).await
}
pub async fn iter_all(&self) -> Result<Vec<RawKvPair>, AppError> {
self.prefix_iter_raw(b"").await
}
pub async fn prefix_iter_raw(
&self,
prefix: impl Into<Vec<u8>>,
) -> Result<Vec<RawKvPair>, AppError> {
self.inner.prefix_iter_raw(prefix.into()).await
}
pub async fn approximate_len(&self) -> Result<usize, AppError> {
Ok(self.prefix_iter_raw(b"").await?.len())
}
pub async fn verify_integrity(&self) -> Result<u64, AppError> {
let all = self.prefix_iter_raw(b"").await?;
let mut corrupted = 0u64;
for (key, value) in &all {
let key_str = String::from_utf8_lossy(key);
if key_str.starts_with("content:")
|| key_str.starts_with("owner:")
|| key_str.starts_with("refresh:")
|| key_str.starts_with("ts:")
{
continue;
}
if serde_json::from_slice::<serde_json::Value>(value).is_err() {
tracing::warn!(key = %key_str, "integrity check: corrupted entry (invalid JSON)");
corrupted += 1;
}
}
Ok(corrupted)
}
}
#[allow(unused_variables)]
async fn create_backend(
config: &StoreConfig,
tuning: &FjallTuning,
) -> Result<Box<dyn StorageBackend>, AppError> {
#[cfg(feature = "store-fjall")]
{
return fjall::FjallBackend::open_with(config, tuning);
}
#[cfg(feature = "store-redis")]
{
return redis::RedisBackend::open(config).await;
}
#[cfg(feature = "store-dynamodb")]
{
return dynamodb::DynamoDbBackend::open(config).await;
}
#[cfg(feature = "store-firestore")]
{
return firestore::FirestoreBackend::open(config).await;
}
#[cfg(feature = "store-cosmosdb")]
{
return cosmosdb::CosmosDbBackend::open(config).await;
}
#[allow(unreachable_code)]
Err(AppError::Config(
"no storage backend compiled — enable a store-* feature".into(),
))
}