1use chrono::Utc;
2
3use crate::application::runtime::runtime_factory::{RuntimeBackend, SurrealAuth};
4use crate::application::runtime::stasis_runtime_builder::StasisRuntimeBuilder;
5use crate::application::runtime::runtime_factory::RuntimeComposition;
6use crate::domain::errors::Result;
7use crate::domain::runtime::job::{JobState, NewJob};
8use crate::domain::runtime::recurring::RecurringDefinition;
9use crate::ports::outbound::runtime::job_store::JobStore;
10use crate::ports::outbound::runtime::outbox_store::OutboxStore;
11use crate::ports::outbound::runtime::recurring_store::RecurringStore;
12
13#[derive(Clone, Debug, Default)]
15pub struct RuntimeStatsSnapshot {
16 pub enqueued_jobs: usize,
17 pub running_jobs: usize,
18 pub succeeded_jobs: usize,
19 pub failed_jobs: usize,
20 pub dead_letter_jobs: usize,
21 pub pending_outbox_events: usize,
22 pub recurring_definitions: usize,
23}
24
25#[derive(Clone)]
27pub struct RuntimeSdk {
28 runtime: RuntimeComposition,
29}
30
31pub type StasisRuntime = RuntimeSdk;
33
34impl RuntimeSdk {
35 pub fn new(runtime: RuntimeComposition) -> Self {
37 Self { runtime }
38 }
39
40 pub async fn in_memory() -> Result<Self> {
42 Self::from_builder(StasisRuntimeBuilder::new(RuntimeBackend::InMemory)).await
43 }
44
45 pub async fn surreal_mem(
47 namespace: impl Into<String>,
48 database: impl Into<String>,
49 ) -> Result<Self> {
50 Self::from_builder(StasisRuntimeBuilder::new(RuntimeBackend::surreal_mem(
51 namespace,
52 database,
53 )))
54 .await
55 }
56
57 pub async fn surreal_ws(
59 endpoint: impl Into<String>,
60 namespace: impl Into<String>,
61 database: impl Into<String>,
62 ) -> Result<Self> {
63 Self::surreal_ws_with_auth(endpoint, namespace, database, None).await
64 }
65
66 pub async fn surreal_ws_with_auth(
68 endpoint: impl Into<String>,
69 namespace: impl Into<String>,
70 database: impl Into<String>,
71 auth: Option<SurrealAuth>,
72 ) -> Result<Self> {
73 let mut backend = RuntimeBackend::surreal_ws(endpoint, namespace, database);
74 if let Some(auth) = auth {
75 backend = backend.with_surreal_auth(auth);
76 }
77 Self::from_builder(StasisRuntimeBuilder::new(backend)).await
78 }
79
80 pub async fn surreal_kv(
82 path: impl Into<String>,
83 namespace: impl Into<String>,
84 database: impl Into<String>,
85 ) -> Result<Self> {
86 Self::surreal_kv_with_auth(path, namespace, database, None).await
87 }
88
89 pub async fn surreal_kv_with_auth(
91 path: impl Into<String>,
92 namespace: impl Into<String>,
93 database: impl Into<String>,
94 auth: Option<SurrealAuth>,
95 ) -> Result<Self> {
96 let mut backend = RuntimeBackend::surreal_kv(path, namespace, database);
97 if let Some(auth) = auth {
98 backend = backend.with_surreal_auth(auth);
99 }
100 Self::from_builder(StasisRuntimeBuilder::new(backend)).await
101 }
102
103 pub async fn surreal_mem_with_auth(
105 namespace: impl Into<String>,
106 database: impl Into<String>,
107 auth: Option<SurrealAuth>,
108 ) -> Result<Self> {
109 let mut backend = RuntimeBackend::surreal_mem(namespace, database);
110 if let Some(auth) = auth {
111 backend = backend.with_surreal_auth(auth);
112 }
113 Self::from_builder(StasisRuntimeBuilder::new(backend)).await
114 }
115
116 pub async fn from_builder(builder: StasisRuntimeBuilder) -> Result<Self> {
118 let runtime = builder.build().await?;
119 Ok(Self::new(runtime))
120 }
121
122 pub fn runtime(&self) -> &RuntimeComposition {
124 &self.runtime
125 }
126
127 pub fn into_runtime(self) -> RuntimeComposition {
129 self.runtime
130 }
131
132 pub async fn enqueue(&self, job: NewJob) -> Result<()> {
134 match &self.runtime {
135 RuntimeComposition::InMemory(rt) => rt.enqueue(job).await,
136 RuntimeComposition::Surreal(rt) => rt.enqueue(job).await,
137 }
138 }
139
140 pub async fn register_recurring(&self, definition: RecurringDefinition) -> Result<()> {
142 match &self.runtime {
143 RuntimeComposition::InMemory(rt) => rt.register_recurring(definition).await,
144 RuntimeComposition::Surreal(rt) => rt.register_recurring(definition).await,
145 }
146 }
147
148 pub async fn process_once(&self, queue: &str, worker_id: &str) -> Result<Option<String>> {
150 let now = Utc::now();
151 match &self.runtime {
152 RuntimeComposition::InMemory(rt) => rt.process_once(queue, worker_id, now).await,
153 RuntimeComposition::Surreal(rt) => rt.process_once(queue, worker_id, now).await,
154 }
155 }
156
157 pub async fn publish_pending_events(&self, limit: usize) -> Result<usize> {
159 let now = Utc::now();
160 match &self.runtime {
161 RuntimeComposition::InMemory(rt) => rt.publish_pending_events(limit, now).await,
162 RuntimeComposition::Surreal(rt) => rt.publish_pending_events(limit, now).await,
163 }
164 }
165
166 pub async fn materialize_recurring_now(&self, scheduler_id: &str) -> Result<usize> {
168 match &self.runtime {
169 RuntimeComposition::InMemory(rt) => rt.materialize_recurring_now(scheduler_id).await,
170 RuntimeComposition::Surreal(rt) => rt.materialize_recurring_now(scheduler_id).await,
171 }
172 }
173
174 pub async fn stats_snapshot(&self, pending_limit: usize) -> Result<RuntimeStatsSnapshot> {
176 Ok(RuntimeStatsSnapshot {
177 enqueued_jobs: self.job_count_by_state(JobState::Enqueued).await?,
178 running_jobs: self.job_count_by_state(JobState::Running).await?,
179 succeeded_jobs: self.job_count_by_state(JobState::Succeeded).await?,
180 failed_jobs: self.job_count_by_state(JobState::Failed).await?,
181 dead_letter_jobs: self.job_count_by_state(JobState::DeadLetter).await?,
182 pending_outbox_events: self.pending_outbox_count(pending_limit).await?,
183 recurring_definitions: self.recurring_count().await?,
184 })
185 }
186
187 pub async fn job_count_by_state(&self, state: JobState) -> Result<usize> {
189 let jobs = match &self.runtime {
190 RuntimeComposition::InMemory(rt) => rt.job_store.list_by_state(state).await?,
191 RuntimeComposition::Surreal(rt) => rt.job_store.list_by_state(state).await?,
192 };
193 Ok(jobs.len())
194 }
195
196 pub async fn pending_outbox_count(&self, limit: usize) -> Result<usize> {
198 let pending = match &self.runtime {
199 RuntimeComposition::InMemory(rt) => rt.outbox_store.list_pending(limit).await?,
200 RuntimeComposition::Surreal(rt) => rt.outbox_store.list_pending(limit).await?,
201 };
202 Ok(pending.len())
203 }
204
205 pub async fn recurring_count(&self) -> Result<usize> {
207 let definitions = match &self.runtime {
208 RuntimeComposition::InMemory(rt) => rt.recurring_store.list().await?,
209 RuntimeComposition::Surreal(rt) => rt.recurring_store.list().await?,
210 };
211 Ok(definitions.len())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use std::env;
218 use std::fs;
219 use std::time::{SystemTime, UNIX_EPOCH};
220
221 use crate::application::runtime::runtime_factory::RuntimeComposition;
222
223 use super::RuntimeSdk;
224
225 #[tokio::test]
226 async fn runtime_sdk_in_memory_constructor_builds() {
227 let runtime = RuntimeSdk::in_memory()
228 .await
229 .expect("in-memory runtime should build");
230 let stats = runtime
231 .stats_snapshot(10)
232 .await
233 .expect("stats snapshot should succeed");
234 assert_eq!(stats.enqueued_jobs, 0);
235 }
236
237 #[tokio::test]
238 async fn runtime_sdk_surreal_mem_constructor_builds() {
239 let runtime = RuntimeSdk::surreal_mem("stasis", "runtime")
240 .await
241 .expect("surreal-mem runtime should build");
242 assert!(matches!(runtime.runtime(), RuntimeComposition::Surreal(_)));
243 }
244
245 #[tokio::test]
246 async fn runtime_sdk_surreal_ws_constructor_rejects_invalid_endpoint() {
247 let result = RuntimeSdk::surreal_ws("not-a-valid-endpoint", "stasis", "runtime").await;
248 assert!(result.is_err(), "invalid websocket endpoint should fail");
249 let err = result.err().expect("result should contain an error");
250 assert!(err.to_string().contains("connect surreal db"));
251 }
252
253 #[tokio::test]
254 async fn runtime_sdk_surreal_kv_constructor_builds() {
255 let nanos = SystemTime::now()
256 .duration_since(UNIX_EPOCH)
257 .expect("system clock should be after epoch")
258 .as_nanos();
259 let path = env::temp_dir().join(format!("stasis-surrealkv-{nanos}"));
260 let path_str = path.to_string_lossy().into_owned();
261
262 let runtime = RuntimeSdk::surreal_kv(path_str, "stasis", "runtime")
263 .await
264 .expect("surreal-kv runtime should build");
265 assert!(matches!(runtime.runtime(), RuntimeComposition::Surreal(_)));
266
267 drop(runtime);
268 let _ = fs::remove_dir_all(path);
269 }
270}