Skip to main content

aimdb_core/transform/
single.rs

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
13// ============================================================================
14// TransformBuilder → TransformPipeline
15// ============================================================================
16
17/// Configures a single-input transform pipeline.
18///
19/// Created by `RecordRegistrar::transform()`. Use `.map()` for stateless
20/// transforms or `.with_state()` for stateful transforms.
21pub 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    /// Stateless 1:1 map. Returning `None` skips output for this input value.
39    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    /// Begin configuring a stateful transform. `S` is the user-defined state type.
54    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
66/// Intermediate builder for stateful single-input transforms.
67pub 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    /// Called for each input value. Receives mutable state, returns optional output.
80    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
95/// Completed single-input transform pipeline, ready to be stored in `TypedRecord`.
96pub 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// ============================================================================
145// Transform Task Runner
146// ============================================================================
147
148#[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    // `Producer<O>` no longer exposes a `.key()` accessor — the
163    // output key is threaded in by the transform descriptor so diagnostics
164    // remain unambiguous when multiple records share type `O`.
165    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}