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 .expect("effect trace tape lock poisoned")
53 .push(entry);
54 }
55
56 #[must_use]
62 pub fn entries(&self) -> Vec<EffectTraceEntry> {
63 self.entries
64 .lock()
65 .expect("effect trace tape lock poisoned")
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.cursor.lock().expect("replay cursor lock poisoned");
135 self.entries.len().saturating_sub(cursor)
136 }
137
138 fn next_entry(&self, expected_kind: &str) -> Result<EffectTraceEntry, String> {
139 let mut cursor = self.cursor.lock().expect("replay cursor lock poisoned");
140 let idx = *cursor;
141 let Some(entry) = self.entries.get(idx) else {
142 return Err(format!(
143 "replay trace exhausted at index {idx}, expected {expected_kind}"
144 ));
145 };
146 if entry.effect_kind != expected_kind {
147 return Err(format!(
148 "replay trace kind mismatch at index {idx}: expected {expected_kind}, got {}",
149 entry.effect_kind
150 ));
151 }
152 *cursor = cursor.saturating_add(1);
153 Ok(entry.clone())
154 }
155
156 fn peek_entry_kind(&self) -> Option<String> {
157 let cursor = *self.cursor.lock().expect("replay cursor lock poisoned");
158 self.entries
159 .get(cursor)
160 .map(|entry| entry.effect_kind.clone())
161 }
162
163 fn parse_send_decision(
164 outputs: &JsonValue,
165 explicit_payload: Option<Value>,
166 ) -> Option<SendDecision> {
167 let decision = outputs.get("decision").and_then(JsonValue::as_str)?;
168 match decision {
169 "deliver" => {
170 let payload = outputs
171 .get("payload")
172 .and_then(|value| serde_json::from_value(value.clone()).ok())
173 .or(explicit_payload)
174 .unwrap_or(Value::Unit);
175 Some(SendDecision::Deliver(payload))
176 }
177 "drop" => Some(SendDecision::Drop),
178 "defer" => Some(SendDecision::Defer),
179 _ => None,
180 }
181 }
182
183 fn parse_acquire_decision(outputs: &JsonValue) -> Option<AcquireDecision> {
184 let decision = outputs.get("decision").and_then(JsonValue::as_str)?;
185 match decision {
186 "grant" => {
187 let evidence = outputs
188 .get("evidence")
189 .and_then(|value| serde_json::from_value(value.clone()).ok())
190 .unwrap_or(Value::Unit);
191 Some(AcquireDecision::Grant(evidence))
192 }
193 "block" => Some(AcquireDecision::Block),
194 _ => None,
195 }
196 }
197}
198