telltale_runtime/effects/handlers/
recording.rs1use async_trait::async_trait;
7use serde::{de::DeserializeOwned, Serialize};
8use std::time::Duration;
9
10use crate::effects::contract::{
11 DeliveryModel, DocumentedHandlerContract, ExtensionDispatchContract, ExtensionDispatchMode,
12 HandlerContractProfile, HandlerContractTier, ProtocolSemanticContract, RetryPolicy,
13 TimeoutPolicy, TransportPolicyContract,
14};
15use crate::effects::{ChoreoHandler, ChoreoResult, ChoreographyError, RoleId};
16
17#[derive(Clone)]
19pub struct RecordingHandler<R: RoleId> {
20 pub events: std::sync::Arc<std::sync::Mutex<Vec<RecordedEvent<R>>>>,
21 role: R,
22}
23
24#[derive(Debug, Clone)]
25pub enum RecordedEvent<R: RoleId> {
26 Send { from: R, to: R, msg_type: String },
27 Recv { from: R, to: R, msg_type: String },
28 Choose { at: R, label: <R as RoleId>::Label },
29 Offer { from: R, to: R },
30}
31
32impl<R: RoleId> RecordingHandler<R> {
33 pub fn new(role: R) -> Self {
34 Self {
35 events: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
36 role,
37 }
38 }
39
40 pub fn events(&self) -> Vec<RecordedEvent<R>> {
41 self.events
42 .lock()
43 .unwrap_or_else(std::sync::PoisonError::into_inner)
44 .clone()
45 }
46
47 pub fn clear(&self) {
48 self.events
49 .lock()
50 .unwrap_or_else(std::sync::PoisonError::into_inner)
51 .clear();
52 }
53}
54
55impl<R: RoleId> DocumentedHandlerContract for RecordingHandler<R> {
56 fn contract_profile() -> HandlerContractProfile {
57 HandlerContractProfile {
58 handler_name: std::any::type_name::<Self>(),
59 tier: HandlerContractTier::ObservationalHarness,
60 semantics: ProtocolSemanticContract {
61 typed_send_recv_roundtrip: false,
62 exact_choice_label_preservation: true,
63 fail_closed_transport_errors: true,
64 timeouts_scoped_to_enforcing_role: true,
65 deterministic_for_regression: true,
66 can_materialize_values: false,
67 },
68 transport: TransportPolicyContract {
69 delivery_model: DeliveryModel::ScriptedHarness,
70 retry_policy: RetryPolicy::None,
71 timeout_policy: TimeoutPolicy::EnforcingRoleOnly,
72 },
73 extension_dispatch: ExtensionDispatchContract {
74 mode: ExtensionDispatchMode::Unsupported,
75 fail_closed_when_unregistered: false,
76 type_exact_before_side_effects: false,
77 },
78 notes: vec![
79 "captures exact operation order for regression use",
80 "recv/offer intentionally fail closed after recording the attempted effect",
81 ],
82 }
83 }
84}
85
86#[async_trait]
87impl<R: RoleId + 'static> ChoreoHandler for RecordingHandler<R> {
88 type Role = R;
89 type Endpoint = ();
90
91 async fn send<M: Serialize + Send + Sync>(
92 &mut self,
93 _ep: &mut Self::Endpoint,
94 to: Self::Role,
95 _msg: &M,
96 ) -> ChoreoResult<()> {
97 self.events
98 .lock()
99 .unwrap_or_else(std::sync::PoisonError::into_inner)
100 .push(RecordedEvent::Send {
101 from: self.role,
102 to,
103 msg_type: std::any::type_name::<M>().to_string(),
104 });
105 Ok(())
106 }
107
108 async fn recv<M: DeserializeOwned + Send>(
109 &mut self,
110 _ep: &mut Self::Endpoint,
111 from: Self::Role,
112 ) -> ChoreoResult<M> {
113 self.events
114 .lock()
115 .unwrap_or_else(std::sync::PoisonError::into_inner)
116 .push(RecordedEvent::Recv {
117 from,
118 to: self.role,
119 msg_type: std::any::type_name::<M>().to_string(),
120 });
121 Err(ChoreographyError::Transport(
122 "RecordingHandler cannot produce values".into(),
123 ))
124 }
125
126 async fn choose(
127 &mut self,
128 _ep: &mut Self::Endpoint,
129 at: Self::Role,
130 label: <Self::Role as RoleId>::Label,
131 ) -> ChoreoResult<()> {
132 self.events
133 .lock()
134 .unwrap_or_else(std::sync::PoisonError::into_inner)
135 .push(RecordedEvent::Choose { at, label });
136 Ok(())
137 }
138
139 async fn offer(
140 &mut self,
141 _ep: &mut Self::Endpoint,
142 from: Self::Role,
143 ) -> ChoreoResult<<Self::Role as RoleId>::Label> {
144 self.events
145 .lock()
146 .unwrap_or_else(std::sync::PoisonError::into_inner)
147 .push(RecordedEvent::Offer {
148 from,
149 to: self.role,
150 });
151 Err(ChoreographyError::Transport(
152 "RecordingHandler cannot produce labels".into(),
153 ))
154 }
155
156 async fn with_timeout<F, T>(
157 &mut self,
158 _ep: &mut Self::Endpoint,
159 _at: Self::Role,
160 _dur: Duration,
161 body: F,
162 ) -> ChoreoResult<T>
163 where
164 F: std::future::Future<Output = ChoreoResult<T>> + Send,
165 {
166 body.await
167 }
168}