khive_runtime/
event_store_guard.rs1use std::collections::BTreeMap;
10use std::sync::Arc;
11
12use async_trait::async_trait;
13use khive_storage::event::{
14 EventGroupBy, EventPageQuery, EventPageWindow, IdempotentEventBatchResult,
15};
16use khive_storage::{
17 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, StorageResult,
18};
19use uuid::Uuid;
20
21use crate::NamespaceToken;
22
23#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct EventAttribution {
32 namespace: String,
33 actor: String,
34 operation: Option<khive_types::OperationAttribution>,
35}
36
37impl EventAttribution {
38 pub fn from_token(token: &NamespaceToken) -> Self {
41 Self {
42 namespace: token.namespace().as_str().to_owned(),
43 actor: format!("{}:{}", token.actor().kind, token.actor().id),
44 operation: khive_storage::operation_context::current_operation_attribution(),
45 }
46 }
47
48 pub fn stamp(&self, mut event: Event) -> Event {
50 event.namespace.clone_from(&self.namespace);
51 event.actor.clone_from(&self.actor);
52 if let Some(operation) = self.operation {
55 event.op_index = Some(operation.op_index);
56 event.ref_resolution = Some(operation.ref_resolution);
57 }
58 event
59 }
60}
61
62pub(crate) struct AttributedEventStore {
63 inner: Arc<dyn EventStore>,
64 attribution: EventAttribution,
65}
66
67impl AttributedEventStore {
68 pub(crate) fn wrap(inner: Arc<dyn EventStore>, token: &NamespaceToken) -> Arc<dyn EventStore> {
69 let mut attribution = EventAttribution::from_token(token);
70 attribution.operation = None;
74 Arc::new(Self { inner, attribution })
75 }
76
77 fn attribute(&self, event: Event) -> Event {
78 self.attribution.stamp(event)
79 }
80
81 fn attribute_many(&self, events: Vec<Event>) -> Vec<Event> {
82 events
83 .into_iter()
84 .map(|event| self.attribute(event))
85 .collect()
86 }
87}
88
89#[async_trait]
90impl EventStore for AttributedEventStore {
91 async fn append_event(&self, event: Event) -> StorageResult<()> {
92 self.inner.append_event(self.attribute(event)).await
93 }
94
95 async fn append_events(&self, events: Vec<Event>) -> StorageResult<BatchWriteSummary> {
96 self.inner.append_events(self.attribute_many(events)).await
97 }
98
99 async fn get_event(&self, id: Uuid) -> StorageResult<Option<Event>> {
100 self.inner.get_event(id).await
101 }
102
103 async fn query_events(
104 &self,
105 filter: EventFilter,
106 page: PageRequest,
107 ) -> StorageResult<Page<Event>> {
108 self.inner.query_events(filter, page).await
109 }
110
111 async fn count_events(&self, filter: EventFilter) -> StorageResult<u64> {
112 self.inner.count_events(filter).await
113 }
114
115 async fn count_events_grouped(
116 &self,
117 filter: EventFilter,
118 group_by: EventGroupBy,
119 ) -> StorageResult<BTreeMap<String, u64>> {
120 self.inner.count_events_grouped(filter, group_by).await
121 }
122
123 async fn query_event_page(&self, query: EventPageQuery) -> StorageResult<EventPageWindow> {
124 self.inner.query_event_page(query).await
125 }
126
127 fn preflight_event(&self, event: &Event) -> StorageResult<()> {
128 self.inner.preflight_event(&self.attribute(event.clone()))
129 }
130
131 async fn append_events_idempotent(
132 &self,
133 events: Vec<Event>,
134 ) -> StorageResult<IdempotentEventBatchResult> {
135 self.inner
136 .append_events_idempotent(self.attribute_many(events))
137 .await
138 }
139
140 fn supports_idempotent_audit_batch(&self) -> bool {
141 self.inner.supports_idempotent_audit_batch()
142 }
143}
144
145#[cfg(test)]
146mod operation_tests {
147 use super::*;
148 use khive_storage::operation_context::scope_operation_attribution;
149 use khive_types::{EventKind, OperationAttribution, RefResolution, SubstrateKind};
150
151 fn event() -> Event {
152 Event::new(
153 "local",
154 "test.operation",
155 EventKind::Audit,
156 SubstrateKind::Event,
157 "fixture",
158 )
159 }
160
161 #[tokio::test]
162 async fn reusable_store_does_not_reuse_operation_but_explicit_capture_survives_defer() {
163 let runtime = crate::KhiveRuntime::memory().unwrap();
164 let token = runtime.authorize(crate::Namespace::local()).unwrap();
165 let operation = OperationAttribution {
166 op_index: 2,
167 ref_resolution: RefResolution::Resolved,
168 };
169 let (store, captured) = scope_operation_attribution(operation, async {
170 (
171 runtime.events(&token).unwrap(),
172 EventAttribution::from_token(&token),
173 )
174 })
175 .await;
176
177 let outside = event();
178 store.append_event(outside.clone()).await.unwrap();
179 let stored = store.get_event(outside.id).await.unwrap().unwrap();
180 assert_eq!((stored.op_index, stored.ref_resolution), (None, None));
181
182 let deferred = tokio::spawn(async move { captured.stamp(event()) })
183 .await
184 .unwrap();
185 assert_eq!(
186 (deferred.op_index, deferred.ref_resolution),
187 (Some(2), Some(RefResolution::Resolved))
188 );
189 store.append_event(deferred.clone()).await.unwrap();
190 let stored = store.get_event(deferred.id).await.unwrap().unwrap();
191 assert_eq!(
192 (stored.op_index, stored.ref_resolution),
193 (Some(2), Some(RefResolution::Resolved))
194 );
195
196 let next = scope_operation_attribution(
197 OperationAttribution {
198 op_index: 7,
199 ref_resolution: RefResolution::Literal,
200 },
201 async { event() },
202 )
203 .await;
204 store.append_event(next.clone()).await.unwrap();
205 let stored = store.get_event(next.id).await.unwrap().unwrap();
206 assert_eq!(
207 (stored.op_index, stored.ref_resolution),
208 (Some(7), Some(RefResolution::Literal))
209 );
210 }
211}