use core::any::Any;
use core::fmt::Debug;
use core::marker::PhantomData;
use alloc::{
boxed::Box,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use crate::executor::{ExecutorError, ExecutorResult};
use crate::transform::{CollectedTransform, TransformDescriptor};
use crate::typed_record::BoxFuture;
#[cfg(any(feature = "std", target_arch = "wasm32"))]
const JOIN_QUEUE_CAPACITY: usize = 64;
#[cfg(not(any(feature = "std", target_arch = "wasm32")))]
const JOIN_QUEUE_CAPACITY: usize = 16;
fn join_channel() -> (
async_channel::Sender<JoinTrigger>,
async_channel::Receiver<JoinTrigger>,
) {
async_channel::bounded(JOIN_QUEUE_CAPACITY)
}
pub enum JoinTrigger {
Input {
index: usize,
value: Box<dyn Any + Send>,
},
}
impl JoinTrigger {
pub fn as_input<T: 'static>(&self) -> Option<&T> {
match self {
JoinTrigger::Input { value, .. } => value.downcast_ref::<T>(),
}
}
pub fn index(&self) -> usize {
match self {
JoinTrigger::Input { index, .. } => *index,
}
}
}
pub struct JoinEventRx {
inner: async_channel::Receiver<JoinTrigger>,
}
impl JoinEventRx {
fn new(inner: async_channel::Receiver<JoinTrigger>) -> Self {
Self { inner }
}
pub async fn recv(&mut self) -> ExecutorResult<JoinTrigger> {
self.inner
.recv()
.await
.map_err(|_| ExecutorError::QueueClosed)
}
}
type JoinInputFactory = Box<
dyn FnOnce(
Arc<crate::AimDb>,
usize,
async_channel::Sender<JoinTrigger>,
) -> BoxFuture<'static, ()>
+ Send
+ Sync,
>;
pub struct JoinBuilder<O> {
inputs: Vec<(String, JoinInputFactory)>,
_phantom: PhantomData<O>,
}
impl<O> JoinBuilder<O>
where
O: Send + Sync + Clone + Debug + 'static,
{
pub(crate) fn new() -> Self {
Self {
inputs: Vec::new(),
_phantom: PhantomData,
}
}
pub fn input<I>(mut self, key: impl crate::RecordKey) -> Self
where
I: Send + Sync + Clone + Debug + 'static,
{
let key_str = key.as_str().to_string();
let key_for_factory = key_str.clone();
let factory: JoinInputFactory = Box::new(
move |db: Arc<crate::AimDb>, index: usize, tx: async_channel::Sender<JoinTrigger>| {
Box::pin(async move {
let consumer = match db.consumer::<I>(&key_for_factory) {
Ok(c) => c,
Err(_e) => {
log_error!(
"🔄 Join input '{}' (index {}) consumer resolution failed: {:?}",
key_for_factory,
index,
_e
);
return;
}
};
let mut reader = consumer.subscribe();
loop {
let value = match reader.recv().await {
Ok(v) => v,
Err(crate::DbError::BufferLagged { .. }) => {
log_warn!(
"🔄 Join input '{}' (index {}) lagged behind, some values skipped",
key_for_factory,
index
);
continue;
}
Err(_) => break,
};
let trigger = JoinTrigger::Input {
index,
value: Box::new(value),
};
if tx.send(trigger).await.is_err() {
break;
}
}
}) as BoxFuture<'static, ()>
},
);
self.inputs.push((key_str, factory));
self
}
pub fn on_triggers<F, Fut>(self, handler: F) -> JoinPipeline<O>
where
F: FnOnce(JoinEventRx, crate::Producer<O>) -> Fut + Send + 'static,
Fut: core::future::Future<Output = ()> + Send + 'static,
{
let inputs = self.inputs;
let input_keys_for_descriptor: Vec<String> =
inputs.iter().map(|(k, _)| k.clone()).collect();
JoinPipeline {
spawn_factory: Box::new(move |_| TransformDescriptor {
input_keys: input_keys_for_descriptor,
build_fn: Box::new(move |producer, db, output_key, _profiling| {
build_join_collected(db, inputs, producer, handler, output_key)
}),
}),
}
}
}
pub struct JoinPipeline<O: Send + Sync + Clone + Debug + 'static> {
pub(crate) spawn_factory: Box<dyn FnOnce(()) -> TransformDescriptor<O> + Send>,
}
impl<O> JoinPipeline<O>
where
O: Send + Sync + Clone + Debug + 'static,
{
pub(crate) fn into_descriptor(self) -> TransformDescriptor<O> {
(self.spawn_factory)(())
}
}
fn build_join_collected<O, F, Fut>(
db: Arc<crate::AimDb>,
inputs: Vec<(String, JoinInputFactory)>,
producer: crate::Producer<O>,
handler: F,
output_key: &str,
) -> CollectedTransform
where
O: Send + Sync + Clone + Debug + 'static,
F: FnOnce(JoinEventRx, crate::Producer<O>) -> Fut + Send + 'static,
Fut: core::future::Future<Output = ()> + Send + 'static,
{
let output_key = output_key.to_string();
let input_keys: Vec<String> = inputs.iter().map(|(k, _)| k.clone()).collect();
log_info!(
"🔄 Join transform building: {:?} → '{}'",
input_keys,
output_key
);
let (tx, rx) = join_channel();
let fanin_futures: Vec<BoxFuture<'static, ()>> = inputs
.into_iter()
.enumerate()
.map(|(index, (_key, factory))| {
let sender = tx.clone();
factory(db.clone(), index, sender)
})
.collect();
drop(tx);
let task_future: BoxFuture<'static, ()> = Box::pin(async move {
log_debug!(
"✅ Join transform '{}' handing receiver to user task",
output_key
);
handler(JoinEventRx::new(rx), producer).await;
log_warn!("🔄 Join transform '{}' user task exited", output_key);
});
CollectedTransform {
task_future,
fanin_futures,
}
}