telltale_vm/effect/
runtime_types.rs1#[derive(Debug, Default)]
3pub struct EffectTraceTape {
4 next_effect_id: AtomicU64,
5 entries: Mutex<Vec<EffectTraceEntry>>,
6}
7
8impl EffectTraceTape {
9 #[must_use]
11 pub fn new() -> Self {
12 Self::default()
13 }
14
15 #[must_use]
17 pub fn from_entries(entries: Vec<EffectTraceEntry>) -> Self {
18 let next_effect_id = entries
19 .last()
20 .map_or(0, |entry| entry.effect_id.saturating_add(1));
21 Self {
22 next_effect_id: AtomicU64::new(next_effect_id),
23 entries: Mutex::new(entries),
24 }
25 }
26
27 pub fn record(
33 &self,
34 effect_kind: &str,
35 inputs: JsonValue,
36 outputs: JsonValue,
37 handler_identity: &str,
38 topology: Option<TopologyPerturbation>,
39 ) {
40 let effect_id = self.next_effect_id.fetch_add(1, Ordering::Relaxed);
41 let entry = EffectTraceEntry {
42 effect_id,
43 effect_kind: effect_kind.to_string(),
44 inputs,
45 outputs,
46 handler_identity: handler_identity.to_string(),
47 ordering_key: effect_id,
48 topology,
49 };
50 self.entries
51 .lock()
52 .unwrap_or_else(|poisoned| poisoned.into_inner())
53 .push(entry);
54 }
55
56 #[must_use]
62 pub fn entries(&self) -> Vec<EffectTraceEntry> {
63 self.entries
64 .lock()
65 .unwrap_or_else(|poisoned| poisoned.into_inner())
66 .clone()
67 }
68}
69
70pub struct RecordingEffectHandler<'a> {
72 inner: &'a dyn EffectHandler,
73 tape: EffectTraceTape,
74}
75
76impl<'a> RecordingEffectHandler<'a> {
77 #[must_use]
79 pub fn new(inner: &'a dyn EffectHandler) -> Self {
80 Self {
81 inner,
82 tape: EffectTraceTape::new(),
83 }
84 }
85
86 #[must_use]
88 pub fn effect_trace(&self) -> Vec<EffectTraceEntry> {
89 self.tape.entries()
90 }
91}
92
93pub struct ReplayEffectHandler<'a> {
95 entries: Arc<[EffectTraceEntry]>,
96 cursor: Mutex<usize>,
97 fallback: Option<&'a dyn EffectHandler>,
98}
99
100impl<'a> ReplayEffectHandler<'a> {
101 #[must_use]
103 pub fn new<E>(entries: E) -> Self
104 where
105 E: Into<Arc<[EffectTraceEntry]>>,
106 {
107 Self {
108 entries: entries.into(),
109 cursor: Mutex::new(0),
110 fallback: None,
111 }
112 }
113
114 #[must_use]
116 pub fn with_fallback<E>(entries: E, fallback: &'a dyn EffectHandler) -> Self
117 where
118 E: Into<Arc<[EffectTraceEntry]>>,
119 {
120 Self {
121 entries: entries.into(),
122 cursor: Mutex::new(0),
123 fallback: Some(fallback),
124 }
125 }
126
127 #[must_use]
133 pub fn remaining(&self) -> usize {
134 let cursor = *self
135 .cursor
136 .lock()
137 .unwrap_or_else(|poisoned| poisoned.into_inner());
138 self.entries.len().saturating_sub(cursor)
139 }
140
141 fn next_entry(&self, expected_kind: &str) -> Result<EffectTraceEntry, String> {
142 let mut cursor = self
143 .cursor
144 .lock()
145 .unwrap_or_else(|poisoned| poisoned.into_inner());
146 let idx = *cursor;
147 let Some(entry) = self.entries.get(idx) else {
148 return Err(format!(
149 "replay trace exhausted at index {idx}, expected {expected_kind}"
150 ));
151 };
152 if entry.effect_kind != expected_kind {
153 return Err(format!(
154 "replay trace kind mismatch at index {idx}: expected {expected_kind}, got {}",
155 entry.effect_kind
156 ));
157 }
158 *cursor = cursor.saturating_add(1);
159 Ok(entry.clone())
160 }
161
162 fn peek_entry_kind(&self) -> Option<String> {
163 let cursor = *self
164 .cursor
165 .lock()
166 .unwrap_or_else(|poisoned| poisoned.into_inner());
167 self.entries
168 .get(cursor)
169 .map(|entry| entry.effect_kind.clone())
170 }
171
172 fn parse_send_decision(
173 outputs: &JsonValue,
174 explicit_payload: Option<Value>,
175 ) -> Option<SendDecision> {
176 let decision = outputs.get("decision").and_then(JsonValue::as_str)?;
177 match decision {
178 "deliver" => {
179 let payload = outputs
180 .get("payload")
181 .and_then(|value| serde_json::from_value(value.clone()).ok())
182 .or(explicit_payload)
183 .unwrap_or(Value::Unit);
184 Some(SendDecision::Deliver(payload))
185 }
186 "drop" => Some(SendDecision::Drop),
187 "defer" => Some(SendDecision::Defer),
188 _ => None,
189 }
190 }
191
192 fn parse_acquire_decision(outputs: &JsonValue) -> Option<AcquireDecision> {
193 let decision = outputs.get("decision").and_then(JsonValue::as_str)?;
194 match decision {
195 "grant" => {
196 let evidence = outputs
197 .get("evidence")
198 .and_then(|value| serde_json::from_value(value.clone()).ok())
199 .unwrap_or(Value::Unit);
200 Some(AcquireDecision::Grant(evidence))
201 }
202 "block" => Some(AcquireDecision::Block),
203 _ => None,
204 }
205 }
206}