telltale_vm/effect/
runtime_types.rs1#[derive(Debug, Default)]
3pub struct EffectTraceTape {
4 next_effect_id: AtomicU64,
5 entries: Mutex<Vec<EffectTraceEntry>>,
6}
7
8fn encode_effect_result<T>(result: &EffectResult<T>) -> JsonValue
9where
10 T: Serialize,
11{
12 match result {
13 EffectResult::Success(value) => json!({
14 "status": "success",
15 "value": value,
16 }),
17 EffectResult::Blocked => json!({
18 "status": "blocked",
19 }),
20 EffectResult::Failure(failure) => json!({
21 "status": "failure",
22 "failure": failure,
23 }),
24 }
25}
26
27fn decode_effect_result<T>(outputs: &JsonValue) -> Option<EffectResult<T>>
28where
29 T: DeserializeOwned,
30{
31 match outputs.get("status").and_then(JsonValue::as_str)? {
32 "success" => {
33 let value = serde_json::from_value(outputs.get("value")?.clone()).ok()?;
34 Some(EffectResult::Success(value))
35 }
36 "blocked" => Some(EffectResult::Blocked),
37 "failure" => {
38 let failure = serde_json::from_value(outputs.get("failure")?.clone()).ok()?;
39 Some(EffectResult::Failure(failure))
40 }
41 _ => None,
42 }
43}
44
45impl EffectTraceTape {
46 #[must_use]
48 pub fn new() -> Self {
49 Self::default()
50 }
51
52 #[must_use]
54 pub fn from_entries(entries: Vec<EffectTraceEntry>) -> Self {
55 let next_effect_id = entries
56 .last()
57 .map_or(0, |entry| entry.effect_id.saturating_add(1));
58 Self {
59 next_effect_id: AtomicU64::new(next_effect_id),
60 entries: Mutex::new(entries),
61 }
62 }
63
64 pub fn record(
70 &self,
71 effect_kind: &str,
72 inputs: JsonValue,
73 outputs: JsonValue,
74 handler_identity: &str,
75 topology: Option<TopologyPerturbation>,
76 ) {
77 let effect_id = self.next_effect_id.fetch_add(1, Ordering::Relaxed);
78 let (effect_interface, effect_operation) =
79 infer_effect_interface_and_operation(effect_kind);
80 let entry = EffectTraceEntry {
81 effect_id,
82 effect_kind: effect_kind.to_string(),
83 inputs,
84 outputs,
85 handler_identity: handler_identity.to_string(),
86 effect_interface,
87 effect_operation,
88 ordering_key: effect_id,
89 topology,
90 };
91 self.entries
92 .lock()
93 .unwrap_or_else(|poisoned| poisoned.into_inner())
94 .push(entry);
95 }
96
97 #[must_use]
103 pub fn entries(&self) -> Vec<EffectTraceEntry> {
104 self.entries
105 .lock()
106 .unwrap_or_else(|poisoned| poisoned.into_inner())
107 .clone()
108 }
109}
110
111pub struct RecordingEffectHandler<'a> {
113 inner: &'a dyn EffectHandler,
114 tape: EffectTraceTape,
115}
116
117impl<'a> RecordingEffectHandler<'a> {
118 #[must_use]
120 pub fn new(inner: &'a dyn EffectHandler) -> Self {
121 Self {
122 inner,
123 tape: EffectTraceTape::new(),
124 }
125 }
126
127 #[must_use]
129 pub fn effect_trace(&self) -> Vec<EffectTraceEntry> {
130 self.tape.entries()
131 }
132}
133
134pub struct ReplayEffectHandler<'a> {
136 entries: Arc<[EffectTraceEntry]>,
137 cursor: Mutex<usize>,
138 fallback: Option<&'a dyn EffectHandler>,
139}
140
141impl<'a> ReplayEffectHandler<'a> {
142 #[must_use]
144 pub fn new<E>(entries: E) -> Self
145 where
146 E: Into<Arc<[EffectTraceEntry]>>,
147 {
148 Self {
149 entries: entries.into(),
150 cursor: Mutex::new(0),
151 fallback: None,
152 }
153 }
154
155 #[must_use]
157 pub fn with_fallback<E>(entries: E, fallback: &'a dyn EffectHandler) -> Self
158 where
159 E: Into<Arc<[EffectTraceEntry]>>,
160 {
161 Self {
162 entries: entries.into(),
163 cursor: Mutex::new(0),
164 fallback: Some(fallback),
165 }
166 }
167
168 #[must_use]
174 pub fn remaining(&self) -> usize {
175 let cursor = *self
176 .cursor
177 .lock()
178 .unwrap_or_else(|poisoned| poisoned.into_inner());
179 self.entries.len().saturating_sub(cursor)
180 }
181
182 fn next_entry(&self, expected_kind: &str) -> Result<EffectTraceEntry, String> {
183 let mut cursor = self
184 .cursor
185 .lock()
186 .unwrap_or_else(|poisoned| poisoned.into_inner());
187 let idx = *cursor;
188 let Some(entry) = self.entries.get(idx) else {
189 return Err(format!(
190 "replay trace exhausted at index {idx}, expected {expected_kind}"
191 ));
192 };
193 if entry.effect_kind != expected_kind {
194 return Err(format!(
195 "replay trace kind mismatch at index {idx}: expected {expected_kind}, got {}",
196 entry.effect_kind
197 ));
198 }
199 *cursor = cursor.saturating_add(1);
200 Ok(entry.clone())
201 }
202
203 fn parse_send_decision(
204 outputs: &JsonValue,
205 _explicit_payload: Option<Value>,
206 ) -> Option<EffectResult<SendDecision>> {
207 let result = decode_effect_result::<SendDecision>(outputs)?;
208 match result {
209 EffectResult::Success(SendDecision::Deliver(payload)) => {
210 Some(EffectResult::Success(SendDecision::Deliver(payload)))
211 }
212 EffectResult::Success(SendDecision::Drop) => {
213 Some(EffectResult::Success(SendDecision::Drop))
214 }
215 EffectResult::Success(SendDecision::Defer) => {
216 Some(EffectResult::Success(SendDecision::Defer))
217 }
218 EffectResult::Blocked => Some(EffectResult::Blocked),
219 EffectResult::Failure(failure) => Some(EffectResult::Failure(failure)),
220 }
221 }
222}