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)]
21pub struct EmbeddedKernelStore {
22 engine: Arc<dyn Engine>,
23}
24
25impl EmbeddedKernelStore {
26 pub fn open(data_dir: &Path) -> Result<Self, PortError> {
31 Self::open_as(data_dir, None)
32 }
33
34 pub fn open_with_engine(data_dir: &Path, engine: StorageEngine) -> Result<Self, PortError> {
38 Self::open_as(data_dir, Some(engine))
39 }
40
41 pub fn engine_of(data_dir: &Path) -> Result<StorageEngine, PortError> {
43 format_version::check_or_stamp_as(data_dir, None)
44 }
45
46 fn open_as(data_dir: &Path, wanted: Option<StorageEngine>) -> Result<Self, PortError> {
47 fs::create_dir_all(data_dir).map_err(|error| {
48 PortError::Unavailable(format!(
49 "embedded store could not create data dir `{}`: {error}",
50 data_dir.display()
51 ))
52 })?;
53 let engine = format_version::check_or_stamp_as(data_dir, wanted)?;
54
55 let store_file = format_version::store_file_path_for(data_dir, engine);
56 fs::create_dir_all(store_file.parent().expect("store file has a parent")).map_err(
57 |error| {
58 PortError::Unavailable(format!(
59 "embedded store could not create store dir under `{}`: {error}",
60 data_dir.display()
61 ))
62 },
63 )?;
64
65 Self::open_store_file(&store_file, engine)
66 }
67
68 pub(crate) fn open_store_file(
73 store_file: &Path,
74 engine: StorageEngine,
75 ) -> Result<Self, PortError> {
76 let engine: Arc<dyn Engine> = match engine {
77 StorageEngine::Redb => Arc::new(RedbEngine::open_file(store_file)?),
78 #[cfg(feature = "sqlite")]
79 StorageEngine::Sqlite => {
80 Arc::new(super::engine::sqlite::SqliteEngine::open_file(store_file)?)
81 }
82 #[cfg(not(feature = "sqlite"))]
83 StorageEngine::Sqlite => {
84 return Err(PortError::Unavailable(format!(
87 "embedded store `{}` needs the sqlite engine, which this binary was built \
88 without",
89 store_file.display()
90 )));
91 }
92 };
93 Ok(Self { engine })
94 }
95
96 pub(crate) fn begin_write(&self) -> Result<Box<dyn WriteTx + '_>, PortError> {
97 self.engine.begin_write()
98 }
99
100 pub(crate) fn begin_read(&self) -> Result<Box<dyn ReadTx + '_>, PortError> {
101 self.engine.begin_read()
102 }
103
104 pub(crate) async fn run<T, F>(&self, task: F) -> Result<T, PortError>
107 where
108 T: Send + 'static,
109 F: FnOnce(&EmbeddedKernelStore) -> Result<T, PortError> + Send + 'static,
110 {
111 let store = self.clone();
112 tokio::task::spawn_blocking(move || task(&store))
113 .await
114 .map_err(|error| {
115 PortError::Unavailable(format!("embedded store worker failed: {error}"))
116 })?
117 }
118
119 pub async fn event_log_stats(&self) -> Result<(u64, u64), PortError> {
122 self.run(|store| {
123 let tx = store.begin_read()?;
124 let count = tx.count(Table::EventLog)?;
125 let last_sequence = tx.last_u64(Table::EventLog)?.map_or(0, |(key, _)| key);
126 Ok((count, last_sequence))
127 })
128 .await
129 }
130
131 pub fn compact_data_dir(data_dir: &Path) -> Result<bool, PortError> {
136 let engine = format_version::check_or_stamp(data_dir)?;
137 let store_file = format_version::store_file_path_for(data_dir, engine);
138 match engine {
139 StorageEngine::Redb => RedbEngine::compact_file(&store_file),
140 #[cfg(feature = "sqlite")]
141 StorageEngine::Sqlite => super::engine::sqlite::SqliteEngine::compact_file(&store_file),
142 #[cfg(not(feature = "sqlite"))]
143 StorageEngine::Sqlite => unreachable!("the format gate refuses uncompiled engines"),
144 }
145 }
146}
147
148pub(crate) fn aggregate_key(root_node_id: &str, role: &str) -> String {
149 format!("{root_node_id}\u{1f}{role}")
150}