kmp_adapter_embedded/adapter/
store.rs1use std::fs;
2use std::path::Path;
3use std::sync::Arc;
4
5use kmp_domain::PortError;
6
7use super::engine::redb::RedbEngine;
8use super::engine::{Engine, ReadTx, Table, WriteTx};
9use super::format_version::{self, StorageEngine};
10
11#[derive(Debug, Clone)]
22pub struct EmbeddedKernelStore {
23 engine: Arc<dyn Engine>,
24}
25
26impl EmbeddedKernelStore {
27 pub fn open(data_dir: &Path) -> Result<Self, PortError> {
32 Self::open_as(data_dir, None)
33 }
34
35 pub fn open_with_engine(data_dir: &Path, engine: StorageEngine) -> Result<Self, PortError> {
40 if engine == StorageEngine::Redb && !format_version::format_version_path(data_dir).exists()
41 {
42 return Err(PortError::InvalidState(
43 "the redb engine is legacy-only and cannot create a new store; use SQLite"
44 .to_string(),
45 ));
46 }
47 Self::open_as(data_dir, Some(engine))
48 }
49
50 pub fn engine_of(data_dir: &Path) -> Result<StorageEngine, PortError> {
52 format_version::check_or_stamp_as(data_dir, None)
53 }
54
55 fn open_as(data_dir: &Path, wanted: Option<StorageEngine>) -> Result<Self, PortError> {
56 fs::create_dir_all(data_dir).map_err(|error| {
57 PortError::Unavailable(format!(
58 "embedded store could not create data dir `{}`: {error}",
59 data_dir.display()
60 ))
61 })?;
62 let engine = format_version::check_or_stamp_as(data_dir, wanted)?;
63
64 let store_file = format_version::store_file_path_for(data_dir, engine);
65 fs::create_dir_all(store_file.parent().expect("store file has a parent")).map_err(
66 |error| {
67 PortError::Unavailable(format!(
68 "embedded store could not create store dir under `{}`: {error}",
69 data_dir.display()
70 ))
71 },
72 )?;
73
74 Self::open_store_file(&store_file, engine)
75 }
76
77 pub(crate) fn open_store_file(
82 store_file: &Path,
83 engine: StorageEngine,
84 ) -> Result<Self, PortError> {
85 let engine: Arc<dyn Engine> = match engine {
86 StorageEngine::Redb => Arc::new(RedbEngine::open_file(store_file)?),
87 StorageEngine::Sqlite => {
88 Arc::new(super::engine::sqlite::SqliteEngine::open_file(store_file)?)
89 }
90 };
91 Ok(Self { engine })
92 }
93
94 pub(crate) fn begin_write(&self) -> Result<Box<dyn WriteTx + '_>, PortError> {
95 self.engine.begin_write()
96 }
97
98 pub(crate) fn begin_read(&self) -> Result<Box<dyn ReadTx + '_>, PortError> {
99 self.engine.begin_read()
100 }
101
102 pub(crate) async fn run<T, F>(&self, task: F) -> Result<T, PortError>
105 where
106 T: Send + 'static,
107 F: FnOnce(&EmbeddedKernelStore) -> Result<T, PortError> + Send + 'static,
108 {
109 let store = self.clone();
110 tokio::task::spawn_blocking(move || task(&store))
111 .await
112 .map_err(|error| {
113 PortError::Unavailable(format!("embedded store worker failed: {error}"))
114 })?
115 }
116
117 pub async fn event_log_stats(&self) -> Result<(u64, u64), PortError> {
120 self.run(|store| {
121 let tx = store.begin_read()?;
122 let count = tx.count(Table::EventLog)?;
123 let last_sequence = tx.last_u64(Table::EventLog)?.map_or(0, |(key, _)| key);
124 Ok((count, last_sequence))
125 })
126 .await
127 }
128
129 pub fn compact_data_dir(data_dir: &Path) -> Result<bool, PortError> {
134 let engine = format_version::check_or_stamp(data_dir)?;
135 let store_file = format_version::store_file_path_for(data_dir, engine);
136 match engine {
137 StorageEngine::Redb => RedbEngine::compact_file(&store_file),
138 StorageEngine::Sqlite => super::engine::sqlite::SqliteEngine::compact_file(&store_file),
139 }
140 }
141}
142
143pub(crate) fn aggregate_key(root_node_id: &str, role: &str) -> String {
144 format!("{root_node_id}\u{1f}{role}")
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn the_public_api_cannot_create_a_fresh_redb_store() {
153 let data_dir = tempfile::tempdir().expect("temp data dir");
154 let error = EmbeddedKernelStore::open_with_engine(data_dir.path(), StorageEngine::Redb)
155 .expect_err("redb is legacy-only");
156 assert!(error.to_string().contains("legacy-only"), "{error}");
157 assert!(!format_version::format_version_path(data_dir.path()).exists());
158 }
159}