Skip to main content

telltale_vm/effect/
runtime_types.rs

1/// Thread-safe effect-trace tape used by recording/replay handlers.
2#[derive(Debug, Default)]
3pub struct EffectTraceTape {
4    next_effect_id: AtomicU64,
5    entries: Mutex<Vec<EffectTraceEntry>>,
6}
7
8impl EffectTraceTape {
9    /// Create an empty tape.
10    #[must_use]
11    pub fn new() -> Self {
12        Self::default()
13    }
14
15    /// Create a tape from pre-recorded entries.
16    #[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    /// Record one effect entry.
28    ///
29    /// # Panics
30    ///
31    /// Panics if the internal mutex is poisoned.
32    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    /// Clone all recorded entries.
57    ///
58    /// # Panics
59    ///
60    /// Panics if the internal mutex is poisoned.
61    #[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
70/// A handler wrapper that records effect outcomes for replay.
71pub struct RecordingEffectHandler<'a> {
72    inner: &'a dyn EffectHandler,
73    tape: EffectTraceTape,
74}
75
76impl<'a> RecordingEffectHandler<'a> {
77    /// Wrap a base handler and begin recording effect outcomes.
78    #[must_use]
79    pub fn new(inner: &'a dyn EffectHandler) -> Self {
80        Self {
81            inner,
82            tape: EffectTraceTape::new(),
83        }
84    }
85
86    /// Clone the recorded effect trace.
87    #[must_use]
88    pub fn effect_trace(&self) -> Vec<EffectTraceEntry> {
89        self.tape.entries()
90    }
91}
92
93/// A replay-mode handler that serves recorded effect outcomes in order.
94pub struct ReplayEffectHandler<'a> {
95    entries: Arc<[EffectTraceEntry]>,
96    cursor: Mutex<usize>,
97    fallback: Option<&'a dyn EffectHandler>,
98}
99
100impl<'a> ReplayEffectHandler<'a> {
101    /// Build a replay handler without fallback behavior.
102    #[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    /// Build a replay handler with fallback behavior for unsupported entries.
115    #[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    /// Number of unconsumed entries.
128    ///
129    /// # Panics
130    ///
131    /// Panics if the internal mutex is poisoned.
132    #[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}