oxinbox_backend/repository/
mod.rs1use std::collections::HashMap;
2use std::sync::{Arc, LazyLock};
3
4use oxinbox_core::Task;
5use oxinbox_core::Uuid;
6use tokio::sync::RwLock;
7use tracing::instrument;
8
9use crate::search::{SearchIndex, SearchResult, hybrid_search};
10
11pub type TaskStore = Arc<RwLock<HashMap<Uuid, Task>>>;
12
13static SEARCH_INDEX: LazyLock<RwLock<SearchIndex>> =
14 LazyLock::new(|| RwLock::new(SearchIndex::default()));
15
16pub async fn index_task(task: &Task) {
17 SEARCH_INDEX.write().await.index_task(task);
18}
19
20pub async fn remove_from_index(id: Uuid) {
21 SEARCH_INDEX.write().await.remove_task(id);
22}
23
24pub async fn store_embedding(task_id: Uuid, embedding: Vec<f32>) {
25 SEARCH_INDEX
26 .write()
27 .await
28 .store_embedding(task_id, embedding);
29}
30
31pub async fn hybrid_search_tasks(
32 tasks: &[Task],
33 query: &str,
34 query_embedding: Option<&[f32]>,
35 limit: usize,
36) -> Vec<SearchResult> {
37 let idx = SEARCH_INDEX.read().await;
38 hybrid_search(tasks, &idx, query, query_embedding, limit, 0.5)
39}
40
41pub struct InMemoryTaskRepository {
42 tasks: TaskStore,
43}
44
45impl InMemoryTaskRepository {
46 pub fn new(_owner_id: i32) -> Self {
47 Self {
48 tasks: Arc::new(RwLock::new(HashMap::new())),
49 }
50 }
51
52 pub fn shared() -> &'static Self {
53 static REPO: LazyLock<InMemoryTaskRepository> =
54 LazyLock::new(|| InMemoryTaskRepository::new(0));
55 &REPO
56 }
57
58 pub async fn clear(&self) {
59 self.tasks.write().await.clear();
60 }
61}
62
63#[allow(async_fn_in_trait)]
64pub trait TaskRepository: Send + Sync {
65 async fn create(&self, task: &Task) -> Result<Task, RepositoryError>;
66 async fn get(&self, id: Uuid) -> Result<Task, RepositoryError>;
67 async fn list(&self, user_id: i32) -> Result<Vec<Task>, RepositoryError>;
68 async fn update(&self, task: &Task) -> Result<Task, RepositoryError>;
69 async fn delete(&self, id: Uuid) -> Result<(), RepositoryError>;
70}
71
72#[derive(thiserror::Error, Debug)]
73pub enum RepositoryError {
74 #[error("database error: {0}")]
75 Database(#[from] sqlx::Error),
76
77 #[error("task not found: {0}")]
78 NotFound(Uuid),
79}
80
81impl TaskRepository for InMemoryTaskRepository {
82 #[instrument(skip(self), fields(task_id = %task.id))]
83 async fn create(&self, task: &Task) -> Result<Task, RepositoryError> {
84 tracing::debug!("storing task");
85 self.tasks.write().await.insert(task.id, task.clone());
86 index_task(task).await;
87 Ok(task.clone())
88 }
89
90 #[instrument(skip(self), fields(task_id = %id))]
91 async fn get(&self, id: Uuid) -> Result<Task, RepositoryError> {
92 let result = self
93 .tasks
94 .read()
95 .await
96 .get(&id)
97 .cloned()
98 .ok_or(RepositoryError::NotFound(id));
99 if result.is_ok() {
100 tracing::debug!("task found");
101 }
102 result
103 }
104
105 #[instrument(skip(self))]
106 async fn list(&self, _: i32) -> Result<Vec<Task>, RepositoryError> {
107 let tasks: Vec<Task> = self.tasks.read().await.values().cloned().collect();
108 tracing::debug!(count = tasks.len(), "listing tasks");
109 Ok(tasks)
110 }
111
112 #[instrument(skip(self), fields(task_id = %task.id))]
113 async fn update(&self, task: &Task) -> Result<Task, RepositoryError> {
114 let mut tasks = self.tasks.write().await;
115 if !tasks.contains_key(&task.id) {
116 tracing::warn!("task not found for update");
117 return Err(RepositoryError::NotFound(task.id));
118 }
119 let result = task.clone();
120 tasks.insert(task.id, result.clone());
121 drop(tasks);
122 index_task(task).await;
123 tracing::debug!("task updated");
124 Ok(result)
125 }
126
127 #[instrument(skip(self), fields(task_id = %id))]
128 async fn delete(&self, id: Uuid) -> Result<(), RepositoryError> {
129 let mut tasks = self.tasks.write().await;
130 let existed = tasks.remove(&id).is_some();
131 drop(tasks);
132 if !existed {
133 tracing::warn!("task not found for deletion");
134 return Err(RepositoryError::NotFound(id));
135 }
136 remove_from_index(id).await;
137 tracing::debug!("task deleted");
138 Ok(())
139 }
140}