use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::approval_core::ApprovalCore;
use crate::messaging_orchestrator::MessagingOrchestrator;
use crate::slack_adapter::SlackAdapter;
#[derive(Default)]
struct SharedCodeMap {
id_to_code: HashMap<String, String>,
next: u64,
}
impl SharedCodeMap {
fn get_or_mint(&mut self, approval_id: &str) -> (String, bool) {
if let Some(code) = self.id_to_code.get(approval_id) {
return (code.clone(), false);
}
let n = self.next;
self.next += 1;
let letter = (b'A' + (n % 26) as u8) as char;
let num = n / 26;
let code = format!("{letter}{num}");
self.id_to_code.insert(approval_id.to_string(), code.clone());
(code, true)
}
fn evict(&mut self, approval_id: &str) {
self.id_to_code.remove(approval_id);
}
}
pub struct FanoutCoordinator {
core: ApprovalCore,
codes: Mutex<SharedCodeMap>,
imessage: Option<Arc<MessagingOrchestrator>>,
slack: Option<Arc<SlackAdapter>>,
}
impl FanoutCoordinator {
pub fn new(
core: ApprovalCore,
imessage: Option<Arc<MessagingOrchestrator>>,
slack: Option<Arc<SlackAdapter>>,
) -> Self {
Self {
core,
codes: Mutex::new(SharedCodeMap::default()),
imessage,
slack,
}
}
pub async fn observe_and_fanout(&self) {
let eligible = self.core.eligible_pending().await;
{
let live: std::collections::HashSet<&str> =
eligible.iter().map(|a| a.id.as_str()).collect();
let mut codes = self.codes.lock().await;
let stale: Vec<String> = codes
.id_to_code
.keys()
.filter(|id| !live.contains(id.as_str()))
.cloned()
.collect();
for id in stale {
codes.evict(&id);
}
}
for approval in &eligible {
let (code, is_new) = {
let mut codes = self.codes.lock().await;
codes.get_or_mint(&approval.id)
};
if !is_new {
continue; }
if let Some(imsg) = &self.imessage {
imsg.send_shared_prompt(approval, &code).await;
}
if let Some(slack) = &self.slack {
slack.post_prompt(&approval.action, &approval.id, &code).await;
}
}
}
}