use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::Mutex;
use crate::domain::A2AError;
use crate::port::event_log::{AsyncEventLog, Replay};
use crate::port::streaming_handler::{SeqEvent, UpdateEvent};
pub const DEFAULT_CAPACITY: usize = 256;
struct TaskLog {
next_id: u64,
events: VecDeque<SeqEvent>,
}
impl TaskLog {
fn new(capacity: usize) -> Self {
Self {
next_id: 0,
events: VecDeque::with_capacity(capacity),
}
}
fn append(&mut self, event: UpdateEvent, capacity: usize) -> SeqEvent {
self.next_id += 1;
let seq = SeqEvent::new(self.next_id, event);
if self.events.len() == capacity {
self.events.pop_front();
}
self.events.push_back(seq.clone());
seq
}
}
#[derive(Clone)]
pub struct InMemoryEventLog {
tasks: Arc<Mutex<HashMap<String, TaskLog>>>,
capacity: usize,
}
impl InMemoryEventLog {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
tasks: Arc::new(Mutex::new(HashMap::new())),
capacity: capacity.max(1),
}
}
}
impl Default for InMemoryEventLog {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AsyncEventLog for InMemoryEventLog {
async fn append(&self, task_id: &str, event: UpdateEvent) -> Result<SeqEvent, A2AError> {
let mut guard = self.tasks.lock().await;
Ok(guard
.entry(task_id.to_string())
.or_insert_with(|| TaskLog::new(self.capacity))
.append(event, self.capacity))
}
async fn replay(&self, task_id: &str, from: u64) -> Result<Replay, A2AError> {
let guard = self.tasks.lock().await;
let Some(log) = guard.get(task_id) else {
return Ok(Replay::bounded_by(None, from, Vec::new()));
};
let oldest = log.events.front().map(|event| event.id);
let events = log
.events
.iter()
.filter(|event| event.id > from)
.cloned()
.collect();
Ok(Replay::bounded_by(oldest, from, events))
}
async fn discard(&self, task_id: &str) -> Result<(), A2AError> {
self.tasks.lock().await.remove(task_id);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{TaskState, TaskStatus, TaskStatusUpdateEvent};
fn event(state: TaskState) -> UpdateEvent {
UpdateEvent::StatusUpdate(TaskStatusUpdateEvent {
task_id: "t".to_string(),
context_id: "ctx".to_string(),
kind: "status-update".to_string(),
status: TaskStatus::new(state, None),
metadata: None,
})
}
#[tokio::test]
async fn ids_start_at_one_and_run_per_task() {
let log = InMemoryEventLog::new();
assert_eq!(
log.append("a", event(TaskState::Working)).await.unwrap().id,
1
);
assert_eq!(
log.append("a", event(TaskState::Working)).await.unwrap().id,
2
);
assert_eq!(
log.append("b", event(TaskState::Working)).await.unwrap().id,
1,
"a second task counts from its own start"
);
}
#[tokio::test]
async fn a_covered_gap_replays_only_its_tail() {
let log = InMemoryEventLog::new();
for _ in 0..5 {
log.append("t", event(TaskState::Working)).await.unwrap();
}
let replay = log.replay("t", 3).await.unwrap();
assert!(replay.complete);
assert_eq!(
replay.events.iter().map(|e| e.id).collect::<Vec<_>>(),
vec![4, 5]
);
}
#[tokio::test]
async fn a_caught_up_client_gets_an_empty_complete_replay() {
let log = InMemoryEventLog::new();
for _ in 0..3 {
log.append("t", event(TaskState::Working)).await.unwrap();
}
let replay = log.replay("t", 3).await.unwrap();
assert!(replay.complete);
assert!(replay.events.is_empty());
}
#[tokio::test]
async fn falling_off_the_ring_is_an_incomplete_replay() {
let log = InMemoryEventLog::with_capacity(4);
for _ in 0..10 {
log.append("t", event(TaskState::Working)).await.unwrap();
}
let replay = log.replay("t", 1).await.unwrap();
assert!(
!replay.complete,
"events 2..6 are gone, so this is not the tail the client asked for"
);
assert_eq!(
replay.events.iter().map(|e| e.id).collect::<Vec<_>>(),
vec![7, 8, 9, 10]
);
let covered = log.replay("t", 6).await.unwrap();
assert!(
covered.complete,
"id 7 is still held, so the gap is covered"
);
}
#[tokio::test]
async fn an_empty_log_covers_only_a_client_that_has_seen_nothing() {
let log = InMemoryEventLog::new();
assert!(log.replay("t", 0).await.unwrap().complete);
assert!(!log.replay("t", 7).await.unwrap().complete);
}
#[tokio::test]
async fn discarding_forgets_the_task() {
let log = InMemoryEventLog::new();
log.append("t", event(TaskState::Working)).await.unwrap();
log.discard("t").await.unwrap();
let replay = log.replay("t", 0).await.unwrap();
assert!(replay.events.is_empty());
assert_eq!(
log.append("t", event(TaskState::Working)).await.unwrap().id,
1,
"a discarded task counts from the start again"
);
}
}