Skip to main content

stasis/infrastructure/runtime/
surreal_thread_store.rs

1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use surrealdb::{engine::any::Any, Surreal};
5use surrealdb_types::SurrealValue;
6
7use crate::domain::errors::{Result, StasisError};
8use crate::domain::runtime::thread::{
9    NewThread, NewThreadEvent, ThreadEvent, ThreadSnapshot,
10};
11use crate::ports::outbound::runtime::thread_store::ThreadStore;
12
13#[derive(Clone)]
14pub struct SurrealThreadStore {
15    db: Surreal<Any>,
16    thread_table: String,
17    event_table: String,
18}
19
20impl SurrealThreadStore {
21    pub fn new(db: Surreal<Any>) -> Self {
22        Self {
23            db,
24            thread_table: "thread".to_string(),
25            event_table: "thread_event".to_string(),
26        }
27    }
28
29    fn port_err(prefix: &str, err: impl std::fmt::Display) -> StasisError {
30        StasisError::PortFailure(format!("{prefix}: {err}"))
31    }
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize, SurrealValue)]
35struct ThreadRecordRow {
36    thread_id: String,
37    parent_thread_id: Option<String>,
38    branch_label: Option<String>,
39    created_at: DateTime<Utc>,
40    updated_at: DateTime<Utc>,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize, SurrealValue)]
44struct ThreadEventRow {
45    event_id: String,
46    thread_id: String,
47    event_kind: String,
48    payload_ref: String,
49    occurred_at: DateTime<Utc>,
50}
51
52impl From<ThreadRecordRow> for ThreadSnapshot {
53    fn from(row: ThreadRecordRow) -> Self {
54        Self {
55            thread_id: row.thread_id,
56            parent_thread_id: row.parent_thread_id,
57            branch_label: row.branch_label,
58            created_at: row.created_at,
59            updated_at: row.updated_at,
60        }
61    }
62}
63
64impl From<ThreadEventRow> for ThreadEvent {
65    fn from(row: ThreadEventRow) -> Self {
66        Self {
67            event_id: row.event_id,
68            thread_id: row.thread_id,
69            event_kind: row.event_kind,
70            payload_ref: row.payload_ref,
71            occurred_at: row.occurred_at,
72        }
73    }
74}
75
76#[async_trait]
77impl ThreadStore for SurrealThreadStore {
78    async fn create_thread(&self, thread: NewThread) -> Result<ThreadSnapshot> {
79        if let Some(parent_thread_id) = &thread.parent_thread_id {
80            let parent = self.get_thread(parent_thread_id).await?;
81            if parent.is_none() {
82                return Err(StasisError::PortFailure(format!(
83                    "parent thread not found: {}",
84                    parent_thread_id
85                )));
86            }
87        }
88
89        let row = ThreadRecordRow {
90            thread_id: thread.thread_id,
91            parent_thread_id: thread.parent_thread_id,
92            branch_label: thread.branch_label,
93            created_at: thread.created_at,
94            updated_at: thread.created_at,
95        };
96
97        self.db
98            .query("CREATE type::record($table, $id) CONTENT $data")
99            .bind(("table", self.thread_table.clone()))
100            .bind(("id", row.thread_id.clone()))
101            .bind(("data", row.clone()))
102            .await
103            .map_err(|e| Self::port_err("create thread", e))?;
104
105        Ok(row.into())
106    }
107
108    async fn get_thread(&self, thread_id: &str) -> Result<Option<ThreadSnapshot>> {
109        let mut response = self
110            .db
111            .query("SELECT * FROM type::record($table, $id)")
112            .bind(("table", self.thread_table.clone()))
113            .bind(("id", thread_id.to_string()))
114            .await
115            .map_err(|e| Self::port_err("get thread", e))?;
116
117        let row: Option<ThreadRecordRow> = response
118            .take(0)
119            .map_err(|e| Self::port_err("decode thread", e))?;
120
121        Ok(row.map(ThreadSnapshot::from))
122    }
123
124    async fn append_event(&self, event: NewThreadEvent) -> Result<ThreadEvent> {
125        let Some(mut thread) = self.get_thread(&event.thread_id).await? else {
126            return Err(StasisError::PortFailure(format!(
127                "thread not found: {}",
128                event.thread_id
129            )));
130        };
131
132        let event_row = ThreadEventRow {
133            event_id: event.event_id,
134            thread_id: event.thread_id,
135            event_kind: event.event_kind,
136            payload_ref: event.payload_ref,
137            occurred_at: event.occurred_at,
138        };
139
140        self.db
141            .query("CREATE type::record($table, $id) CONTENT $data")
142            .bind(("table", self.event_table.clone()))
143            .bind(("id", event_row.event_id.clone()))
144            .bind(("data", event_row.clone()))
145            .await
146            .map_err(|e| Self::port_err("append thread event", e))?;
147
148        thread.updated_at = event_row.occurred_at;
149        let thread_row = ThreadRecordRow {
150            thread_id: thread.thread_id.clone(),
151            parent_thread_id: thread.parent_thread_id.clone(),
152            branch_label: thread.branch_label.clone(),
153            created_at: thread.created_at,
154            updated_at: thread.updated_at,
155        };
156
157        self.db
158            .query("UPSERT type::record($table, $id) CONTENT $data")
159            .bind(("table", self.thread_table.clone()))
160            .bind(("id", thread_row.thread_id.clone()))
161            .bind(("data", thread_row))
162            .await
163            .map_err(|e| Self::port_err("update thread metadata", e))?;
164
165        Ok(event_row.into())
166    }
167
168    async fn list_events(&self, thread_id: &str) -> Result<Vec<ThreadEvent>> {
169        let mut response = self
170            .db
171            .query("SELECT * FROM type::table($table) WHERE thread_id = $thread_id ORDER BY occurred_at ASC")
172            .bind(("table", self.event_table.clone()))
173            .bind(("thread_id", thread_id.to_string()))
174            .await
175            .map_err(|e| Self::port_err("list thread events", e))?;
176
177        let rows: Vec<ThreadEventRow> = response
178            .take(0)
179            .map_err(|e| Self::port_err("decode thread events", e))?;
180
181        Ok(rows.into_iter().map(ThreadEvent::from).collect())
182    }
183
184    async fn fork_thread(
185        &self,
186        parent_thread_id: &str,
187        child_thread_id: &str,
188        branch_label: Option<String>,
189        created_at: DateTime<Utc>,
190    ) -> Result<ThreadSnapshot> {
191        self.create_thread(NewThread {
192            thread_id: child_thread_id.to_string(),
193            parent_thread_id: Some(parent_thread_id.to_string()),
194            branch_label,
195            created_at,
196        })
197        .await
198    }
199
200    async fn list_lineage(&self, thread_id: &str) -> Result<Vec<ThreadSnapshot>> {
201        let mut lineage = Vec::new();
202        let mut cursor = self.get_thread(thread_id).await?;
203
204        while let Some(node) = cursor {
205            cursor = if let Some(parent_thread_id) = &node.parent_thread_id {
206                self.get_thread(parent_thread_id).await?
207            } else {
208                None
209            };
210            lineage.push(node);
211        }
212
213        lineage.reverse();
214        Ok(lineage)
215    }
216}