1use std::time::{Duration, Instant};
2
3use async_trait::async_trait;
4use cortexkit_bus_trait::{
5 BusError, BusResult, ContentDigest, DurableOwner, Headers, Message, PublishReceipt, Stream,
6 StreamCursor, StreamDelivery,
7};
8
9use crate::backend::{
10 durable_name, BackendEvent, DurableState, InMemoryBus, Lease, QueueEntry, StoredMessage,
11};
12
13pub struct InMemoryStreamCursor {
18 bus: InMemoryBus,
19 key: String,
21 id: u64,
22 in_flight_index: Option<usize>,
23}
24
25#[async_trait]
26impl Stream for InMemoryBus {
27 type Cursor = InMemoryStreamCursor;
28
29 async fn publish(
30 &self,
31 subject: &str,
32 id: &str,
33 digest: ContentDigest,
34 headers: Headers,
35 ) -> BusResult<PublishReceipt> {
36 let mut state = self.inner.lock().expect("in-memory bus poisoned");
37 if state.config.denied_subjects.contains(subject) {
38 state
39 .permission_violations
40 .push_back(cortexkit_bus_trait::AsyncPermissionViolation {
41 subject: subject.into(),
42 reason: "fixture-configured publish denial".into(),
43 });
44 return Err(BusError::denied(
45 subject,
46 "fixture-configured publish denial",
47 ));
48 }
49
50 let now = (self.deduplication_clock)();
51 let window = state.config.deduplication_window;
52 state
53 .seen_ids
54 .retain(|_, (_, stored_at)| now.duration_since(*stored_at) < window);
55 if let Some(&(stream_seq, _)) = state.seen_ids.get(id) {
56 return Ok(PublishReceipt::new(stream_seq, true));
57 }
58
59 let message = Message {
60 subject: subject.into(),
61 id: id.into(),
62 digest,
63 headers,
64 };
65 if let Some(config) = &state.config.work_queue {
66 if subject == config.subject {
67 let queued_bytes = state
68 .queue
69 .iter()
70 .map(|entry| entry.message.wire_size())
71 .sum::<usize>();
72 if queued_bytes + message.wire_size() > config.max_bytes {
73 return Err(BusError::unavailable_limit(
74 "max_bytes",
75 format!("work queue {} is full", config.subject),
76 ));
77 }
78 }
79 }
80
81 let stream_seq = state.next_stream_seq;
82 state.next_stream_seq += 1;
83 state.seen_ids.insert(id.into(), (stream_seq, now));
84 state.stream_messages.push(StoredMessage {
85 stream_seq,
86 message: message.clone(),
87 });
88 if state
89 .config
90 .work_queue
91 .as_ref()
92 .is_some_and(|config| subject == config.subject)
93 {
94 let token = cortexkit_bus_trait::DeliveryToken(state.next_delivery_token);
95 state.next_delivery_token += 1;
96 state.queue.push_back(QueueEntry {
97 token,
98 message: message.clone(),
99 delivery_count: 0,
100 available_at: Instant::now(),
101 in_flight: false,
102 });
103 }
104 state.events.push(BackendEvent::Published {
105 subject: subject.into(),
106 id: id.into(),
107 });
108 Ok(PublishReceipt::new(stream_seq, false))
109 }
110
111 async fn consumer(&self, owner: DurableOwner<'_>) -> BusResult<Self::Cursor> {
112 let key = durable_name(owner)?;
113 let mut state = self.inner.lock().expect("in-memory bus poisoned");
114 let Some(durable_state) = state.durable_cursors.get_mut(&key) else {
115 return Err(BusError::absent(format!("durable consumer {key}")));
116 };
117 durable_state.next_cursor_id += 1;
118 let id = durable_state.next_cursor_id;
119 Ok(InMemoryStreamCursor {
120 bus: self.clone(),
121 key,
122 id,
123 in_flight_index: None,
124 })
125 }
126}
127
128#[async_trait]
129impl StreamCursor for InMemoryStreamCursor {
130 async fn next(&mut self) -> BusResult<Option<StreamDelivery>> {
131 if self.in_flight_index.is_some() {
132 return Err(BusError::unavailable(
133 "the current delivery must be acknowledged, negatively acknowledged or terminated",
134 ));
135 }
136
137 let mut guard = self.bus.inner.lock().expect("in-memory bus poisoned");
138 let state = &mut *guard;
139 let ack_wait = state.config.stream_ack_wait;
140 let durable = state
141 .durable_cursors
142 .get_mut(&self.key)
143 .expect("durable disappeared after binding");
144 let now = (self.bus.stream_clock)();
145 if durable
147 .lease
148 .as_ref()
149 .is_some_and(|lease| lease.deadline > now)
150 {
151 return Ok(None);
152 }
153 if durable
154 .available_at
155 .is_some_and(|available_at| available_at > now)
156 {
157 return Ok(None);
158 }
159 let Some(stored) = state.stream_messages.get(durable.next_index) else {
160 return Ok(None);
161 };
162 durable.delivery_count += 1;
163 durable.available_at = None;
164 durable.lease = Some(Lease {
165 cursor_id: self.id,
166 deadline: now + ack_wait,
167 });
168 self.in_flight_index = Some(durable.next_index);
169 Ok(Some(StreamDelivery {
170 stream_seq: stored.stream_seq,
171 delivery_count: durable.delivery_count,
172 message: stored.message.clone(),
173 }))
174 }
175
176 async fn ack(&mut self) -> BusResult<()> {
177 let index = self.take_in_flight()?;
178 let mut state = self.bus.inner.lock().expect("in-memory bus poisoned");
179 let durable = durable_mut(&mut state.durable_cursors, &self.key);
180 if durable.next_index == index {
183 advance(durable);
184 }
185 Ok(())
186 }
187
188 async fn nak(&mut self, delay: Duration) -> BusResult<()> {
189 let index = self.take_in_flight()?;
190 let mut state = self.bus.inner.lock().expect("in-memory bus poisoned");
191 let durable = durable_mut(&mut state.durable_cursors, &self.key);
192 if durable.next_index == index && holds_lease(durable, self.id) {
193 durable.lease = None;
194 durable.available_at = Some((self.bus.stream_clock)() + delay);
195 }
196 Ok(())
197 }
198
199 async fn term(&mut self, reason: Option<&str>) -> BusResult<()> {
200 let index = self.take_in_flight()?;
201 let mut guard = self.bus.inner.lock().expect("in-memory bus poisoned");
202 let state = &mut *guard;
203 let durable = durable_mut(&mut state.durable_cursors, &self.key);
204 if durable.next_index == index {
205 advance(durable);
208 state.events.push(BackendEvent::StreamTerminated {
209 stream_seq: state.stream_messages[index].stream_seq,
210 reason: reason.map(str::to_owned),
211 });
212 }
213 Ok(())
214 }
215
216 async fn in_progress(&mut self) -> BusResult<()> {
217 let index = self
218 .in_flight_index
219 .ok_or_else(|| BusError::absent("in-flight stream delivery"))?;
220 let mut guard = self.bus.inner.lock().expect("in-memory bus poisoned");
221 let state = &mut *guard;
222 let ack_wait = state.config.stream_ack_wait;
223 let durable = durable_mut(&mut state.durable_cursors, &self.key);
224 if durable.next_index == index && holds_lease(durable, self.id) {
227 if let Some(lease) = durable.lease.as_mut() {
228 lease.deadline = (self.bus.stream_clock)() + ack_wait;
229 }
230 }
231 Ok(())
232 }
233}
234
235impl InMemoryStreamCursor {
236 fn take_in_flight(&mut self) -> BusResult<usize> {
237 self.in_flight_index
238 .take()
239 .ok_or_else(|| BusError::absent("in-flight stream delivery"))
240 }
241}
242
243impl Drop for InMemoryStreamCursor {
248 fn drop(&mut self) {
249 let Some(index) = self.in_flight_index else {
250 return;
251 };
252 let Ok(mut state) = self.bus.inner.lock() else {
253 return;
254 };
255 if let Some(durable) = state.durable_cursors.get_mut(&self.key) {
256 if durable.next_index == index && holds_lease(durable, self.id) {
257 durable.lease = None;
258 }
259 }
260 }
261}
262
263fn durable_mut<'a>(
264 durables: &'a mut std::collections::HashMap<String, DurableState>,
265 key: &str,
266) -> &'a mut DurableState {
267 durables
268 .get_mut(key)
269 .expect("durable disappeared after binding")
270}
271
272fn holds_lease(durable: &DurableState, cursor_id: u64) -> bool {
273 durable
274 .lease
275 .as_ref()
276 .is_some_and(|lease| lease.cursor_id == cursor_id)
277}
278
279fn advance(durable: &mut DurableState) {
281 durable.next_index += 1;
282 durable.available_at = None;
283 durable.delivery_count = 0;
284 durable.lease = None;
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::InMemoryConfig;
291
292 #[tokio::test]
293 async fn a_cursor_binds_the_durable_of_the_kind_its_caller_names() {
294 let bus = InMemoryBus::new(
295 InMemoryConfig::default().with_durable(DurableOwner::Module("prefrontal-core")),
296 );
297 assert!(!bus
298 .publish(
299 "ck.box.room.room_a.post",
300 "room:room_a:1",
301 ContentDigest::of_bytes(b"post"),
302 Headers::new(),
303 )
304 .await
305 .expect("publish")
306 .duplicate());
307
308 let mut cursor = bus
309 .consumer(DurableOwner::Module("prefrontal-core"))
310 .await
311 .expect("the module durable binds");
312 let delivery = cursor.next().await.expect("next").expect("a delivery");
313 assert_eq!(delivery.message.id, "room:room_a:1");
314 cursor.ack().await.expect("ack");
315
316 assert!(matches!(
319 bus.consumer(DurableOwner::Agent("prefrontal-core")).await,
320 Err(BusError::Absent { .. })
321 ));
322 assert!(matches!(
324 bus.consumer(DurableOwner::Module("prefrontal.core")).await,
325 Err(BusError::Denied { .. })
326 ));
327 }
328}