use std::collections::HashMap;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;
use arc_swap::ArcSwap;
use crate::ChatMessage;
use crate::util::UnwrapPoison;
#[derive(Debug, Default)]
pub struct TranscriptSnapshot {
pub history: Vec<ChatMessage>,
pub token_count: Option<u64>,
}
struct TranscriptEntry {
generation: u64,
holder: Arc<ArcSwap<TranscriptSnapshot>>,
}
#[derive(Default)]
pub(crate) struct TranscriptRegistry {
inner: Mutex<HashMap<String, TranscriptEntry>>,
}
impl TranscriptRegistry {
pub fn register(&self, agent_id: String, generation: u64) -> Arc<ArcSwap<TranscriptSnapshot>> {
let holder = Arc::new(ArcSwap::from_pointee(TranscriptSnapshot::default()));
let mut map = self.inner.lock().unwrap_poison();
map.insert(
agent_id,
TranscriptEntry {
generation,
holder: holder.clone(),
},
);
holder
}
pub fn deregister(&self, agent_id: &str, generation: u64) {
let mut map = self.inner.lock().unwrap_poison();
if let Some(entry) = map.get(agent_id)
&& entry.generation == generation
{
map.remove(agent_id);
}
}
#[must_use]
pub fn snapshot(&self, agent_id: &str) -> Option<Arc<TranscriptSnapshot>> {
let holder = {
self.inner
.lock()
.unwrap_poison()
.get(agent_id)
.map(|e| e.holder.clone())
}?;
Some(holder.load_full())
}
}
pub(crate) static TRANSCRIPT_REGISTRY: LazyLock<TranscriptRegistry> =
LazyLock::new(TranscriptRegistry::default);
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicU64;
fn next_gen() -> u64 {
static GEN: AtomicU64 = AtomicU64::new(1);
GEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
#[test]
fn register_publishes_initial_empty_snapshot() {
let agent_id = format!("snap_{}", crate::generate_suffix());
let generation = next_gen();
let holder = TRANSCRIPT_REGISTRY.register(agent_id.clone(), generation);
let snap = TRANSCRIPT_REGISTRY
.snapshot(&agent_id)
.expect("registered agent has a snapshot");
assert!(snap.history.is_empty());
assert_eq!(snap.token_count, None);
let _ = holder;
TRANSCRIPT_REGISTRY.deregister(&agent_id, generation);
}
#[test]
fn deregister_removes_generation_match_only() {
let agent_id = format!("snap_dereg_{}", crate::generate_suffix());
let generation = next_gen();
let holder = TRANSCRIPT_REGISTRY.register(agent_id.clone(), generation);
TRANSCRIPT_REGISTRY.deregister(&agent_id, generation + 1);
assert!(
TRANSCRIPT_REGISTRY.snapshot(&agent_id).is_some(),
"stale deregister must not remove the entry"
);
TRANSCRIPT_REGISTRY.deregister(&agent_id, generation);
assert!(
TRANSCRIPT_REGISTRY.snapshot(&agent_id).is_none(),
"matching deregister removes the entry"
);
let _ = holder;
}
#[test]
fn store_publishes_new_snapshot_for_reader() {
let agent_id = format!("snap_store_{}", crate::generate_suffix());
let generation = next_gen();
let holder = TRANSCRIPT_REGISTRY.register(agent_id.clone(), generation);
let snap = TranscriptSnapshot {
history: vec![ChatMessage::assistant("hello")],
token_count: Some(42),
};
holder.store(Arc::new(snap));
let read = TRANSCRIPT_REGISTRY
.snapshot(&agent_id)
.expect("snapshot visible after store");
assert_eq!(read.history.len(), 1);
assert_eq!(read.token_count, Some(42));
TRANSCRIPT_REGISTRY.deregister(&agent_id, generation);
}
#[test]
fn replacement_gets_fresh_holder_and_stale_deregister_keeps_it() {
let agent_id = format!("snap_replace_{}", crate::generate_suffix());
let gen_a = next_gen();
let _holder_a = TRANSCRIPT_REGISTRY.register(agent_id.clone(), gen_a);
let gen_b = next_gen();
let holder_b = TRANSCRIPT_REGISTRY.register(agent_id.clone(), gen_b);
TRANSCRIPT_REGISTRY.deregister(&agent_id, gen_a);
assert!(
TRANSCRIPT_REGISTRY.snapshot(&agent_id).is_some(),
"replacement entry survives the stale deregister"
);
let _ = holder_b;
TRANSCRIPT_REGISTRY.deregister(&agent_id, gen_b);
}
}