1use crate::bus::{BusManager, BusMessage};
2use crate::cache::EntityCache;
3use crate::mutation_batch::{MutationBatch, SlotContext};
4use crate::view::{ViewIndex, ViewSpec};
5use crate::websocket::frame::{apply_wire_format, Mode, SourceFrame};
6use arete_interpreter::CanonicalLog;
7use bytes::Bytes;
8use serde_json::Value;
9use smallvec::SmallVec;
10use std::sync::Arc;
11use tokio::sync::mpsc;
12use tracing::{debug, error, info_span, instrument};
13
14#[cfg(feature = "otel")]
15use crate::metrics::Metrics;
16
17pub struct Projector {
18 view_index: Arc<ViewIndex>,
19 bus_manager: BusManager,
20 entity_cache: EntityCache,
21 mutations_rx: mpsc::Receiver<MutationBatch>,
22 snapshot_runtime: Option<crate::snapshot::SnapshotRuntime>,
23 journal: Option<Arc<crate::journal::EventJournal>>,
24 #[cfg(feature = "otel")]
25 metrics: Option<Arc<Metrics>>,
26}
27
28impl Projector {
29 #[cfg(feature = "otel")]
30 pub fn new(
31 view_index: Arc<ViewIndex>,
32 bus_manager: BusManager,
33 entity_cache: EntityCache,
34 mutations_rx: mpsc::Receiver<MutationBatch>,
35 metrics: Option<Arc<Metrics>>,
36 ) -> Self {
37 Self {
38 view_index,
39 bus_manager,
40 entity_cache,
41 mutations_rx,
42 snapshot_runtime: None,
43 journal: None,
44 metrics,
45 }
46 }
47
48 #[cfg(not(feature = "otel"))]
49 pub fn new(
50 view_index: Arc<ViewIndex>,
51 bus_manager: BusManager,
52 entity_cache: EntityCache,
53 mutations_rx: mpsc::Receiver<MutationBatch>,
54 ) -> Self {
55 Self {
56 view_index,
57 bus_manager,
58 entity_cache,
59 mutations_rx,
60 snapshot_runtime: None,
61 journal: None,
62 }
63 }
64
65 pub fn with_snapshot_runtime(
67 mut self,
68 snapshot_runtime: crate::snapshot::SnapshotRuntime,
69 ) -> Self {
70 self.snapshot_runtime = Some(snapshot_runtime);
71 self
72 }
73
74 pub fn with_journal(mut self, journal: Arc<crate::journal::EventJournal>) -> Self {
76 self.journal = Some(journal);
77 self
78 }
79
80 pub async fn run(mut self) {
81 debug!("Projector started");
82
83 let mut json_buffer = Vec::with_capacity(4096);
84
85 while let Some(mut batch) = self.mutations_rx.recv().await {
86 let mut log = CanonicalLog::new();
87 log.set("phase", "projector");
88
89 let batch_size = batch.len();
90 let slot_context = batch.slot_context;
91 let batch_span = info_span!(
92 parent: &batch.span,
93 "projector.batch",
94 batch.mutations = batch_size,
95 batch.position = tracing::field::Empty,
96 frames_published = tracing::field::Empty,
97 );
98 if let Some(context) = slot_context {
99 batch_span.record("batch.position", context.to_seq_string());
100 }
101 let _span_guard = batch_span.enter();
102 let mut frames_published = 0u32;
103 let mut errors = 0u32;
104
105 if let Some(ctx) = batch.event_context.as_ref() {
106 log.set("program", &ctx.program)
107 .set("event_kind", &ctx.event_kind)
108 .set("event_type", &ctx.event_type)
109 .set("account", &ctx.account)
110 .set("accounts_count", ctx.accounts_count);
111 }
112
113 for mutation in std::mem::take(&mut batch.mutations).into_iter() {
114 #[cfg(feature = "otel")]
115 let export = mutation.export.clone();
116
117 match self
118 .process_mutation(mutation, slot_context, &mut json_buffer)
119 .await
120 {
121 Ok(count) => frames_published += count,
122 Err(e) => {
123 error!("Failed to process mutation: {}", e);
124 errors += 1;
125 }
126 }
127
128 #[cfg(feature = "otel")]
129 if let Some(ref metrics) = self.metrics {
130 metrics.record_mutation_processed(&export);
131 }
132 }
133
134 if batch_size > 0 {
139 if let Some(snapshot_runtime) = &self.snapshot_runtime {
140 snapshot_runtime.record_applied_batch(slot_context.map(|ctx| ctx.slot));
141 }
142 }
143 drop(batch.snapshot_guard.take());
144
145 if let Some(ack) = batch.flush_ack.take() {
148 let _ = ack.send(());
149 }
150
151 log.set("batch_size", batch_size)
152 .set("frames_published", frames_published)
153 .set("errors", errors);
154 batch_span.record("frames_published", frames_published);
155
156 #[cfg(feature = "otel")]
157 if let Some(ref metrics) = self.metrics {
158 metrics.record_projector_latency(log.duration_ms());
159 }
160
161 log.emit();
162 }
163
164 debug!("Projector stopped");
165 }
166
167 #[instrument(
168 name = "projector.mutation",
169 skip(self, mutation, slot_context, json_buffer),
170 fields(export = %mutation.export)
171 )]
172 async fn process_mutation(
173 &self,
174 mutation: arete_interpreter::Mutation,
175 slot_context: Option<SlotContext>,
176 json_buffer: &mut Vec<u8>,
177 ) -> anyhow::Result<u32> {
178 let specs = self.view_index.by_export(&mutation.export);
179
180 if specs.is_empty() {
181 return Ok(0);
182 }
183
184 let key = Self::extract_key(&mutation.key);
185 let arete_interpreter::Mutation {
186 mut patch, append, ..
187 } = mutation;
188
189 if let Some(ctx) = slot_context {
191 if let Value::Object(ref mut map) = patch {
192 map.insert("_seq".to_string(), Value::String(ctx.to_seq_string()));
193 }
194 }
195
196 let matching_specs: SmallVec<[&ViewSpec; 4]> = specs
197 .iter()
198 .filter(|spec| spec.filters.matches(&key))
199 .collect();
200
201 let match_count = matching_specs.len();
202 if match_count == 0 {
203 return Ok(0);
204 }
205
206 let mut frames_published = 0u32;
207
208 for (i, spec) in matching_specs.into_iter().enumerate() {
209 let is_last = i == match_count - 1;
210 let patch_data = if is_last {
211 std::mem::take(&mut patch)
212 } else {
213 patch.clone()
214 };
215
216 let projected = spec.projection.apply(patch_data);
217 let mut wire_data = projected.clone();
218 apply_wire_format(&mut wire_data, &spec.wire_format);
219
220 let seq = slot_context.map(|ctx| ctx.to_seq_string());
222
223 let journal = self
228 .journal
229 .as_ref()
230 .filter(|journal| journal.is_enabled() && spec.mode == Mode::Append);
231
232 let mut frame = SourceFrame {
233 mode: spec.mode,
234 export: spec.id.clone(),
235 op: "patch",
236 key: key.clone(),
237 data: wire_data,
238 append: append.clone(),
239 seq,
240 offset: None,
241 };
242
243 let retained = match journal {
244 Some(journal) => {
245 journal
246 .append_with(&spec.id, &key, |offset| {
247 frame.offset = Some(offset);
248 json_buffer.clear();
249 serde_json::to_writer(&mut *json_buffer, &frame)?;
250 Ok::<_, anyhow::Error>(Arc::new(Bytes::copy_from_slice(json_buffer)))
251 })
252 .await?
253 }
254 None => None,
255 };
256 let payload = match retained {
257 Some((_offset, payload)) => payload,
258 None => {
261 frame.offset = None;
262 json_buffer.clear();
263 serde_json::to_writer(&mut *json_buffer, &frame)?;
264 Arc::new(Bytes::copy_from_slice(json_buffer))
265 }
266 };
267
268 self.entity_cache
269 .upsert_with_append(&spec.id, &key, projected, &frame.append)
270 .await;
271
272 if spec.mode == Mode::List {
273 self.update_derived_view_caches(&spec.id, &key).await;
274 }
275
276 let message = Arc::new(BusMessage {
277 key: key.clone(),
278 entity: spec.id.clone(),
279 payload,
280 });
281
282 self.publish_frame(spec, message).await;
283 frames_published += 1;
284
285 #[cfg(feature = "otel")]
286 if let Some(ref metrics) = self.metrics {
287 let mode_str = match spec.mode {
288 Mode::List => "list",
289 Mode::State => "state",
290 Mode::Append => "append",
291 };
292 metrics.record_frame_published(mode_str, &spec.export);
293 }
294 }
295
296 Ok(frames_published)
297 }
298
299 fn extract_key(key: &serde_json::Value) -> String {
300 key.as_str()
301 .map(|s| s.to_string())
302 .or_else(|| key.as_u64().map(|n| n.to_string()))
303 .or_else(|| key.as_i64().map(|n| n.to_string()))
304 .or_else(|| {
305 key.as_array().and_then(|arr| {
306 let bytes: Vec<u8> = arr
307 .iter()
308 .filter_map(|v| v.as_u64().map(|n| n as u8))
309 .collect();
310 if bytes.len() == arr.len() {
311 Some(hex::encode(&bytes))
312 } else {
313 None
314 }
315 })
316 })
317 .unwrap_or_else(|| key.to_string())
318 }
319
320 async fn update_derived_view_caches(&self, source_view_id: &str, entity_key: &str) {
321 let derived_views = self.view_index.get_derived_views_for_source(source_view_id);
322 if derived_views.is_empty() {
323 return;
324 }
325
326 let entity_data = match self.entity_cache.get(source_view_id, entity_key).await {
327 Some(data) => data,
328 None => return,
329 };
330
331 let max_entries = self.entity_cache.max_entities_per_view();
334 let sorted_caches = self.view_index.sorted_caches();
335 let mut caches = sorted_caches.write().await;
336
337 for derived_spec in derived_views {
338 if let Some(cache) = caches.get_mut(&derived_spec.id) {
339 cache.upsert_bounded(entity_key.to_string(), entity_data.clone(), max_entries);
340 debug!(
341 "Updated sorted cache for derived view {} with key {}",
342 derived_spec.id, entity_key
343 );
344 }
345 }
346 }
347
348 #[instrument(
349 name = "projector.publish",
350 skip(self, spec, message),
351 fields(view_id = %spec.id, mode = ?spec.mode)
352 )]
353 async fn publish_frame(&self, spec: &ViewSpec, message: Arc<BusMessage>) {
354 match spec.mode {
355 Mode::State => {
356 self.bus_manager
357 .publish_state(&spec.id, &message.key, message.payload.clone())
358 .await;
359 }
360 Mode::List | Mode::Append => {
361 self.bus_manager.publish_list(&spec.id, message).await;
362 }
363 }
364 }
365}