stasis/infrastructure/runtime/
surreal_thread_store.rs1use 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}