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, debug_span, error, 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 = debug_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), false) = (slot_context, batch_span.is_disabled()) {
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 level = "debug",
170 skip(self, mutation, slot_context, json_buffer),
171 fields(export = %mutation.export)
172 )]
173 async fn process_mutation(
174 &self,
175 mutation: arete_interpreter::Mutation,
176 slot_context: Option<SlotContext>,
177 json_buffer: &mut Vec<u8>,
178 ) -> anyhow::Result<u32> {
179 let specs = self.view_index.by_export(&mutation.export);
180
181 if specs.is_empty() {
182 return Ok(0);
183 }
184
185 let key = Self::extract_key(&mutation.key);
186 let arete_interpreter::Mutation {
187 mut patch, append, ..
188 } = mutation;
189
190 if let Some(ctx) = slot_context {
192 if let Value::Object(ref mut map) = patch {
193 map.insert("_seq".to_string(), Value::String(ctx.to_seq_string()));
194 }
195 }
196
197 let matching_specs: SmallVec<[&ViewSpec; 4]> = specs
198 .iter()
199 .filter(|spec| spec.filters.matches(&key))
200 .collect();
201
202 let match_count = matching_specs.len();
203 if match_count == 0 {
204 return Ok(0);
205 }
206
207 let mut frames_published = 0u32;
208
209 for (i, spec) in matching_specs.into_iter().enumerate() {
210 let is_last = i == match_count - 1;
211 let patch_data = if is_last {
212 std::mem::take(&mut patch)
213 } else {
214 patch.clone()
215 };
216
217 let projected = spec.projection.apply(patch_data);
218 let mut wire_data = projected.clone();
219 apply_wire_format(&mut wire_data, &spec.wire_format);
220
221 let seq = slot_context.map(|ctx| ctx.to_seq_string());
223
224 let journal = self
229 .journal
230 .as_ref()
231 .filter(|journal| journal.is_enabled() && spec.mode == Mode::Append);
232
233 let mut frame = SourceFrame {
234 mode: spec.mode,
235 export: spec.id.clone(),
236 op: "patch",
237 key: key.clone(),
238 data: wire_data,
239 append: append.clone(),
240 seq,
241 offset: None,
242 };
243
244 let retained = match journal {
245 Some(journal) => {
246 journal
247 .append_with(&spec.id, &key, |offset| {
248 frame.offset = Some(offset);
249 json_buffer.clear();
250 serde_json::to_writer(&mut *json_buffer, &frame)?;
251 Ok::<_, anyhow::Error>(Arc::new(Bytes::copy_from_slice(json_buffer)))
252 })
253 .await?
254 }
255 None => None,
256 };
257 let payload = match retained {
258 Some((_offset, payload)) => payload,
259 None => {
262 frame.offset = None;
263 json_buffer.clear();
264 serde_json::to_writer(&mut *json_buffer, &frame)?;
265 Arc::new(Bytes::copy_from_slice(json_buffer))
266 }
267 };
268
269 self.entity_cache
270 .upsert_with_append(&spec.id, &key, projected, &frame.append)
271 .await;
272
273 if spec.mode == Mode::List {
274 self.update_derived_view_caches(&spec.id, &key).await;
275 }
276
277 let message = Arc::new(BusMessage {
278 key: key.clone(),
279 entity: spec.id.clone(),
280 payload,
281 });
282
283 self.publish_frame(spec, message).await;
284 frames_published += 1;
285
286 #[cfg(feature = "otel")]
287 if let Some(ref metrics) = self.metrics {
288 let mode_str = match spec.mode {
289 Mode::List => "list",
290 Mode::State => "state",
291 Mode::Append => "append",
292 };
293 metrics.record_frame_published(mode_str, &spec.export);
294 }
295 }
296
297 Ok(frames_published)
298 }
299
300 fn extract_key(key: &serde_json::Value) -> String {
301 key.as_str()
302 .map(|s| s.to_string())
303 .or_else(|| key.as_u64().map(|n| n.to_string()))
304 .or_else(|| key.as_i64().map(|n| n.to_string()))
305 .or_else(|| {
306 key.as_array().and_then(|arr| {
307 let bytes: Vec<u8> = arr
308 .iter()
309 .filter_map(|v| v.as_u64().map(|n| n as u8))
310 .collect();
311 if bytes.len() == arr.len() {
312 Some(hex::encode(&bytes))
313 } else {
314 None
315 }
316 })
317 })
318 .unwrap_or_else(|| key.to_string())
319 }
320
321 async fn update_derived_view_caches(&self, source_view_id: &str, entity_key: &str) {
322 let derived_views = self.view_index.get_derived_views_for_source(source_view_id);
323 if derived_views.is_empty() {
324 return;
325 }
326
327 let entity_data = match self.entity_cache.get(source_view_id, entity_key).await {
328 Some(data) => data,
329 None => return,
330 };
331
332 let max_entries = self.entity_cache.max_entities_per_view();
335 let sorted_caches = self.view_index.sorted_caches();
336 let mut caches = sorted_caches.write().await;
337
338 let mut keeping: SmallVec<[&str; 4]> = SmallVec::new();
343 for spec in &derived_views {
344 let Some(cache) = caches.get_mut(&spec.id) else {
345 continue;
346 };
347 let passes = spec
348 .pipeline
349 .as_ref()
350 .and_then(|pipeline| pipeline.filter.as_ref())
351 .is_none_or(|filter| filter.matches(&entity_data));
352 if !passes {
353 cache.remove(entity_key);
354 continue;
355 }
356 if cache.would_keep(entity_key, &entity_data, max_entries) {
357 keeping.push(spec.id.as_str());
358 }
359 }
360 let mut entity_data = Some(entity_data);
361 for (index, view_id) in keeping.iter().enumerate() {
362 let Some(cache) = caches.get_mut(*view_id) else {
363 continue;
364 };
365 let entity = if index + 1 == keeping.len() {
366 entity_data.take()
367 } else {
368 entity_data.clone()
369 };
370 let Some(entity) = entity else {
371 continue;
372 };
373 cache.upsert_bounded(entity_key.to_string(), entity, max_entries);
374 debug!(
375 "Updated sorted cache for derived view {} with key {}",
376 view_id, entity_key
377 );
378 }
379 }
380
381 #[instrument(
382 name = "projector.publish",
383 level = "debug",
384 skip(self, spec, message),
385 fields(view_id = %spec.id, mode = ?spec.mode)
386 )]
387 async fn publish_frame(&self, spec: &ViewSpec, message: Arc<BusMessage>) {
388 match spec.mode {
389 Mode::State => {
390 self.bus_manager
391 .publish_state(&spec.id, &message.key, message.payload.clone())
392 .await;
393 }
394 Mode::List | Mode::Append => {
395 self.bus_manager.publish_list(&spec.id, message).await;
396 }
397 }
398 }
399}