use std::path::Path;
use std::sync::{Arc, Mutex, MutexGuard};
use crate::core::{AppError, ErrorKind};
#[cfg(feature = "http-client")]
use thingd::ThingdResult;
use thingd::{MemoryEvent, MemoryObject, ReplicationConfig, ReplicationService, ThingStore};
#[cfg(feature = "thingd-maintenance")]
use thingd::{StorageDiagnostics, StorageMaintenanceStatus, StorageValidationReport};
#[cfg(feature = "thingd-maintenance")]
pub use thingd::{PersistentSearchMode, RecoveryBudget};
#[cfg(feature = "thingd-connectors")]
pub mod connectors {
pub use thingd::{
Column, ColumnType, Connector, ConnectorAuth, ConnectorConfig, ConnectorDescriptor,
ConnectorOperation, ExcelConnector, FileConnector, GoogleSheetsConnector, MysqlConnector,
PostgresConnector, PullStream, Schema, SslMode, SyncStrategy,
};
}
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()),
}
}
#[cfg(feature = "http-client")]
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)
}
#[cfg(feature = "thingd-maintenance")]
pub fn storage_diagnostics(&self) -> Result<StorageDiagnostics, AppError> {
self.with_engine(|engine| engine.with_store(|store| store.storage_diagnostics()))?
.map_err(native_error)
}
#[cfg(feature = "thingd-maintenance")]
pub fn storage_maintenance_status(&self) -> Result<StorageMaintenanceStatus, AppError> {
self.with_engine(|engine| engine.with_store(|store| store.storage_maintenance_status()))
}
#[cfg(feature = "thingd-maintenance")]
pub fn validate_path(path: impl AsRef<Path>) -> Result<StorageValidationReport, AppError> {
thingd::PersistentEngine::validate_path(path).map_err(native_error)
}
#[cfg(feature = "thingd-maintenance")]
pub fn compact_storage(&self) -> Result<(), AppError> {
self.with_engine(|engine| engine.with_store(|store| store.compact_storage()))?
.map_err(native_error)
}
#[cfg(feature = "thingd-maintenance")]
pub fn search_rebuild_step(&self, batch_size: usize) -> Result<bool, AppError> {
self.with_engine(|engine| engine.with_store(|store| store.search_rebuild_step(batch_size)))?
.map_err(native_error)
}
#[cfg(feature = "thingd-maintenance")]
pub fn retry_search_rebuild(&self) -> Result<bool, AppError> {
self.with_engine(|engine| engine.with_store(|store| store.retry_search_rebuild()))
}
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)
})
})?
}
}
#[cfg(feature = "thingd-maintenance")]
fn native_error(error: thingd::ThingdError) -> AppError {
AppError::new(ErrorKind::Dependency, error.to_string())
}
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(())
}
#[cfg(all(test, feature = "thingd-maintenance"))]
mod maintenance_tests {
use super::NativeThingdStore;
#[test]
fn reports_memory_store_diagnostics_and_maintenance() {
let store = NativeThingdStore::memory();
store
.with_engine(|engine| {
engine
.with_store(|native| {
native.put_object(thingd::MemoryObject::new(
"notes",
"one",
r#"{"ok":true}"#,
))
})
.unwrap();
})
.unwrap();
let diagnostics = store.storage_diagnostics().unwrap();
assert_eq!(diagnostics.objects, 1);
assert_eq!(diagnostics.events, 0);
let maintenance = store.storage_maintenance_status().unwrap();
assert_eq!(maintenance.state, "idle");
}
#[test]
fn memory_search_rebuild_controls_are_safe() {
let store = NativeThingdStore::memory();
assert!(store.search_rebuild_step(32).unwrap());
assert!(!store.retry_search_rebuild().unwrap());
}
}