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
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    /// Create an empty tape.
47    #[must_use]
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Create a tape from pre-recorded entries.
53    #[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    /// Record one effect entry.
65    ///
66    /// # Panics
67    ///
68    /// Panics if the internal mutex is poisoned.
69    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    /// Clone all recorded entries.
98    ///
99    /// # Panics
100    ///
101    /// Panics if the internal mutex is poisoned.
102    #[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
111/// A handler wrapper that records effect outcomes for replay.
112pub struct RecordingEffectHandler<'a> {
113    inner: &'a dyn EffectHandler,
114    tape: EffectTraceTape,
115}
116
117impl<'a> RecordingEffectHandler<'a> {
118    /// Wrap a base handler and begin recording effect outcomes.
119    #[must_use]
120    pub fn new(inner: &'a dyn EffectHandler) -> Self {
121        Self {
122            inner,
123            tape: EffectTraceTape::new(),
124        }
125    }
126
127    /// Clone the recorded effect trace.
128    #[must_use]
129    pub fn effect_trace(&self) -> Vec<EffectTraceEntry> {
130        self.tape.entries()
131    }
132}
133
134/// A replay-mode handler that serves recorded effect outcomes in order.
135pub struct ReplayEffectHandler<'a> {
136    entries: Arc<[EffectTraceEntry]>,
137    cursor: Mutex<usize>,
138    fallback: Option<&'a dyn EffectHandler>,
139}
140
141impl<'a> ReplayEffectHandler<'a> {
142    /// Build a replay handler without fallback behavior.
143    #[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    /// Build a replay handler with fallback behavior for unsupported entries.
156    #[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    /// Number of unconsumed entries.
169    ///
170    /// # Panics
171    ///
172    /// Panics if the internal mutex is poisoned.
173    #[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}