1use std::sync::Arc;
10
11use tokio_util::sync::CancellationToken;
12
13use crate::effect::{Effect, TraceEvent};
14use crate::error::ActorError;
15use crate::exit::Exit;
16use crate::fact::{cut_log, Fact, LogCut, NewFact};
17use crate::log::LogStore;
18
19pub struct Transition<S> {
20 pub key: String,
21 pub run: TransitionProgram<S>,
22}
23
24pub struct PreparedTransition<S> {
27 pub facts: Vec<NewFact>,
28 pub run: Effect<Vec<NewFact>, ActorError, S>,
29}
30
31pub enum TransitionProgram<S> {
32 Immediate(Effect<Vec<NewFact>, ActorError, S>),
33 Prepared(Effect<PreparedTransition<S>, ActorError, S>),
34}
35
36impl<S> From<Effect<Vec<NewFact>, ActorError, S>> for TransitionProgram<S> {
37 fn from(run: Effect<Vec<NewFact>, ActorError, S>) -> Self {
38 Self::Immediate(run)
39 }
40}
41
42impl<S: Send + Sync + 'static> TransitionProgram<S> {
43 pub fn map_services(self, project: impl Fn(Arc<S>) -> Arc<S> + Send + Sync + 'static) -> Self {
46 let project = Arc::new(project);
47 match self {
48 Self::Immediate(run) => {
49 Self::Immediate(run.map_services(move |services| project(services)))
50 }
51 Self::Prepared(prepare) => {
52 let continuation_project = Arc::clone(&project);
53 Self::Prepared(
54 prepare
55 .map(move |prepared| {
56 let project = Arc::clone(&continuation_project);
57 PreparedTransition {
58 facts: prepared.facts,
59 run: prepared.run.map_services(move |services| project(services)),
60 }
61 })
62 .map_services(move |services| project(services)),
63 )
64 }
65 }
66 }
67}
68
69type Projection<S, V> =
70 dyn Fn(&[Fact]) -> Result<(V, Vec<Transition<S>>), ActorError> + Send + Sync;
71
72pub struct ErasedComponent<S, V> {
73 pub(crate) project: Arc<Projection<S, V>>,
74}
75
76pub fn component<S, V, St, I, Step, Out>(
77 initial: I,
78 step: Step,
79 output: Out,
80) -> ErasedComponent<S, V>
81where
82 S: Send + Sync + 'static,
83 V: 'static,
84 St: Send + 'static,
85 I: Fn() -> St + Send + Sync + 'static,
86 Step: Fn(St, &Fact) -> St + Send + Sync + 'static,
87 Out: Fn(&St) -> (V, Vec<Transition<S>>) + Send + Sync + 'static,
88{
89 ErasedComponent {
90 project: Arc::new(move |facts| {
91 let mut state = initial();
92 for fact in facts {
93 state = step(state, fact);
94 }
95 Ok(output(&state))
96 }),
97 }
98}
99
100pub struct Actor<S, V> {
101 pub name: &'static str,
102 components: Vec<ErasedComponent<S, V>>,
103 merge: Arc<dyn Fn(Vec<V>) -> V + Send + Sync>,
104}
105
106impl<S, V> Actor<S, V>
107where
108 S: Send + Sync + 'static,
109 V: 'static,
110{
111 pub fn new(
112 name: &'static str,
113 components: Vec<ErasedComponent<S, V>>,
114 merge: impl Fn(Vec<V>) -> V + Send + Sync + 'static,
115 ) -> Self {
116 Self {
117 name,
118 components,
119 merge: Arc::new(merge),
120 }
121 }
122
123 fn project(&self, facts: &[Fact]) -> Result<(V, Vec<Transition<S>>), ActorError> {
124 let mut views = Vec::with_capacity(self.components.len());
125 let mut transitions = Vec::new();
126 for component in &self.components {
127 let (view, enabled) = (component.project)(facts)?;
128 views.push(view);
129 transitions.extend(enabled);
130 }
131 let mut seen = std::collections::BTreeSet::new();
132 for transition in &transitions {
133 if !seen.insert(transition.key.clone()) {
134 return Err(ActorError::DuplicateTransition {
135 key: transition.key.clone(),
136 });
137 }
138 }
139 let causes: std::collections::BTreeSet<&str> = facts
140 .iter()
141 .filter_map(|fact| fact.cause.as_deref())
142 .collect();
143 transitions.retain(|transition| !causes.contains(transition.key.as_str()));
144 transitions.sort_by(|left, right| left.key.cmp(&right.key));
145 Ok(((self.merge)(views), transitions))
146 }
147}
148
149pub struct Settlement<V> {
150 pub view: V,
151 pub log: Vec<Fact>,
152 pub steps: u32,
153 pub cut: LogCut,
154 pub trace: Vec<TraceEvent>,
155}
156
157pub async fn resume<S, V>(
158 actor: &Actor<S, V>,
159 log: &dyn LogStore,
160 services: Arc<S>,
161 thread_id: &str,
162 limit: u32,
163) -> Result<Settlement<V>, Exit<ActorError>>
164where
165 S: Send + Sync + 'static,
166 V: 'static,
167{
168 resume_with_cancel(
169 actor,
170 log,
171 services,
172 thread_id,
173 limit,
174 &CancellationToken::new(),
175 )
176 .await
177}
178
179pub async fn resume_with_cancel<S, V>(
180 actor: &Actor<S, V>,
181 log: &dyn LogStore,
182 services: Arc<S>,
183 thread_id: &str,
184 limit: u32,
185 cancel: &CancellationToken,
186) -> Result<Settlement<V>, Exit<ActorError>>
187where
188 S: Send + Sync + 'static,
189 V: 'static,
190{
191 let mut steps = 0;
192 let mut trace_events = Vec::new();
193 loop {
194 let facts = match log.read(thread_id) {
195 Ok(facts) => facts,
196 Err(error) => return Err(Exit::Fail(error)),
197 };
198 let (view, transitions) = match actor.project(&facts) {
199 Ok(projected) => projected,
200 Err(error) => return Err(Exit::Fail(error)),
201 };
202 let Some(next) = transitions.into_iter().next() else {
203 return Ok(Settlement {
204 cut: cut_log(&facts),
205 view,
206 log: facts,
207 steps,
208 trace: trace_events,
209 });
210 };
211 if steps >= limit {
212 return Err(Exit::Fail(ActorError::StepLimit { limit }));
213 }
214 let run = match next.run {
218 TransitionProgram::Immediate(run) => run,
219 TransitionProgram::Prepared(prepare) => {
220 let (prepared, span) = prepare
221 .with_span(format!("preparation:{}", next.key))
222 .run_with_cancel(Arc::clone(&services), cancel.clone())
223 .await;
224 trace_events.extend(span);
225 let prepared = prepared?;
226 log.append(thread_id, &prepared.facts, None)
230 .map_err(Exit::Fail)?;
231 prepared.run
232 }
233 };
234 let (result, span) = run
235 .with_span(format!("transition:{}", next.key))
236 .run_with_cancel(Arc::clone(&services), cancel.clone())
237 .await;
238 trace_events.extend(span);
239 let produced = match result {
240 Ok(produced) => produced,
241 Err(error) => return Err(error),
242 };
243 if let Err(error) = log.append(thread_id, &produced, Some(next.key.as_str())) {
244 return Err(Exit::Fail(error));
245 }
246 steps += 1;
247 }
254}
255
256pub async fn ingest<S, V>(
257 actor: &Actor<S, V>,
258 log: &dyn LogStore,
259 services: Arc<S>,
260 thread_id: &str,
261 fact: NewFact,
262 limit: u32,
263) -> Result<Settlement<V>, Exit<ActorError>>
264where
265 S: Send + Sync + 'static,
266 V: 'static,
267{
268 ingest_with_cancel(
269 actor,
270 log,
271 services,
272 thread_id,
273 fact,
274 limit,
275 &CancellationToken::new(),
276 )
277 .await
278}
279
280pub fn append_input(log: &dyn LogStore, thread_id: &str, fact: NewFact) -> Result<(), ActorError> {
286 log.append(thread_id, &[fact], None).map(|_| ())
287}
288
289pub async fn ingest_with_cancel<S, V>(
290 actor: &Actor<S, V>,
291 log: &dyn LogStore,
292 services: Arc<S>,
293 thread_id: &str,
294 fact: NewFact,
295 limit: u32,
296 cancel: &CancellationToken,
297) -> Result<Settlement<V>, Exit<ActorError>>
298where
299 S: Send + Sync + 'static,
300 V: 'static,
301{
302 append_input(log, thread_id, fact).map_err(Exit::Fail)?;
303 resume_with_cancel(actor, log, services, thread_id, limit, cancel).await
304}
305
306#[cfg(test)]
307#[path = "actor/tests.rs"]
308mod tests;