Skip to main content

oxinbox_backend/repository/
mod.rs

1use 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}