1use core::fmt::Debug;
2use core::marker::PhantomData;
3
4use alloc::{
5 boxed::Box,
6 string::{String, ToString},
7 sync::Arc,
8 vec,
9};
10
11use crate::transform::{CollectedTransform, TransformDescriptor, TransformProfiling};
12
13pub struct TransformBuilder<I, O> {
22 input_key: String,
23 _phantom: PhantomData<(I, O)>,
24}
25
26impl<I, O> TransformBuilder<I, O>
27where
28 I: Send + Sync + Clone + Debug + 'static,
29 O: Send + Sync + Clone + Debug + 'static,
30{
31 pub(crate) fn new(input_key: String) -> Self {
32 Self {
33 input_key,
34 _phantom: PhantomData,
35 }
36 }
37
38 pub fn map<F>(self, f: F) -> TransformPipeline<I, O>
40 where
41 F: Fn(&I) -> Option<O> + Send + Sync + 'static,
42 {
43 TransformPipeline {
44 input_key: self.input_key,
45 spawn_factory: Box::new(move |input_key| {
46 let transform_fn = move |val: &I, _state: &mut ()| f(val);
47 create_single_transform_descriptor::<I, O, ()>(input_key, (), transform_fn)
48 }),
49 _phantom_i: PhantomData,
50 }
51 }
52
53 pub fn with_state<S: Send + Sync + 'static>(
55 self,
56 initial: S,
57 ) -> StatefulTransformBuilder<I, O, S> {
58 StatefulTransformBuilder {
59 input_key: self.input_key,
60 initial_state: initial,
61 _phantom: PhantomData,
62 }
63 }
64}
65
66pub struct StatefulTransformBuilder<I, O, S> {
68 input_key: String,
69 initial_state: S,
70 _phantom: PhantomData<(I, O)>,
71}
72
73impl<I, O, S> StatefulTransformBuilder<I, O, S>
74where
75 I: Send + Sync + Clone + Debug + 'static,
76 O: Send + Sync + Clone + Debug + 'static,
77 S: Send + Sync + 'static,
78{
79 pub fn on_value<F>(self, f: F) -> TransformPipeline<I, O>
81 where
82 F: Fn(&I, &mut S) -> Option<O> + Send + Sync + 'static,
83 {
84 let initial = self.initial_state;
85 TransformPipeline {
86 input_key: self.input_key,
87 spawn_factory: Box::new(move |input_key| {
88 create_single_transform_descriptor::<I, O, S>(input_key, initial, f)
89 }),
90 _phantom_i: PhantomData,
91 }
92 }
93}
94
95pub struct TransformPipeline<I, O: Send + Sync + Clone + Debug + 'static> {
97 pub(crate) input_key: String,
98 pub(crate) spawn_factory: Box<dyn FnOnce(String) -> TransformDescriptor<O> + Send + Sync>,
99 pub(crate) _phantom_i: PhantomData<I>,
100}
101
102impl<I, O> TransformPipeline<I, O>
103where
104 I: Send + Sync + Clone + Debug + 'static,
105 O: Send + Sync + Clone + Debug + 'static,
106{
107 pub(crate) fn into_descriptor(self) -> TransformDescriptor<O> {
108 (self.spawn_factory)(self.input_key)
109 }
110}
111
112fn create_single_transform_descriptor<I, O, S>(
113 input_key: String,
114 initial_state: S,
115 transform_fn: impl Fn(&I, &mut S) -> Option<O> + Send + Sync + 'static,
116) -> TransformDescriptor<O>
117where
118 I: Send + Sync + Clone + Debug + 'static,
119 O: Send + Sync + Clone + Debug + 'static,
120 S: Send + Sync + 'static,
121{
122 let input_key_clone = input_key.clone();
123 let input_keys = vec![input_key];
124
125 TransformDescriptor {
126 input_keys,
127 build_fn: Box::new(
128 move |producer, db, output_key, profiling| CollectedTransform {
129 task_future: Box::pin(run_single_transform::<I, O, S>(
130 db,
131 input_key_clone,
132 output_key.to_string(),
133 producer,
134 initial_state,
135 transform_fn,
136 profiling,
137 )),
138 fanin_futures: alloc::vec::Vec::new(),
139 },
140 ),
141 }
142}
143
144#[allow(unused_variables)]
149pub(crate) async fn run_single_transform<I, O, S>(
150 db: Arc<crate::AimDb>,
151 input_key: String,
152 output_key: String,
153 producer: crate::Producer<O>,
154 mut state: S,
155 transform_fn: impl Fn(&I, &mut S) -> Option<O> + Send + Sync + 'static,
156 profiling: TransformProfiling,
157) where
158 I: Send + Sync + Clone + Debug + 'static,
159 O: Send + Sync + Clone + Debug + 'static,
160 S: Send + 'static,
161{
162 log_info!("🔄 Transform started: '{}' → '{}'", input_key, output_key);
166
167 let consumer = match db.consumer::<I>(&input_key) {
168 Ok(c) => c,
169 Err(_e) => {
170 log_error!(
171 "🔄 Transform '{}' → '{}' FATAL: failed to resolve consumer: {:?}",
172 input_key,
173 output_key,
174 _e
175 );
176 return;
177 }
178 };
179 let mut reader = consumer.subscribe();
180
181 log_debug!(
182 "✅ Transform '{}' → '{}' subscribed, entering event loop",
183 input_key,
184 output_key
185 );
186
187 loop {
188 match reader.recv().await {
189 Ok(input_value) => {
190 #[cfg(feature = "observability")]
191 {
192 let started_ns = profiling.as_ref().map(|(_, clock)| clock());
193 let output_value = transform_fn(&input_value, &mut state);
194 if let (Some((metrics, clock)), Some(started_ns)) = (&profiling, started_ns) {
195 metrics.record(clock().saturating_sub(started_ns));
196 }
197 if let Some(output_value) = output_value {
198 producer.produce(output_value);
199 }
200 }
201 #[cfg(not(feature = "observability"))]
202 {
203 if let Some(output_value) = transform_fn(&input_value, &mut state) {
204 producer.produce(output_value);
205 }
206 }
207 }
208 Err(crate::DbError::BufferLagged { .. }) => {
209 log_warn!(
210 "🔄 Transform '{}' → '{}' lagged behind, some values skipped",
211 input_key,
212 output_key
213 );
214 continue;
215 }
216 Err(_) => {
217 log_warn!(
218 "🔄 Transform '{}' → '{}' input closed, task exiting",
219 input_key,
220 output_key
221 );
222 break;
223 }
224 }
225 }
226}