1use std::{
9 any::Any,
10 collections::HashMap,
11 sync::{Arc, Mutex},
12 thread,
13 time::Duration,
14};
15
16use dashmap::DashMap;
17use hyphae::{
18 Cell, CellImmutable, CellMap, CellMutable, Gettable, IdFor, MaterializeDefinite, Mutable,
19 WeakCellMap,
20};
21use serde::de::DeserializeOwned;
22use uuid::Uuid;
23
24use super::{
25 HandlerRegistry, RelationshipManager,
26 persister::{PersistError, PersistHealth, PersisterRouter},
27};
28use crate::{
29 cache::CacheKey,
30 client::{ConnectionStatus, MykoClient},
31 common::{
32 to_value::ToValue,
33 with_id::{WithId, WithTypedId},
34 },
35 core::item::{
36 AnyItem, Eventable, IngestBufferPolicy, downcast_any_item_arc, typed_map_arc_from_any_item,
37 typed_map_from_any_item_with_typed_id,
38 },
39 query::{
40 FilteredCellMap, QueryContext, QueryFactory, QueryHandler, QueryParams, QueryRequest,
41 QueryTestCtx,
42 },
43 report::{ReportContext, ReportHandler, ReportId},
44 request::RequestContext,
45 search::SearchIndex,
46 store::StoreRegistry,
47 view::{FilteredViewCellMap, TypedViewCellMap, ViewFactory},
48 wire::{EventOptions, MEvent, MEventType},
49};
50
51type AnyItemArc = Arc<dyn AnyItem>;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) enum Origin {
62 Local,
65 Cascade,
67 #[allow(dead_code)]
75 Remote,
76}
77
78impl Origin {
79 pub(crate) fn from_options(options: &EventOptions) -> Origin {
82 if options.prevent_relationship_updates {
83 Origin::Cascade
84 } else {
85 Origin::Local
86 }
87 }
88
89 fn should_cascade(self, change: MEventType) -> bool {
105 match self {
106 Origin::Local => true,
107 Origin::Cascade => change == MEventType::DEL,
108 Origin::Remote => false,
109 }
110 }
111
112 fn should_produce(self) -> bool {
119 self != Origin::Remote
120 }
121}
122
123trait ReportCacheEntryDyn: Any + Send + Sync {
127 fn as_any(&self) -> &dyn Any;
128 fn is_alive(&self) -> bool;
129}
130
131struct ReportCacheEntry<T> {
132 weak: hyphae::cell::WeakCell<T, CellImmutable>,
133}
134
135impl<T> ReportCacheEntry<T>
136where
137 T: Clone + Send + Sync + 'static,
138{
139 fn new(cell: &Cell<T, CellImmutable>) -> Self {
140 Self {
141 weak: cell.downgrade(),
142 }
143 }
144
145 fn get(&self) -> Option<Cell<T, CellImmutable>> {
146 self.weak.upgrade()
147 }
148}
149
150impl<T> ReportCacheEntryDyn for ReportCacheEntry<T>
151where
152 T: Clone + Send + Sync + 'static,
153{
154 fn as_any(&self) -> &dyn Any {
155 self
156 }
157
158 fn is_alive(&self) -> bool {
159 self.weak.upgrade().is_some()
160 }
161}
162
163struct MapCacheEntry {
164 weak: WeakCellMap<Arc<str>, AnyItemArc>,
165 typed: Mutex<HashMap<std::any::TypeId, Box<dyn Any + Send + Sync>>>,
169}
170
171#[derive(Default)]
172struct BufferedIngestState {
173 events: Vec<MEvent>,
174 flush_scheduled: bool,
175}
176
177struct BufferedIngestType {
178 state: Mutex<BufferedIngestState>,
179}
180
181impl BufferedIngestType {
182 fn new() -> Self {
183 Self {
184 state: Mutex::new(BufferedIngestState::default()),
185 }
186 }
187}
188
189impl MapCacheEntry {
190 fn new(map: &FilteredCellMap) -> Self {
191 Self {
192 weak: map.downgrade(),
193 typed: Mutex::new(HashMap::new()),
194 }
195 }
196
197 fn get(&self) -> Option<FilteredCellMap> {
198 self.weak.upgrade().map(|map| map.lock())
199 }
200
201 fn get_or_create_typed<K, V, F>(&self, create: F) -> Option<CellMap<K, V, CellImmutable>>
206 where
207 K: std::hash::Hash + Eq + hyphae::traits::CellValue + 'static,
208 V: hyphae::traits::CellValue + 'static,
209 F: FnOnce(FilteredCellMap) -> CellMap<K, V, CellImmutable>,
210 {
211 let type_key = std::any::TypeId::of::<WeakCellMap<K, V>>();
212 let mut typed = self.typed.lock().unwrap();
213
214 if let Some(entry) = typed.get(&type_key) {
216 if let Some(weak) = entry.downcast_ref::<WeakCellMap<K, V>>()
217 && let Some(strong) = weak.upgrade()
218 {
219 return Some(strong.lock());
220 }
221 typed.remove(&type_key);
223 }
224
225 let source = self.weak.upgrade()?.lock();
227 let built = create(source);
228 typed.insert(type_key, Box::new(built.downgrade()));
229 Some(built)
230 }
231}
232
233#[derive(Clone)]
240pub struct CellServerCtx {
241 pub host_id: Uuid,
243 pub registry: Arc<StoreRegistry>,
245 pub handler_registry: Arc<HandlerRegistry>,
247 relationship_manager: Arc<RelationshipManager>,
249 persisters: Arc<PersisterRouter>,
251 search_index: Arc<SearchIndex>,
253 peer_clients: Arc<DashMap<Arc<str>, Arc<MykoClient>>>,
255 peer_clients_tick: Cell<u64, CellMutable>,
257 event_sink: Option<flume::Sender<MEvent>>,
259 query_cache: Arc<DashMap<String, MapCacheEntry, ahash::RandomState>>,
264 view_cache: Arc<DashMap<String, MapCacheEntry, ahash::RandomState>>,
266 report_cache: Arc<DashMap<String, Arc<dyn ReportCacheEntryDyn>, ahash::RandomState>>,
268 compute_gates: Arc<DashMap<String, Arc<std::sync::Mutex<()>>, ahash::RandomState>>,
271 ingest_buffers: Arc<DashMap<Arc<str>, Arc<BufferedIngestType>, ahash::RandomState>>,
273 history_replay: Option<Arc<dyn crate::server::HistoryReplayProvider>>,
275}
276
277impl CellServerCtx {
278 #[allow(clippy::too_many_arguments)]
280 pub fn new(
281 host_id: Uuid,
282 registry: Arc<StoreRegistry>,
283 handler_registry: Arc<HandlerRegistry>,
284 relationship_manager: Arc<RelationshipManager>,
285 persisters: Arc<PersisterRouter>,
286 search_index: Arc<SearchIndex>,
287 peer_clients: Arc<DashMap<Arc<str>, Arc<MykoClient>>>,
288 event_sink: Option<flume::Sender<MEvent>>,
289 history_replay: Option<Arc<dyn crate::server::HistoryReplayProvider>>,
290 ) -> Self {
291 Self {
292 host_id,
293 registry,
294 handler_registry,
295 relationship_manager,
296 persisters,
297 search_index,
298 peer_clients,
299 peer_clients_tick: Cell::new(0).with_name("peer_clients_tick"),
300 event_sink,
301 query_cache: Arc::new(DashMap::with_hasher(ahash::RandomState::new())),
302 view_cache: Arc::new(DashMap::with_hasher(ahash::RandomState::new())),
303 report_cache: Arc::new(DashMap::with_hasher(ahash::RandomState::new())),
304 compute_gates: Arc::new(DashMap::with_hasher(ahash::RandomState::new())),
305 ingest_buffers: Arc::new(DashMap::with_hasher(ahash::RandomState::new())),
306 history_replay,
307 }
308 }
309
310 fn cache_key<T: CacheKey>(
311 &self,
312 kind: &str,
313 id: &str,
314 params: &T,
315 request: &RequestContext,
316 ) -> String {
317 let payload_hash = params.cache_key_hash();
318 format!("{}:{kind}:{id}:{payload_hash:016x}", request.host_id)
319 }
320
321 pub fn search_index(&self) -> &Arc<SearchIndex> {
323 &self.search_index
324 }
325
326 pub fn history_replay(&self) -> Option<&Arc<dyn crate::server::HistoryReplayProvider>> {
328 self.history_replay.as_ref()
329 }
330
331 pub fn register_peer_client<S: AsRef<str>>(&self, peer_id: S, client: Arc<MykoClient>) {
333 self.peer_clients
334 .insert(Arc::<str>::from(peer_id.as_ref()), client);
335 let next = self.peer_clients_tick.get().saturating_add(1);
336 self.peer_clients_tick.set(next);
337 }
338
339 pub fn unregister_peer_client(&self, peer_id: &str) {
341 if self.peer_clients.remove(peer_id).is_some() {
342 let next = self.peer_clients_tick.get().saturating_add(1);
343 self.peer_clients_tick.set(next);
344 }
345 }
346
347 pub fn peer_client(&self, peer_id: &str) -> Option<Arc<MykoClient>> {
349 self.peer_clients
350 .get(peer_id)
351 .map(|entry| entry.value().clone())
352 }
353
354 pub fn peer_connection_status(&self, peer_id: &str) -> Option<ConnectionStatus> {
356 self.peer_client(peer_id)
357 .map(|client| client.get_connection_status_sync())
358 }
359
360 pub fn peer_clients_tick(&self) -> Cell<u64, CellImmutable> {
362 self.peer_clients_tick.clone().lock()
363 }
364
365 pub fn peer_client_count(&self) -> usize {
367 self.peer_clients.len()
368 }
369
370 pub fn persist_health(&self) -> Arc<PersistHealth> {
372 self.persisters.default_health()
373 }
374
375 pub fn query_cache_len(&self) -> usize {
377 self.query_cache.len()
378 }
379
380 pub fn view_cache_len(&self) -> usize {
382 self.view_cache.len()
383 }
384
385 pub fn report_cache_len(&self) -> usize {
387 self.report_cache.len()
388 }
389
390 pub fn report_cache_live_count(&self) -> usize {
392 self.report_cache
393 .iter()
394 .filter(|entry| entry.value().is_alive())
395 .count()
396 }
397
398 pub fn query_cache_live_count(&self) -> usize {
400 self.query_cache
401 .iter()
402 .filter(|entry| entry.value().weak.upgrade().is_some())
403 .count()
404 }
405
406 pub fn view_cache_live_count(&self) -> usize {
408 self.view_cache
409 .iter()
410 .filter(|entry| entry.value().weak.upgrade().is_some())
411 .count()
412 }
413
414 pub fn sweep_dead_cache_entries(&self) {
416 self.query_cache
417 .retain(|_, entry| entry.weak.upgrade().is_some());
418 self.view_cache
419 .retain(|_, entry| entry.weak.upgrade().is_some());
420 self.report_cache.retain(|_, entry| entry.is_alive());
421 }
422
423 pub fn parse_item(
433 &self,
434 entity_type: &str,
435 json: serde_json::Value,
436 ) -> Option<Arc<dyn AnyItem>> {
437 let parse = self.handler_registry.get_item_parser(entity_type)?;
438 parse(json).ok()
439 }
440
441 pub fn set<T>(&self, entity: &T) -> Result<(), PersistError>
449 where
450 T: Eventable + 'static,
451 {
452 self.set_with_origin(entity, Origin::Local)
453 }
454
455 #[deprecated(note = "EventOptions is internal plumbing; use `set` instead")]
461 pub fn set_with_options<T>(
462 &self,
463 entity: &T,
464 options: Option<EventOptions>,
465 ) -> Result<(), PersistError>
466 where
467 T: Eventable + 'static,
468 {
469 self.set_with_origin(entity, Origin::from_options(&options.unwrap_or_default()))
470 }
471
472 pub(crate) fn set_with_origin<T>(&self, entity: &T, origin: Origin) -> Result<(), PersistError>
475 where
476 T: Eventable + 'static,
477 {
478 let item: Arc<dyn AnyItem> = Arc::new(entity.clone());
479 self.reduce_one(&item, MEventType::SET);
480 self.apply_effects(std::slice::from_ref(&item), MEventType::SET, origin)
481 }
482
483 pub fn del<T>(&self, entity: &T) -> Result<(), PersistError>
487 where
488 T: Eventable + Clone + 'static,
489 {
490 self.del_with_origin(entity, Origin::Local)
491 }
492
493 #[deprecated(note = "EventOptions is internal plumbing; use `del` instead")]
497 pub fn del_with_options<T>(
498 &self,
499 entity: &T,
500 options: Option<EventOptions>,
501 ) -> Result<(), PersistError>
502 where
503 T: Eventable + Clone + 'static,
504 {
505 self.del_with_origin(entity, Origin::from_options(&options.unwrap_or_default()))
506 }
507
508 pub(crate) fn del_with_origin<T>(&self, entity: &T, origin: Origin) -> Result<(), PersistError>
509 where
510 T: Eventable + Clone + 'static,
511 {
512 let item: Arc<dyn AnyItem> = Arc::new(entity.clone());
513 self.reduce_one(&item, MEventType::DEL);
514 self.apply_effects(std::slice::from_ref(&item), MEventType::DEL, origin)
515 }
516
517 pub fn batch_set<T>(&self, entities: &[T]) -> Result<(), PersistError>
521 where
522 T: Eventable + Clone + 'static,
523 {
524 self.batch_set_with_origin(entities, Origin::Local)
525 }
526
527 #[deprecated(note = "EventOptions is internal plumbing; use `batch_set` instead")]
531 pub fn batch_set_with_options<T>(
532 &self,
533 entities: &[T],
534 options: Option<EventOptions>,
535 ) -> Result<(), PersistError>
536 where
537 T: Eventable + Clone + 'static,
538 {
539 self.batch_set_with_origin(entities, Origin::from_options(&options.unwrap_or_default()))
540 }
541
542 pub(crate) fn batch_set_with_origin<T>(
544 &self,
545 entities: &[T],
546 origin: Origin,
547 ) -> Result<(), PersistError>
548 where
549 T: Eventable + Clone + 'static,
550 {
551 if entities.is_empty() {
552 return Ok(());
553 }
554 let items: Vec<Arc<dyn AnyItem>> = entities
555 .iter()
556 .map(|e| Arc::new(e.clone()) as Arc<dyn AnyItem>)
557 .collect();
558 self.emit_grouped(&items, MEventType::SET, origin)
559 }
560
561 pub fn batch_del<T>(&self, entities: &[T]) -> Result<(), PersistError>
565 where
566 T: Eventable + Clone + 'static,
567 {
568 self.batch_del_with_origin(entities, Origin::Local)
569 }
570
571 #[deprecated(note = "EventOptions is internal plumbing; use `batch_del` instead")]
575 pub fn batch_del_with_options<T>(
576 &self,
577 entities: &[T],
578 options: Option<EventOptions>,
579 ) -> Result<(), PersistError>
580 where
581 T: Eventable + Clone + 'static,
582 {
583 self.batch_del_with_origin(entities, Origin::from_options(&options.unwrap_or_default()))
584 }
585
586 pub(crate) fn batch_del_with_origin<T>(
588 &self,
589 entities: &[T],
590 origin: Origin,
591 ) -> Result<(), PersistError>
592 where
593 T: Eventable + Clone + 'static,
594 {
595 if entities.is_empty() {
596 return Ok(());
597 }
598 let items: Vec<Arc<dyn AnyItem>> = entities
599 .iter()
600 .map(|e| Arc::new(e.clone()) as Arc<dyn AnyItem>)
601 .collect();
602 self.emit_grouped(&items, MEventType::DEL, origin)
603 }
604
605 pub fn set_dyn(&self, item: Arc<dyn AnyItem>) -> Result<(), PersistError> {
613 self.set_dyn_with_origin(item, Origin::Local)
614 }
615
616 #[deprecated(note = "EventOptions is internal plumbing; use `set_dyn` instead")]
620 pub fn set_dyn_with_options(
621 &self,
622 item: Arc<dyn AnyItem>,
623 options: Option<EventOptions>,
624 ) -> Result<(), PersistError> {
625 self.set_dyn_with_origin(item, Origin::from_options(&options.unwrap_or_default()))
626 }
627
628 pub(crate) fn set_dyn_with_origin(
629 &self,
630 item: Arc<dyn AnyItem>,
631 origin: Origin,
632 ) -> Result<(), PersistError> {
633 self.reduce_one(&item, MEventType::SET);
634 self.apply_effects(std::slice::from_ref(&item), MEventType::SET, origin)
635 }
636
637 pub fn batch_set_dyn(&self, items: &[Arc<dyn AnyItem>]) -> Result<(), PersistError> {
639 self.batch_set_dyn_with_origin(items, Origin::Local)
640 }
641
642 #[deprecated(note = "EventOptions is internal plumbing; use `batch_set_dyn` instead")]
646 pub fn batch_set_dyn_with_options(
647 &self,
648 items: &[Arc<dyn AnyItem>],
649 options: Option<EventOptions>,
650 ) -> Result<(), PersistError> {
651 self.batch_set_dyn_with_origin(items, Origin::from_options(&options.unwrap_or_default()))
652 }
653
654 pub(crate) fn batch_set_dyn_with_origin(
655 &self,
656 items: &[Arc<dyn AnyItem>],
657 origin: Origin,
658 ) -> Result<(), PersistError> {
659 self.emit_grouped(items, MEventType::SET, origin)
660 }
661
662 pub fn del_dyn(&self, item: Arc<dyn AnyItem>) -> Result<(), PersistError> {
666 self.del_dyn_with_origin(item, Origin::Local)
667 }
668
669 #[deprecated(note = "EventOptions is internal plumbing; use `del_dyn` instead")]
673 pub fn del_dyn_with_options(
674 &self,
675 item: Arc<dyn AnyItem>,
676 options: Option<EventOptions>,
677 ) -> Result<(), PersistError> {
678 self.del_dyn_with_origin(item, Origin::from_options(&options.unwrap_or_default()))
679 }
680
681 pub(crate) fn del_dyn_with_origin(
682 &self,
683 item: Arc<dyn AnyItem>,
684 origin: Origin,
685 ) -> Result<(), PersistError> {
686 self.reduce_one(&item, MEventType::DEL);
687 self.apply_effects(std::slice::from_ref(&item), MEventType::DEL, origin)
688 }
689
690 pub fn batch_del_dyn(&self, items: &[Arc<dyn AnyItem>]) -> Result<(), PersistError> {
692 self.batch_del_dyn_with_origin(items, Origin::Local)
693 }
694
695 #[deprecated(note = "EventOptions is internal plumbing; use `batch_del_dyn` instead")]
699 pub fn batch_del_dyn_with_options(
700 &self,
701 items: &[Arc<dyn AnyItem>],
702 options: Option<EventOptions>,
703 ) -> Result<(), PersistError> {
704 self.batch_del_dyn_with_origin(items, Origin::from_options(&options.unwrap_or_default()))
705 }
706
707 pub(crate) fn batch_del_dyn_with_origin(
708 &self,
709 items: &[Arc<dyn AnyItem>],
710 origin: Origin,
711 ) -> Result<(), PersistError> {
712 self.emit_grouped(items, MEventType::DEL, origin)
713 }
714
715 pub fn del_by_id(&self, entity_type: &str, id: &str) -> Result<(), PersistError> {
722 self.del_by_id_with_origin(entity_type, id, Origin::Local)
723 }
724
725 #[deprecated(note = "EventOptions is internal plumbing; use `del_by_id` instead")]
729 pub fn del_by_id_with_options(
730 &self,
731 entity_type: &str,
732 id: &str,
733 options: Option<EventOptions>,
734 ) -> Result<(), PersistError> {
735 self.del_by_id_with_origin(
736 entity_type,
737 id,
738 Origin::from_options(&options.unwrap_or_default()),
739 )
740 }
741
742 pub(crate) fn del_by_id_with_origin(
743 &self,
744 entity_type: &str,
745 id: &str,
746 origin: Origin,
747 ) -> Result<(), PersistError> {
748 let id_arc: Arc<str> = id.into();
749
750 let existing = self
751 .registry
752 .get(entity_type)
753 .and_then(|store| store.get(&id_arc).get());
754
755 crate::server::entity_set_stats::record_del(entity_type);
756
757 self.registry.get_or_create(entity_type).remove(&id_arc);
759
760 self.search_index.remove_entity(entity_type, id);
762
763 if origin.should_produce() {
765 if let Some(item) = existing {
766 self.produce_del_dyn(&item)?;
767 } else {
768 log::warn!(
769 "del_by_id could not persist DEL without full entity: {}:{}",
770 entity_type,
771 id
772 );
773 }
774 }
775
776 log::trace!("Published DEL {}:{}", entity_type, id);
777 Ok(())
778 }
779
780 pub fn apply_event(&self, event: MEvent) -> Result<bool, PersistError> {
784 Ok(self.apply_event_batch(vec![event])? == 1)
785 }
786
787 pub fn apply_event_batch(&self, events: Vec<MEvent>) -> Result<usize, PersistError> {
792 if events.is_empty() {
793 return Ok(0);
794 }
795
796 let mut accepted = 0usize;
797 let mut immediate_events = Vec::new();
798 let mut buffered_by_type: HashMap<Arc<str>, (u64, Vec<MEvent>)> = HashMap::new();
799
800 for event in events {
801 match self
802 .handler_registry
803 .get_item_buffer_policy(&event.item_type)
804 {
805 IngestBufferPolicy::None => immediate_events.push(event),
806 IngestBufferPolicy::TimeWindow { window_ms } => {
807 let entity_type: Arc<str> = event.item_type.clone().into();
808 buffered_by_type
809 .entry(entity_type)
810 .or_insert_with(|| (window_ms, Vec::new()))
811 .1
812 .push(event);
813 }
814 }
815 }
816
817 if !immediate_events.is_empty() {
818 accepted += self.apply_event_batch_immediate(immediate_events)?;
819 }
820
821 for (entity_type, (window_ms, buffered_events)) in buffered_by_type {
822 accepted += buffered_events.len();
823 self.enqueue_buffered_events(entity_type, window_ms, buffered_events);
824 }
825
826 Ok(accepted)
827 }
828
829 fn apply_event_batch_immediate(&self, events: Vec<MEvent>) -> Result<usize, PersistError> {
830 if events.is_empty() {
831 return Ok(0);
832 }
833 let input_len = events.len();
834
835 let mut set_items: Vec<Arc<dyn AnyItem>> = Vec::new();
836 let mut del_items: Vec<Arc<dyn AnyItem>> = Vec::new();
837
838 for event in events {
839 let change = event.change_type;
840 let item_type = event.item_type;
841 let item_value = event.item;
842 let Some(item) = self.parse_item(&item_type, item_value) else {
843 log::warn!("Unknown entity type or parse error for ingest: {item_type}");
844 continue;
845 };
846 match change {
847 MEventType::SET => set_items.push(item),
848 MEventType::DEL => del_items.push(item),
849 }
850 }
851
852 let applied = set_items.len() + del_items.len();
853 if applied == 0 {
854 return Ok(0);
855 }
856
857 log::trace!(
858 target: "myko::server::context",
859 "apply_event_batch parsed: input_events={} sets={} dels={}",
860 input_len,
861 set_items.len(),
862 del_items.len()
863 );
864
865 self.emit_grouped(&set_items, MEventType::SET, Origin::Local)?;
868 self.emit_grouped(&del_items, MEventType::DEL, Origin::Local)?;
869
870 Ok(applied)
871 }
872
873 fn ingest_buffer_for(&self, entity_type: Arc<str>) -> Arc<BufferedIngestType> {
874 self.ingest_buffers
875 .entry(entity_type)
876 .or_insert_with(|| Arc::new(BufferedIngestType::new()))
877 .clone()
878 }
879
880 fn enqueue_buffered_events(&self, entity_type: Arc<str>, window_ms: u64, events: Vec<MEvent>) {
881 let buffer = self.ingest_buffer_for(entity_type.clone());
882 let should_schedule = {
883 let Ok(mut state) = buffer.state.lock() else {
884 log::error!(
885 "Could not acquire ingest buffer lock for entity_type={}",
886 entity_type
887 );
888 if let Err(e) = self.apply_event_batch_immediate(events) {
889 log::error!("Failed to apply buffered events for {}: {}", entity_type, e);
890 }
891 return;
892 };
893
894 state.events.extend(events);
895 if state.flush_scheduled {
896 false
897 } else {
898 state.flush_scheduled = true;
899 true
900 }
901 };
902
903 if !should_schedule {
904 return;
905 }
906
907 let ctx = self.clone();
908 thread::spawn(move || {
909 thread::sleep(Duration::from_millis(window_ms));
910 ctx.flush_buffered_events_for_type(&entity_type);
911 });
912 }
913
914 fn flush_buffered_events_for_type(&self, entity_type: &Arc<str>) -> usize {
915 let Some(buffer) = self
916 .ingest_buffers
917 .get(entity_type.as_ref())
918 .map(|entry| entry.clone())
919 else {
920 return 0;
921 };
922
923 let events = {
924 let Ok(mut state) = buffer.state.lock() else {
925 log::error!(
926 "Could not acquire ingest buffer lock for flush entity_type={}",
927 entity_type
928 );
929 return 0;
930 };
931
932 state.flush_scheduled = false;
933 if state.events.is_empty() {
934 return 0;
935 }
936
937 std::mem::take(&mut state.events)
938 };
939
940 log::trace!(
941 target: "myko::server::context",
942 "flush_buffered_events entity_type={} count={}",
943 entity_type,
944 events.len()
945 );
946
947 match self.apply_event_batch_immediate(events) {
948 Ok(count) => count,
949 Err(e) => {
950 log::error!("Failed to flush buffered events for {}: {}", entity_type, e);
951 0
952 }
953 }
954 }
955
956 #[cfg(test)]
957 fn flush_all_buffered_events(&self) -> usize {
958 let entity_types: Vec<Arc<str>> = self
959 .ingest_buffers
960 .iter()
961 .map(|entry| entry.key().clone())
962 .collect();
963
964 entity_types
965 .into_iter()
966 .map(|entity_type| self.flush_buffered_events_for_type(&entity_type))
967 .sum()
968 }
969
970 fn reduce_one(&self, item: &Arc<dyn AnyItem>, change: MEventType) {
979 let entity_type = item.entity_type();
980 match change {
981 MEventType::SET => {
982 crate::server::entity_set_stats::record_set(entity_type);
983 self.registry
984 .get_or_create(entity_type)
985 .insert(item.id(), item.clone());
986 }
987 MEventType::DEL => {
988 crate::server::entity_set_stats::record_del(entity_type);
989 self.registry.get_or_create(entity_type).remove(&item.id());
990 }
991 }
992 }
993
994 fn emit_grouped(
1003 &self,
1004 items: &[Arc<dyn AnyItem>],
1005 change: MEventType,
1006 origin: Origin,
1007 ) -> Result<(), PersistError> {
1008 if items.is_empty() {
1009 return Ok(());
1010 }
1011
1012 let mut by_type: std::collections::BTreeMap<&'static str, Vec<Arc<dyn AnyItem>>> =
1013 std::collections::BTreeMap::new();
1014 for item in items {
1015 by_type
1016 .entry(item.entity_type())
1017 .or_default()
1018 .push(item.clone());
1019 }
1020
1021 for (entity_type, group) in &by_type {
1024 let store = self.registry.get_or_create(entity_type);
1025 match change {
1026 MEventType::SET => {
1027 let mut entries: Vec<(Arc<str>, Arc<dyn AnyItem>)> =
1028 Vec::with_capacity(group.len());
1029 for item in group {
1030 crate::server::entity_set_stats::record_set(entity_type);
1031 entries.push((item.id(), item.clone()));
1032 }
1033 store.insert_many(entries);
1034 }
1035 MEventType::DEL => {
1036 let mut ids: Vec<Arc<str>> = Vec::with_capacity(group.len());
1037 for item in group {
1038 crate::server::entity_set_stats::record_del(entity_type);
1039 ids.push(item.id());
1040 }
1041 store.remove_many(ids);
1042 }
1043 }
1044 }
1045
1046 for group in by_type.values() {
1048 self.apply_effects(group, change, origin)?;
1049 }
1050 Ok(())
1051 }
1052
1053 fn apply_effects(
1062 &self,
1063 items: &[Arc<dyn AnyItem>],
1064 change: MEventType,
1065 origin: Origin,
1066 ) -> Result<(), PersistError> {
1067 match change {
1069 MEventType::SET => {
1070 for item in items {
1071 self.search_index.index_item(item);
1072 }
1073 }
1074 MEventType::DEL => {
1075 for item in items {
1076 self.search_index
1077 .remove_entity(item.entity_type(), &item.id());
1078 }
1079 }
1080 }
1081
1082 if origin.should_cascade(change) {
1084 match change {
1085 MEventType::SET => {
1086 for item in items {
1087 self.relationship_manager.forward_set(item.clone(), self)?;
1088 }
1089 }
1090 MEventType::DEL => self.relationship_manager.forward_del_batch(items, self)?,
1091 }
1092 }
1093
1094 if origin.should_produce() {
1096 match change {
1097 MEventType::SET => {
1098 for item in items {
1099 self.produce_set_dyn(item)?;
1100 }
1101 }
1102 MEventType::DEL => {
1103 for item in items {
1104 self.produce_del_dyn(item)?;
1105 }
1106 }
1107 }
1108 }
1109
1110 Ok(())
1111 }
1112
1113 fn produce_del_dyn(&self, item: &Arc<dyn AnyItem>) -> Result<(), PersistError> {
1118 if let Some(persister) = self.persisters.resolve(item.entity_type()) {
1119 let event = MEvent::del_from_any(item, &self.host_id.to_string());
1120 persister.persist(event)?;
1121 }
1122 if let Some(sink) = &self.event_sink {
1123 let event = MEvent::del_from_any(item, &self.host_id.to_string());
1124 let _ = sink.send(event);
1125 }
1126 Ok(())
1127 }
1128
1129 fn produce_set_dyn(&self, item: &Arc<dyn AnyItem>) -> Result<(), PersistError> {
1130 if let Some(persister) = self.persisters.resolve(item.entity_type()) {
1131 let event = MEvent::set_from_value(
1132 item.entity_type(),
1133 item.to_value(),
1134 &self.host_id.to_string(),
1135 );
1136 persister.persist(event)?;
1137 }
1138 if let Some(sink) = &self.event_sink {
1139 let event = MEvent::set_from_value(
1140 item.entity_type(),
1141 item.to_value(),
1142 &self.host_id.to_string(),
1143 );
1144 let _ = sink.send(event);
1145 }
1146 Ok(())
1147 }
1148
1149 pub fn query_map<Q>(
1158 &self,
1159 query: Q,
1160 request: Arc<RequestContext>,
1161 ) -> CellMap<<Q::Item as WithTypedId>::Id, Arc<Q::Item>, CellImmutable>
1162 where
1163 Q: QueryParams + 'static,
1164 Q::Item: Eventable
1165 + WithId
1166 + WithTypedId
1167 + DeserializeOwned
1168 + Clone
1169 + std::fmt::Debug
1170 + Send
1171 + Sync
1172 + 'static,
1173 {
1174 let key = self.cache_key("query", Q::query_id_static().as_ref(), &query, &request);
1175 let untyped = self.query_map_untyped(query, request);
1177 if let Some(entry) = self.query_cache.get(&key)
1178 && let Some(typed) = entry.value().get_or_create_typed(|source| {
1179 typed_map_from_any_item_with_typed_id(source, "CellServerCtx::query_map")
1180 })
1181 {
1182 return typed;
1183 }
1184 self.query_cache
1186 .insert(key.clone(), MapCacheEntry::new(&untyped));
1187 let entry = self.query_cache.get(&key).expect("just re-inserted");
1188 entry
1189 .value()
1190 .get_or_create_typed(|source| {
1191 typed_map_from_any_item_with_typed_id(source, "CellServerCtx::query_map")
1192 })
1193 .expect("typed projection from freshly inserted entry")
1194 }
1195
1196 pub fn query_map_by_str<Q>(
1200 &self,
1201 query: Q,
1202 request: Arc<RequestContext>,
1203 ) -> CellMap<Arc<str>, Arc<Q::Item>, CellImmutable>
1204 where
1205 Q: QueryParams + 'static,
1206 Q::Item:
1207 Eventable + WithId + DeserializeOwned + Clone + std::fmt::Debug + Send + Sync + 'static,
1208 {
1209 let key = self.cache_key("query", Q::query_id_static().as_ref(), &query, &request);
1210 let untyped = self.query_map_untyped(query, request);
1211 if let Some(entry) = self.query_cache.get(&key)
1212 && let Some(typed) = entry.value().get_or_create_typed(|source| {
1213 typed_map_arc_from_any_item(source, "CellServerCtx::query_map_by_str")
1214 })
1215 {
1216 return typed;
1217 }
1218 self.query_cache
1220 .insert(key.clone(), MapCacheEntry::new(&untyped));
1221 let entry = self.query_cache.get(&key).expect("just re-inserted");
1222 entry
1223 .value()
1224 .get_or_create_typed(|source| {
1225 typed_map_arc_from_any_item(source, "CellServerCtx::query_map_by_str")
1226 })
1227 .expect("typed projection from freshly inserted entry")
1228 }
1229
1230 pub fn query_map_untyped<Q>(&self, query: Q, request: Arc<RequestContext>) -> FilteredCellMap
1249 where
1250 Q: QueryFactory + QueryHandler + QueryParams + Clone + Send + Sync + 'static,
1251 Q::Item: DeserializeOwned + Clone + std::fmt::Debug + Send + Sync + 'static,
1252 {
1253 let key = self.cache_key("query", Q::query_id_static().as_ref(), &query, &request);
1254
1255 if let Some(cell) = self.try_get_cached_query(&key) {
1257 return cell;
1258 }
1259
1260 let gate = self
1261 .compute_gates
1262 .entry(key.clone())
1263 .or_insert_with(|| Arc::new(std::sync::Mutex::new(())))
1264 .clone();
1265 let _lock = gate.lock().unwrap();
1266
1267 if let Some(cell) = self.try_get_cached_query(&key) {
1269 return cell;
1270 }
1271
1272 let query_req = QueryRequest::with_tx(query, request.tx.clone());
1273 let any_query: Arc<dyn crate::query::AnyQuery> = Arc::new(query_req);
1274
1275 let built = Q::cell_factory(
1276 any_query,
1277 self.registry.clone(),
1278 request,
1279 Some(Arc::new(self.clone())),
1280 )
1281 .expect("query cell factory should not fail for typed query");
1282 self.query_cache.insert(key, MapCacheEntry::new(&built));
1283 built
1284 }
1285
1286 fn try_get_cached_query(&self, key: &str) -> Option<FilteredCellMap> {
1287 let existing = self.query_cache.get(key)?;
1288 if let Some(shared) = existing.value().get() {
1289 return Some(shared);
1290 }
1291 drop(existing);
1292 self.query_cache.remove(key);
1293 None
1294 }
1295
1296 pub fn view_map_untyped<V>(&self, view: V, request: Arc<RequestContext>) -> FilteredViewCellMap
1298 where
1299 V: ViewFactory + Clone + Send + Sync + 'static,
1300 V::Item: DeserializeOwned + Clone + std::fmt::Debug + Send + Sync + 'static,
1301 {
1302 let key = self.cache_key("view", V::view_id_static().as_ref(), &view, &request);
1303
1304 if let Some(cell) = self.try_get_cached_view(&key) {
1306 return cell;
1307 }
1308
1309 let gate = self
1310 .compute_gates
1311 .entry(key.clone())
1312 .or_insert_with(|| Arc::new(std::sync::Mutex::new(())))
1313 .clone();
1314 let _lock = gate.lock().unwrap();
1315
1316 if let Some(cell) = self.try_get_cached_view(&key) {
1318 return cell;
1319 }
1320
1321 let view_req = crate::view::ViewRequest::with_tx(view, request.tx.clone());
1322 let any_view: Arc<dyn crate::view::AnyView> = Arc::new(view_req);
1323
1324 let built = V::cell_factory(
1325 any_view,
1326 self.registry.clone(),
1327 request,
1328 Arc::new(self.clone()),
1329 )
1330 .expect("view cell factory should not fail for typed view");
1331 self.view_cache.insert(key, MapCacheEntry::new(&built));
1332 built
1333 }
1334
1335 fn try_get_cached_view(&self, key: &str) -> Option<FilteredViewCellMap> {
1336 let existing = self.view_cache.get(key)?;
1337 if let Some(shared) = existing.value().get() {
1338 return Some(shared);
1339 }
1340 drop(existing);
1341 self.view_cache.remove(key);
1342 None
1343 }
1344
1345 pub fn view_map<V>(&self, view: V, request: Arc<RequestContext>) -> FilteredViewCellMap
1347 where
1348 V: ViewFactory + Clone + Send + Sync + 'static,
1349 V::Item: DeserializeOwned + Clone + std::fmt::Debug + Send + Sync + 'static,
1350 {
1351 self.view_map_untyped(view, request)
1352 }
1353
1354 pub fn view<V>(&self, view: V, request: Arc<RequestContext>) -> TypedViewCellMap<V::Item>
1356 where
1357 V: ViewFactory + Clone + Send + Sync + 'static,
1358 V::Item: DeserializeOwned + Clone + std::fmt::Debug + Send + Sync + 'static,
1359 {
1360 let key = self.cache_key("view", V::view_id_static().as_ref(), &view, &request);
1361 let _untyped = self.view_map_untyped(view, request);
1362 if let Some(entry) = self.view_cache.get(&key)
1363 && let Some(typed) = entry.value().get_or_create_typed(|source| {
1364 typed_map_arc_from_any_item(source, "CellServerCtx::view")
1365 })
1366 {
1367 return typed;
1368 }
1369 unreachable!("view_map_untyped just populated the cache")
1370 }
1371
1372 pub fn entity_snapshot<T>(&self, id: &<T as WithTypedId>::Id) -> Option<Arc<T>>
1374 where
1375 T: Eventable + WithTypedId + Send + Sync + 'static,
1376 <T as WithTypedId>::Id: hyphae::IdFor<T, MapKey = Arc<str>>,
1377 {
1378 let store = self.registry.get_or_create(T::entity_name_static());
1379 let map_key = id.map_key();
1380 let item = store.get_value(&map_key)?;
1381 Some(downcast_any_item_arc::<T>(
1382 &item,
1383 "CellServerCtx::entity_snapshot",
1384 ))
1385 }
1386
1387 pub fn entity_snapshots<T>(&self) -> Vec<Arc<T>>
1389 where
1390 T: Eventable + WithTypedId + Send + Sync + 'static,
1391 <T as WithTypedId>::Id: hyphae::IdFor<T, MapKey = Arc<str>>,
1392 {
1393 let store = self.registry.get_or_create(T::entity_name_static());
1394 store
1395 .snapshot()
1396 .into_iter()
1397 .map(|(_, item)| downcast_any_item_arc::<T>(&item, "CellServerCtx::entity_snapshots"))
1398 .collect()
1399 }
1400
1401 pub fn entity_snapshots_by_id<T>(
1403 &self,
1404 ids: impl IntoIterator<Item = <T as WithTypedId>::Id>,
1405 ) -> Vec<Arc<T>>
1406 where
1407 T: Eventable + WithTypedId + Send + Sync + 'static,
1408 <T as WithTypedId>::Id: hyphae::IdFor<T, MapKey = Arc<str>>,
1409 {
1410 ids.into_iter()
1411 .filter_map(|id| self.entity_snapshot::<T>(&id))
1412 .collect()
1413 }
1414
1415 pub fn query_snapshot<Q>(&self, query: Q, request: Arc<RequestContext>) -> Vec<Arc<Q::Item>>
1421 where
1422 Q: QueryHandler + QueryParams + Clone + Send + Sync + 'static,
1423 Q::Item: DeserializeOwned + Clone + std::fmt::Debug + Send + Sync + 'static,
1424 {
1425 let query_item_type = Q::query_item_type_static();
1426 let store = self.registry.get_or_create(&query_item_type);
1427
1428 let query_context = Arc::new(QueryContext {
1429 req: request.clone(),
1430 });
1431 let query = Arc::new(query);
1432
1433 store
1434 .snapshot()
1435 .into_iter()
1436 .filter_map(|(_, item)| {
1437 let typed_item =
1438 downcast_any_item_arc::<Q::Item>(&item, "CellServerCtx::query_snapshot");
1439 let ctx = QueryTestCtx {
1440 item: typed_item.clone(),
1441 query: query.clone(),
1442 query_context: query_context.clone(),
1443 };
1444 if Q::test_entity(ctx) {
1445 Some(typed_item)
1446 } else {
1447 None
1448 }
1449 })
1450 .collect()
1451 }
1452
1453 pub fn report<R>(
1454 &self,
1455 report: R,
1456 request: Arc<RequestContext>,
1457 ) -> Cell<Arc<R::Output>, CellImmutable>
1458 where
1459 R: ReportHandler + ReportId + CacheKey + Clone + serde::Serialize + 'static,
1460 {
1461 let key = self.cache_key("report", report.report_id().as_ref(), &report, &request);
1462 let report_id = report.report_id();
1463
1464 if let Some(cell) = self.try_get_cached_report::<R>(&key) {
1466 crate::server::report_cache_stats::record_hit(&report_id);
1467 log::trace!(
1468 target: "myko::server::context::report_cache",
1469 "report_cache HIT report_id={} key={}",
1470 report_id,
1471 key,
1472 );
1473 return cell;
1474 }
1475
1476 let gate = self
1479 .compute_gates
1480 .entry(key.clone())
1481 .or_insert_with(|| Arc::new(std::sync::Mutex::new(())))
1482 .clone();
1483 let _lock = gate.lock().unwrap();
1484
1485 if let Some(cell) = self.try_get_cached_report::<R>(&key) {
1487 crate::server::report_cache_stats::record_hit_after_gate(&report_id);
1488 log::trace!(
1489 target: "myko::server::context::report_cache",
1490 "report_cache HIT_AFTER_GATE report_id={} key={}",
1491 report_id,
1492 key,
1493 );
1494 return cell;
1495 }
1496
1497 if log::log_enabled!(target: "myko::server::context::report_cache", log::Level::Trace) {
1501 let payload = serde_json::to_string(&report)
1502 .unwrap_or_else(|e| format!("<serialize error: {e}>"));
1503 log::trace!(
1504 target: "myko::server::context::report_cache",
1505 "report_cache MISS_COMPUTE report_id={} key={} payload={}",
1506 report_id,
1507 key,
1508 payload,
1509 );
1510 }
1511
1512 let nested_ctx = ReportContext::new(request, Arc::new(self.clone()));
1513 let built = report.compute(nested_ctx).materialize();
1517 self.report_cache
1518 .insert(key.clone(), Arc::new(ReportCacheEntry::new(&built)));
1519
1520 crate::server::report_cache_stats::record_miss(&report_id);
1521
1522 built
1523 }
1524
1525 fn try_get_cached_report<R>(&self, key: &str) -> Option<Cell<Arc<R::Output>, CellImmutable>>
1527 where
1528 R: ReportHandler + 'static,
1529 {
1530 let existing = self.report_cache.get(key)?;
1531 if let Some(entry) = existing
1532 .value()
1533 .as_any()
1534 .downcast_ref::<ReportCacheEntry<Arc<R::Output>>>()
1535 && let Some(shared) = entry.get()
1536 {
1537 return Some(shared);
1538 }
1539 drop(existing);
1541 self.report_cache.remove(key);
1542 None
1543 }
1544
1545 pub fn new_server_transaction(&self) -> Arc<RequestContext> {
1546 Arc::new(RequestContext {
1547 tx: Arc::<str>::from(Uuid::new_v4().to_string()),
1548 client_id: None,
1549 lineage: vec![],
1550 host_id: self.host_id,
1551 created_at: chrono::Utc::now().to_string(),
1552 windback: None,
1553 })
1554 }
1555}
1556
1557impl std::fmt::Debug for CellServerCtx {
1558 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1559 f.debug_struct("CellServerCtx").finish()
1560 }
1561}
1562
1563#[cfg(test)]
1564mod tests {
1565 use std::sync::Arc;
1566
1567 use serde::{Deserialize, Serialize};
1568 use serde_json::json;
1569 use uuid::Uuid;
1570
1571 use super::CellServerCtx;
1572 use crate::{
1573 common::with_id::WithId,
1574 core::item::{
1575 AnyItem, Eventable, IngestBufferPolicy, IngestBufferRegistration, ItemRegistration,
1576 },
1577 hyphae::Gettable,
1578 search::SearchIndex,
1579 server::{HandlerRegistry, RelationshipManager, persister::PersisterRouter},
1580 store::StoreRegistry,
1581 wire::{MEvent, MEventType},
1582 };
1583
1584 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1585 struct BufferedTestItem {
1586 id: Arc<str>,
1587 value: i32,
1588 }
1589
1590 impl WithId for BufferedTestItem {
1591 fn id(&self) -> Arc<str> {
1592 self.id.clone()
1593 }
1594 }
1595
1596 impl AnyItem for BufferedTestItem {
1597 fn as_any(&self) -> &dyn std::any::Any {
1598 self
1599 }
1600
1601 fn entity_type(&self) -> &'static str {
1602 "BufferedTestItem"
1603 }
1604
1605 fn equals(&self, other: &dyn AnyItem) -> bool {
1606 other
1607 .as_any()
1608 .downcast_ref::<Self>()
1609 .map(|typed| self == typed)
1610 .unwrap_or(false)
1611 }
1612 }
1613
1614 impl Eventable for BufferedTestItem {
1615 const ENTITY_NAME_STATIC: &'static str = "BufferedTestItem";
1616 }
1617
1618 inventory::submit! {
1619 ItemRegistration {
1620 entity_type: "BufferedTestItem",
1621 crate_name: env!("CARGO_PKG_NAME"),
1622 parse: BufferedTestItem::parse,
1623 parse_bytes: BufferedTestItem::parse_bytes,
1624 serialize_json: |any| {
1625 let typed = any.as_any().downcast_ref::<BufferedTestItem>().unwrap();
1626 ::serde_json::value::to_raw_value(typed)
1627 },
1628 }
1629 }
1630
1631 inventory::submit! {
1632 IngestBufferRegistration {
1633 entity_type: "BufferedTestItem",
1634 policy: IngestBufferPolicy::TimeWindow { window_ms: 60_000 },
1635 }
1636 }
1637
1638 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1639 struct ImmediateTestItem {
1640 id: Arc<str>,
1641 value: i32,
1642 }
1643
1644 impl WithId for ImmediateTestItem {
1645 fn id(&self) -> Arc<str> {
1646 self.id.clone()
1647 }
1648 }
1649
1650 impl AnyItem for ImmediateTestItem {
1651 fn as_any(&self) -> &dyn std::any::Any {
1652 self
1653 }
1654
1655 fn entity_type(&self) -> &'static str {
1656 "ImmediateTestItem"
1657 }
1658
1659 fn equals(&self, other: &dyn AnyItem) -> bool {
1660 other
1661 .as_any()
1662 .downcast_ref::<Self>()
1663 .map(|typed| self == typed)
1664 .unwrap_or(false)
1665 }
1666 }
1667
1668 impl Eventable for ImmediateTestItem {
1669 const ENTITY_NAME_STATIC: &'static str = "ImmediateTestItem";
1670 }
1671
1672 inventory::submit! {
1673 ItemRegistration {
1674 entity_type: "ImmediateTestItem",
1675 crate_name: env!("CARGO_PKG_NAME"),
1676 parse: ImmediateTestItem::parse,
1677 parse_bytes: ImmediateTestItem::parse_bytes,
1678 serialize_json: |any| {
1679 let typed = any.as_any().downcast_ref::<ImmediateTestItem>().unwrap();
1680 ::serde_json::value::to_raw_value(typed)
1681 },
1682 }
1683 }
1684
1685 fn make_ctx() -> CellServerCtx {
1686 CellServerCtx::new(
1687 Uuid::new_v4(),
1688 Arc::new(StoreRegistry::new()),
1689 Arc::new(HandlerRegistry::new()),
1690 Arc::new(RelationshipManager::new()),
1691 Arc::new(PersisterRouter::default()),
1692 Arc::new(SearchIndex::new()),
1693 Arc::new(dashmap::DashMap::new()),
1694 None,
1695 None,
1696 )
1697 }
1698
1699 #[test]
1700 fn apply_event_batch_keeps_default_entities_immediate() {
1701 let ctx = make_ctx();
1702 let applied = ctx
1703 .apply_event_batch(vec![MEvent {
1704 item: json!({
1705 "id": "immediate-1",
1706 "value": 7,
1707 }),
1708 change_type: MEventType::SET,
1709 item_type: "ImmediateTestItem".to_string(),
1710 created_at: "2026-03-12T00:00:00Z".to_string(),
1711 tx: "tx-immediate".to_string(),
1712 source_id: Some("test".to_string()),
1713 }])
1714 .expect("apply_event_batch should succeed");
1715
1716 assert_eq!(applied, 1);
1717 let store = ctx.registry.get_or_create("ImmediateTestItem");
1718 assert!(store.get(&Arc::<str>::from("immediate-1")).get().is_some());
1719 }
1720
1721 #[test]
1722 fn apply_event_batch_buffers_opted_in_entities() {
1723 let ctx = make_ctx();
1724 let applied = ctx
1725 .apply_event_batch(vec![MEvent {
1726 item: json!({
1727 "id": "buffered-1",
1728 "value": 42,
1729 }),
1730 change_type: MEventType::SET,
1731 item_type: "BufferedTestItem".to_string(),
1732 created_at: "2026-03-12T00:00:00Z".to_string(),
1733 tx: "tx-buffered".to_string(),
1734 source_id: Some("test".to_string()),
1735 }])
1736 .expect("apply_event_batch should succeed");
1737
1738 assert_eq!(applied, 1);
1739 let store = ctx.registry.get_or_create("BufferedTestItem");
1740 assert!(store.get(&Arc::<str>::from("buffered-1")).get().is_none());
1741
1742 let flushed = ctx.flush_all_buffered_events();
1743 assert_eq!(flushed, 1);
1744 assert!(store.get(&Arc::<str>::from("buffered-1")).get().is_some());
1745 }
1746}