a2a_rs/adapter/storage/
event_log.rs1use std::collections::{HashMap, VecDeque};
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use tokio::sync::Mutex;
8
9use crate::domain::A2AError;
10use crate::port::event_log::{AsyncEventLog, Replay};
11use crate::port::streaming_handler::{SeqEvent, UpdateEvent};
12
13pub const DEFAULT_CAPACITY: usize = 256;
15
16struct TaskLog {
18 next_id: u64,
19 events: VecDeque<SeqEvent>,
20}
21
22impl TaskLog {
23 fn new(capacity: usize) -> Self {
24 Self {
25 next_id: 0,
26 events: VecDeque::with_capacity(capacity),
27 }
28 }
29
30 fn append(&mut self, event: UpdateEvent, capacity: usize) -> SeqEvent {
31 self.next_id += 1;
32 let seq = SeqEvent::new(self.next_id, event);
33 if self.events.len() == capacity {
34 self.events.pop_front();
35 }
36 self.events.push_back(seq.clone());
37 seq
38 }
39}
40
41#[derive(Clone)]
61pub struct InMemoryEventLog {
62 tasks: Arc<Mutex<HashMap<String, TaskLog>>>,
63 capacity: usize,
64}
65
66impl InMemoryEventLog {
67 pub fn new() -> Self {
69 Self::with_capacity(DEFAULT_CAPACITY)
70 }
71
72 pub fn with_capacity(capacity: usize) -> Self {
77 Self {
78 tasks: Arc::new(Mutex::new(HashMap::new())),
79 capacity: capacity.max(1),
80 }
81 }
82}
83
84impl Default for InMemoryEventLog {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90#[async_trait]
91impl AsyncEventLog for InMemoryEventLog {
92 async fn append(&self, task_id: &str, event: UpdateEvent) -> Result<SeqEvent, A2AError> {
93 let mut guard = self.tasks.lock().await;
94 Ok(guard
95 .entry(task_id.to_string())
96 .or_insert_with(|| TaskLog::new(self.capacity))
97 .append(event, self.capacity))
98 }
99
100 async fn replay(&self, task_id: &str, from: u64) -> Result<Replay, A2AError> {
101 let guard = self.tasks.lock().await;
102 let Some(log) = guard.get(task_id) else {
103 return Ok(Replay::bounded_by(None, from, Vec::new()));
104 };
105 let oldest = log.events.front().map(|event| event.id);
106 let events = log
107 .events
108 .iter()
109 .filter(|event| event.id > from)
110 .cloned()
111 .collect();
112 Ok(Replay::bounded_by(oldest, from, events))
113 }
114
115 async fn discard(&self, task_id: &str) -> Result<(), A2AError> {
116 self.tasks.lock().await.remove(task_id);
117 Ok(())
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::domain::{TaskState, TaskStatus, TaskStatusUpdateEvent};
125
126 fn event(state: TaskState) -> UpdateEvent {
127 UpdateEvent::StatusUpdate(TaskStatusUpdateEvent {
128 task_id: "t".to_string(),
129 context_id: "ctx".to_string(),
130 kind: "status-update".to_string(),
131 status: TaskStatus::new(state, None),
132 metadata: None,
133 })
134 }
135
136 #[tokio::test]
137 async fn ids_start_at_one_and_run_per_task() {
138 let log = InMemoryEventLog::new();
139 assert_eq!(
140 log.append("a", event(TaskState::Working)).await.unwrap().id,
141 1
142 );
143 assert_eq!(
144 log.append("a", event(TaskState::Working)).await.unwrap().id,
145 2
146 );
147 assert_eq!(
148 log.append("b", event(TaskState::Working)).await.unwrap().id,
149 1,
150 "a second task counts from its own start"
151 );
152 }
153
154 #[tokio::test]
155 async fn a_covered_gap_replays_only_its_tail() {
156 let log = InMemoryEventLog::new();
157 for _ in 0..5 {
158 log.append("t", event(TaskState::Working)).await.unwrap();
159 }
160
161 let replay = log.replay("t", 3).await.unwrap();
162 assert!(replay.complete);
163 assert_eq!(
164 replay.events.iter().map(|e| e.id).collect::<Vec<_>>(),
165 vec![4, 5]
166 );
167 }
168
169 #[tokio::test]
172 async fn a_caught_up_client_gets_an_empty_complete_replay() {
173 let log = InMemoryEventLog::new();
174 for _ in 0..3 {
175 log.append("t", event(TaskState::Working)).await.unwrap();
176 }
177
178 let replay = log.replay("t", 3).await.unwrap();
179 assert!(replay.complete);
180 assert!(replay.events.is_empty());
181 }
182
183 #[tokio::test]
186 async fn falling_off_the_ring_is_an_incomplete_replay() {
187 let log = InMemoryEventLog::with_capacity(4);
188 for _ in 0..10 {
189 log.append("t", event(TaskState::Working)).await.unwrap();
190 }
191
192 let replay = log.replay("t", 1).await.unwrap();
193 assert!(
194 !replay.complete,
195 "events 2..6 are gone, so this is not the tail the client asked for"
196 );
197 assert_eq!(
198 replay.events.iter().map(|e| e.id).collect::<Vec<_>>(),
199 vec![7, 8, 9, 10]
200 );
201
202 let covered = log.replay("t", 6).await.unwrap();
203 assert!(
204 covered.complete,
205 "id 7 is still held, so the gap is covered"
206 );
207 }
208
209 #[tokio::test]
212 async fn an_empty_log_covers_only_a_client_that_has_seen_nothing() {
213 let log = InMemoryEventLog::new();
214 assert!(log.replay("t", 0).await.unwrap().complete);
215 assert!(!log.replay("t", 7).await.unwrap().complete);
216 }
217
218 #[tokio::test]
219 async fn discarding_forgets_the_task() {
220 let log = InMemoryEventLog::new();
221 log.append("t", event(TaskState::Working)).await.unwrap();
222 log.discard("t").await.unwrap();
223
224 let replay = log.replay("t", 0).await.unwrap();
225 assert!(replay.events.is_empty());
226 assert_eq!(
227 log.append("t", event(TaskState::Working)).await.unwrap().id,
228 1,
229 "a discarded task counts from the start again"
230 );
231 }
232}