use std::path::Path;
use std::sync::{Arc, Mutex, MutexGuard};
use crate::core::{AppError, ErrorKind};
use thingd::{
MemoryEvent, MemoryObject, ReplicationConfig, ReplicationService, ThingStore, ThingdResult,
};
pub enum NativeThingdEngine {
Memory(Box<thingd::MemoryEngine>),
Persistent(Box<thingd::PersistentEngine>),
}
impl NativeThingdEngine {
pub(crate) fn with_store<R>(&mut self, operation: impl FnOnce(&mut dyn ThingStore) -> R) -> R {
match self {
Self::Memory(engine) => operation(engine.as_mut()),
Self::Persistent(engine) => operation(engine.as_mut()),
}
}
pub(crate) fn with_replication_service<R>(
&mut self,
config: ReplicationConfig,
operation: impl FnOnce(&mut ReplicationService<'_>) -> ThingdResult<R>,
) -> ThingdResult<R> {
self.with_store(|store| operation(&mut ReplicationService::new(store, config)))
}
}
#[derive(Clone)]
pub struct NativeThingdStore {
engine: Arc<Mutex<NativeThingdEngine>>,
}
fn lock_engine(
store: &Mutex<NativeThingdEngine>,
) -> Result<MutexGuard<'_, NativeThingdEngine>, AppError> {
store.lock().map_err(|e| {
AppError::new(
ErrorKind::Internal,
format!("thingd engine mutex poisoned: {e}"),
)
})
}
impl NativeThingdStore {
#[must_use]
pub fn memory() -> Self {
Self {
engine: Arc::new(Mutex::new(NativeThingdEngine::Memory(Box::new(
thingd::MemoryEngine::new(),
)))),
}
}
pub fn persistent(path: impl AsRef<Path>) -> Result<Self, thingd::ThingdError> {
Self::persistent_with_options(path, thingd::PersistentOpenOptions::default())
}
pub fn persistent_with_options(
path: impl AsRef<Path>,
options: thingd::PersistentOpenOptions,
) -> Result<Self, thingd::ThingdError> {
let engine = thingd::PersistentEngine::open_with_options(path, options)?;
Ok(Self {
engine: Arc::new(Mutex::new(NativeThingdEngine::Persistent(Box::new(engine)))),
})
}
pub fn with_engine<R>(
&self,
operation: impl FnOnce(&mut NativeThingdEngine) -> R,
) -> Result<R, AppError> {
let mut engine = lock_engine(&self.engine)?;
Ok(operation(&mut engine))
}
pub fn lock(&self) -> Result<MutexGuard<'_, NativeThingdEngine>, AppError> {
lock_engine(&self.engine)
}
pub fn put_object_replicated(
&self,
object: MemoryObject,
config: &ReplicationConfig,
) -> Result<MemoryObject, AppError> {
ensure_source_config(config)?;
let config = config.clone();
self.with_engine(|engine| {
engine.with_store(|store| {
let object = store
.put_object(object)
.map_err(|error| AppError::new(ErrorKind::Internal, error.to_string()))?;
ReplicationService::new(store, config)
.record_object_upsert(&object)
.map_err(|error| AppError::new(ErrorKind::Internal, error.to_string()))?;
Ok(object)
})
})?
}
pub fn delete_object_replicated(
&self,
collection: &str,
id: &str,
config: &ReplicationConfig,
) -> Result<(), AppError> {
ensure_source_config(config)?;
let config = config.clone();
self.with_engine(|engine| {
engine.with_store(|store| {
store
.delete_object(collection, id)
.map_err(|error| AppError::new(ErrorKind::Internal, error.to_string()))?;
ReplicationService::new(store, config)
.record_object_delete(collection, id)
.map_err(|error| AppError::new(ErrorKind::Internal, error.to_string()))
})
})?
}
pub fn append_event_replicated(
&self,
event: MemoryEvent,
config: &ReplicationConfig,
) -> Result<MemoryEvent, AppError> {
ensure_source_config(config)?;
let config = config.clone();
self.with_engine(|engine| {
engine.with_store(|store| {
let event = store
.append_event(event)
.map_err(|error| AppError::new(ErrorKind::Internal, error.to_string()))?;
ReplicationService::new(store, config)
.record_event_append(&event)
.map_err(|error| AppError::new(ErrorKind::Internal, error.to_string()))?;
Ok(event)
})
})?
}
}
fn ensure_source_config(config: &ReplicationConfig) -> Result<(), AppError> {
if config.role != thingd::ReplicationRole::Source {
return Err(AppError::new(
ErrorKind::Validation,
"replication-aware native mutations require a source configuration",
));
}
if config.source_id.trim().is_empty() {
return Err(AppError::new(
ErrorKind::Validation,
"replication-aware native mutations require a source ID",
));
}
Ok(())
}