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, 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 #[cfg(feature = "otel")]
24 metrics: Option<Arc<Metrics>>,
25}
26
27impl Projector {
28 #[cfg(feature = "otel")]
29 pub fn new(
30 view_index: Arc<ViewIndex>,
31 bus_manager: BusManager,
32 entity_cache: EntityCache,
33 mutations_rx: mpsc::Receiver<MutationBatch>,
34 metrics: Option<Arc<Metrics>>,
35 ) -> Self {
36 Self {
37 view_index,
38 bus_manager,
39 entity_cache,
40 mutations_rx,
41 snapshot_runtime: None,
42 metrics,
43 }
44 }
45
46 #[cfg(not(feature = "otel"))]
47 pub fn new(
48 view_index: Arc<ViewIndex>,
49 bus_manager: BusManager,
50 entity_cache: EntityCache,
51 mutations_rx: mpsc::Receiver<MutationBatch>,
52 ) -> Self {
53 Self {
54 view_index,
55 bus_manager,
56 entity_cache,
57 mutations_rx,
58 snapshot_runtime: None,
59 }
60 }
61
62 pub fn with_snapshot_runtime(
64 mut self,
65 snapshot_runtime: crate::snapshot::SnapshotRuntime,
66 ) -> Self {
67 self.snapshot_runtime = Some(snapshot_runtime);
68 self
69 }
70
71 pub async fn run(mut self) {
72 debug!("Projector started");
73
74 let mut json_buffer = Vec::with_capacity(4096);
75
76 while let Some(mut batch) = self.mutations_rx.recv().await {
77 let _span_guard = batch.span.enter();
78
79 let mut log = CanonicalLog::new();
80 log.set("phase", "projector");
81
82 let batch_size = batch.len();
83 let slot_context = batch.slot_context;
84 let mut frames_published = 0u32;
85 let mut errors = 0u32;
86
87 if let Some(ctx) = batch.event_context.as_ref() {
88 log.set("program", &ctx.program)
89 .set("event_kind", &ctx.event_kind)
90 .set("event_type", &ctx.event_type)
91 .set("account", &ctx.account)
92 .set("accounts_count", ctx.accounts_count);
93 }
94
95 for mutation in std::mem::take(&mut batch.mutations).into_iter() {
96 #[cfg(feature = "otel")]
97 let export = mutation.export.clone();
98
99 match self
100 .process_mutation(mutation, slot_context, &mut json_buffer)
101 .await
102 {
103 Ok(count) => frames_published += count,
104 Err(e) => {
105 error!("Failed to process mutation: {}", e);
106 errors += 1;
107 }
108 }
109
110 #[cfg(feature = "otel")]
111 if let Some(ref metrics) = self.metrics {
112 metrics.record_mutation_processed(&export);
113 }
114 }
115
116 if batch_size > 0 {
121 if let Some(snapshot_runtime) = &self.snapshot_runtime {
122 snapshot_runtime.record_applied_batch(slot_context.map(|ctx| ctx.slot));
123 }
124 }
125 drop(batch.snapshot_guard.take());
126
127 if let Some(ack) = batch.flush_ack.take() {
130 let _ = ack.send(());
131 }
132
133 log.set("batch_size", batch_size)
134 .set("frames_published", frames_published)
135 .set("errors", errors);
136
137 #[cfg(feature = "otel")]
138 if let Some(ref metrics) = self.metrics {
139 metrics.record_projector_latency(log.duration_ms());
140 }
141
142 log.emit();
143 }
144
145 debug!("Projector stopped");
146 }
147
148 #[instrument(
149 name = "projector.mutation",
150 skip(self, mutation, slot_context, json_buffer),
151 fields(export = %mutation.export)
152 )]
153 async fn process_mutation(
154 &self,
155 mutation: arete_interpreter::Mutation,
156 slot_context: Option<SlotContext>,
157 json_buffer: &mut Vec<u8>,
158 ) -> anyhow::Result<u32> {
159 let specs = self.view_index.by_export(&mutation.export);
160
161 if specs.is_empty() {
162 return Ok(0);
163 }
164
165 let key = Self::extract_key(&mutation.key);
166 let arete_interpreter::Mutation {
167 mut patch, append, ..
168 } = mutation;
169
170 if let Some(ctx) = slot_context {
172 if let Value::Object(ref mut map) = patch {
173 map.insert("_seq".to_string(), Value::String(ctx.to_seq_string()));
174 }
175 }
176
177 let matching_specs: SmallVec<[&ViewSpec; 4]> = specs
178 .iter()
179 .filter(|spec| spec.filters.matches(&key))
180 .collect();
181
182 let match_count = matching_specs.len();
183 if match_count == 0 {
184 return Ok(0);
185 }
186
187 let mut frames_published = 0u32;
188
189 for (i, spec) in matching_specs.into_iter().enumerate() {
190 let is_last = i == match_count - 1;
191 let patch_data = if is_last {
192 std::mem::take(&mut patch)
193 } else {
194 patch.clone()
195 };
196
197 let projected = spec.projection.apply(patch_data);
198 let mut wire_data = projected.clone();
199 apply_wire_format(&mut wire_data, &spec.wire_format);
200
201 let seq = slot_context.map(|ctx| ctx.to_seq_string());
203
204 let frame = SourceFrame {
205 mode: spec.mode,
206 export: spec.id.clone(),
207 op: "patch",
208 key: key.clone(),
209 data: wire_data,
210 append: append.clone(),
211 seq,
212 };
213
214 json_buffer.clear();
215 serde_json::to_writer(&mut *json_buffer, &frame)?;
216 let payload = Arc::new(Bytes::copy_from_slice(json_buffer));
217
218 self.entity_cache
219 .upsert_with_append(&spec.id, &key, projected, &frame.append)
220 .await;
221
222 if spec.mode == Mode::List {
223 self.update_derived_view_caches(&spec.id, &key).await;
224 }
225
226 let message = Arc::new(BusMessage {
227 key: key.clone(),
228 entity: spec.id.clone(),
229 payload,
230 });
231
232 self.publish_frame(spec, message).await;
233 frames_published += 1;
234
235 #[cfg(feature = "otel")]
236 if let Some(ref metrics) = self.metrics {
237 let mode_str = match spec.mode {
238 Mode::List => "list",
239 Mode::State => "state",
240 Mode::Append => "append",
241 };
242 metrics.record_frame_published(mode_str, &spec.export);
243 }
244 }
245
246 Ok(frames_published)
247 }
248
249 fn extract_key(key: &serde_json::Value) -> String {
250 key.as_str()
251 .map(|s| s.to_string())
252 .or_else(|| key.as_u64().map(|n| n.to_string()))
253 .or_else(|| key.as_i64().map(|n| n.to_string()))
254 .or_else(|| {
255 key.as_array().and_then(|arr| {
256 let bytes: Vec<u8> = arr
257 .iter()
258 .filter_map(|v| v.as_u64().map(|n| n as u8))
259 .collect();
260 if bytes.len() == arr.len() {
261 Some(hex::encode(&bytes))
262 } else {
263 None
264 }
265 })
266 })
267 .unwrap_or_else(|| key.to_string())
268 }
269
270 async fn update_derived_view_caches(&self, source_view_id: &str, entity_key: &str) {
271 let derived_views = self.view_index.get_derived_views_for_source(source_view_id);
272 if derived_views.is_empty() {
273 return;
274 }
275
276 let entity_data = match self.entity_cache.get(source_view_id, entity_key).await {
277 Some(data) => data,
278 None => return,
279 };
280
281 let sorted_caches = self.view_index.sorted_caches();
282 let mut caches = sorted_caches.write().await;
283
284 for derived_spec in derived_views {
285 if let Some(cache) = caches.get_mut(&derived_spec.id) {
286 cache.upsert(entity_key.to_string(), entity_data.clone());
287 debug!(
288 "Updated sorted cache for derived view {} with key {}",
289 derived_spec.id, entity_key
290 );
291 }
292 }
293 }
294
295 #[instrument(
296 name = "projector.publish",
297 skip(self, spec, message),
298 fields(view_id = %spec.id, mode = ?spec.mode)
299 )]
300 async fn publish_frame(&self, spec: &ViewSpec, message: Arc<BusMessage>) {
301 match spec.mode {
302 Mode::State => {
303 self.bus_manager
304 .publish_state(&spec.id, &message.key, message.payload.clone())
305 .await;
306 }
307 Mode::List | Mode::Append => {
308 self.bus_manager.publish_list(&spec.id, message).await;
309 }
310 }
311 }
312}