1use std::sync::{Arc, Mutex};
45
46use tokio::sync::Semaphore;
47
48use dejadb_core::error::{DejaDbError, Hash, Result};
49use dejadb_core::types::{Grain, GrainType};
50
51use crate::{DejaDB, DejaDbOptions, DeserializedGrain, StoreStats};
52
53struct Shared {
54 db: Mutex<Option<DejaDB>>,
55 gate: Semaphore,
60}
61
62#[derive(Clone)]
72pub struct AsyncDejaDB {
73 inner: Arc<Shared>,
74}
75
76impl AsyncDejaDB {
77 pub async fn open(path: &str) -> Result<Self> {
79 let p = path.to_owned();
80 Self::opened(move || DejaDB::open(&p)).await
81 }
82
83 pub async fn open_with(path: &str, opts: DejaDbOptions) -> Result<Self> {
85 let p = path.to_owned();
86 Self::opened(move || DejaDB::open_with(&p, opts)).await
87 }
88
89 pub async fn open_encrypted(path: &str, key: [u8; 32]) -> Result<Self> {
95 let p = path.to_owned();
96 let key = zeroize::Zeroizing::new(key);
97 Self::opened(move || DejaDB::open_encrypted(&p, *key)).await
98 }
99
100 async fn opened<F>(open: F) -> Result<Self>
101 where
102 F: FnOnce() -> Result<DejaDB> + Send + 'static,
103 {
104 let db = offload(open).await?;
105 Ok(Self {
106 inner: Arc::new(Shared {
107 db: Mutex::new(Some(db)),
108 gate: Semaphore::new(1),
109 }),
110 })
111 }
112
113 pub async fn with<T, F>(&self, op: F) -> Result<T>
126 where
127 F: FnOnce(&mut DejaDB) -> Result<T> + Send + 'static,
128 T: Send + 'static,
129 {
130 let _permit = self.inner.gate.acquire().await.map_err(|_| closed())?;
131 let inner = Arc::clone(&self.inner);
132 offload(move || {
133 let mut guard = inner.db.lock().map_err(|_| poisoned())?;
134 let db = guard.as_mut().ok_or_else(closed)?;
135 op(db)
136 })
137 .await
138 }
139
140 pub async fn add<G>(&self, grain: G) -> Result<Hash>
142 where
143 G: Grain + Send + 'static,
144 {
145 self.with(move |db| db.add(&grain)).await
146 }
147
148 pub async fn add_if_novel<G>(&self, grain: G) -> Result<(Hash, bool)>
150 where
151 G: Grain + Send + 'static,
152 {
153 self.with(move |db| db.add_if_novel(&grain)).await
154 }
155
156 pub async fn latest(
158 &self,
159 ns: &str,
160 subject: &str,
161 relation: &str,
162 ) -> Result<Option<DeserializedGrain>> {
163 let (ns, subject, relation) = (ns.to_owned(), subject.to_owned(), relation.to_owned());
164 self.with(move |db| db.latest(&ns, &subject, &relation))
165 .await
166 }
167
168 pub async fn recall_hybrid(
170 &self,
171 ns: &str,
172 subject: Option<&str>,
173 relation: Option<&str>,
174 query: Option<&str>,
175 k: usize,
176 deadline: Option<std::time::Duration>,
177 ) -> Result<Vec<DeserializedGrain>> {
178 let ns = ns.to_owned();
179 let subject = subject.map(str::to_owned);
180 let relation = relation.map(str::to_owned);
181 let query = query.map(str::to_owned);
182 self.with(move |db| {
183 db.recall_hybrid(
184 &ns,
185 subject.as_deref(),
186 relation.as_deref(),
187 query.as_deref(),
188 k,
189 deadline,
190 )
191 })
192 .await
193 }
194
195 pub async fn recent(
197 &self,
198 ns: &str,
199 gtype: Option<GrainType>,
200 limit: usize,
201 ) -> Result<Vec<DeserializedGrain>> {
202 let ns = ns.to_owned();
203 self.with(move |db| db.recent(&ns, gtype, limit)).await
204 }
205
206 pub async fn forget(&self, hash: Hash) -> Result<()> {
208 self.with(move |db| db.forget(&hash)).await
209 }
210
211 pub async fn stats(&self) -> Result<StoreStats> {
213 self.with(|db| db.stats()).await
214 }
215
216 pub async fn close(self) -> Result<()> {
228 let _permit = self.inner.gate.acquire().await.map_err(|_| closed())?;
229 let taken = {
230 let mut guard = self.inner.db.lock().map_err(|_| poisoned())?;
231 guard.take()
232 };
233 match taken {
234 Some(db) => {
235 offload(move || {
236 drop(db);
237 Ok(())
238 })
239 .await
240 }
241 None => Ok(()),
242 }
243 }
244}
245
246impl Drop for AsyncDejaDB {
247 fn drop(&mut self) {
248 if let Some(db) = Arc::get_mut(&mut self.inner)
252 .and_then(|s| s.db.get_mut().ok())
253 .and_then(Option::take)
254 {
255 std::thread::spawn(move || drop(db));
256 }
257 }
258}
259
260async fn offload<T, F>(op: F) -> Result<T>
262where
263 F: FnOnce() -> Result<T> + Send + 'static,
264 T: Send + 'static,
265{
266 match tokio::task::spawn_blocking(op).await {
267 Ok(r) => r,
268 Err(e) => Err(DejaDbError::Storage(format!(
269 "dejadb blocking task failed: {e}"
270 ))),
271 }
272}
273
274fn poisoned() -> DejaDbError {
275 DejaDbError::Storage("dejadb handle poisoned by a panic in another task".into())
276}
277
278fn closed() -> DejaDbError {
279 DejaDbError::Storage("dejadb handle already closed".into())
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use dejadb_core::types::Fact;
286
287 fn tmp(name: &str) -> String {
288 let dir = tempfile::tempdir().expect("tempdir").keep();
289 dir.join(name).to_string_lossy().into_owned()
290 }
291
292 #[tokio::test]
294 async fn is_usable_from_async_code() {
295 let path = tmp("async-usable.db");
296 let db = AsyncDejaDB::open(&path).await.expect("open");
297
298 let mut fact = Fact::new("john", "prefers", "dark mode");
299 fact.common_mut().namespace = Some("caller".to_string());
300 db.add(fact).await.expect("add");
301
302 let got = db
303 .latest("caller", "john", "prefers")
304 .await
305 .expect("latest")
306 .expect("a grain");
307 assert_eq!(
308 got.fields.get("object").and_then(|v| v.as_str()),
309 Some("dark mode"),
310 );
311
312 drop(db);
313 }
314
315 #[tokio::test]
316 async fn escape_hatch_runs_arbitrary_ops_safely() {
317 let path = tmp("async-with.db");
318 let db = AsyncDejaDB::open(&path).await.expect("open");
319
320 let stats = db.with(|db| db.stats()).await.expect("stats via with");
321 assert_eq!(stats.grains, 0);
322 }
323
324 #[tokio::test]
325 async fn close_awaits_teardown() {
326 let path = tmp("async-close.db");
327 let db = AsyncDejaDB::open(&path).await.expect("open");
328
329 let mut fact = Fact::new("grace", "built", "the compiler");
330 fact.common_mut().namespace = Some("caller".to_string());
331 db.add(fact).await.expect("add");
332
333 db.close().await.expect("close");
334
335 let again = AsyncDejaDB::open(&path).await.expect("reopen");
336 assert!(again
337 .latest("caller", "grace", "built")
338 .await
339 .expect("latest")
340 .is_some());
341 again.close().await.expect("close again");
342 }
343
344 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
348 async fn concurrent_operations_queue_without_hogging_the_blocking_pool() {
349 let path = tmp("async-concurrent.db");
350 let db = AsyncDejaDB::open(&path).await.expect("open");
351
352 let writes = (0..64).map(|i| {
353 let db = db.clone();
354 tokio::spawn(async move {
355 let mut fact = Fact::new(&format!("s{i}"), "n", &i.to_string());
356 fact.common_mut().namespace = Some("caller".to_string());
357 db.add(fact).await
358 })
359 });
360 for w in writes {
361 w.await.expect("task").expect("add");
362 }
363
364 let stats = db.stats().await.expect("stats");
365 assert_eq!(stats.grains, 64);
366 db.close().await.expect("close");
367 }
368
369 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
370 async fn clones_share_one_store() {
371 let path = tmp("async-clone.db");
372 let db = AsyncDejaDB::open(&path).await.expect("open");
373 let handle = db.clone();
374
375 let mut fact = Fact::new("linus", "wrote", "git");
376 fact.common_mut().namespace = Some("caller".to_string());
377 handle.add(fact).await.expect("add via clone");
378
379 drop(handle);
380
381 assert!(db
382 .latest("caller", "linus", "wrote")
383 .await
384 .expect("latest via original")
385 .is_some());
386 }
387
388 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
389 async fn works_on_a_multi_thread_runtime() {
390 let path = tmp("async-multi.db");
391 let db = AsyncDejaDB::open(&path).await.expect("open");
392
393 let mut fact = Fact::new("ada", "wrote", "the first program");
394 fact.common_mut().namespace = Some("caller".to_string());
395 db.add(fact).await.expect("add");
396
397 assert!(db
398 .latest("caller", "ada", "wrote")
399 .await
400 .expect("latest")
401 .is_some());
402 }
403}