Skip to main content

myko/server/
context.rs

1//! Server context for the cell-based server.
2//!
3//! Provides modules (like PeerRegistry) with the ability to:
4//! - Run reactive queries (like GetPeerServers)
5//! - Publish entities (Reduce → Relationships → Persist)
6//! - Access server identity (host_id)
7
8use 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/// Where a mutation came from. This is the single policy point for the apply
54/// pipeline's loop-safety: it determines whether a mutation should run
55/// relationship cascades. (Both origins produce.)
56///
57/// It replaces the scattered per-call loop-guard flag checks; `from_options`
58/// bridges the legacy `EventOptions::prevent_relationship_updates` flag to an
59/// `Origin` for the deprecated `*_with_options` methods.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) enum Origin {
62    /// A command handler / server module emitting a new mutation here (also a
63    /// client event ingested over the WebSocket). Cascades and produces.
64    Local,
65    /// A relationship cascade product — a consequence of another mutation here.
66    Cascade,
67    /// An event replicated from a peer server: already durable and already
68    /// cascaded at its origin. Applied to the store + search index only — it must
69    /// not cascade (the origin already replicated its cascade products) and must
70    /// not produce (which would echo it back around the peer mesh).
71    ///
72    /// Reserved: nothing constructs this right now (peer-origin tracking has been
73    /// moved off the wire). The wiring is kept for when that mechanism returns.
74    #[allow(dead_code)]
75    Remote,
76}
77
78impl Origin {
79    /// Bridge the legacy `EventOptions::prevent_relationship_updates` flag to an
80    /// `Origin`: cascade products set it (→ `Cascade`); everything else `Local`.
81    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    /// Whether this origin's mutations should run relationship cascades.
90    ///
91    /// - `Local` mutations always cascade.
92    /// - `Cascade` products are gated on the change type: a **DEL** product
93    ///   keeps cascading, so a deleted parent's children, grandchildren, … are
94    ///   all removed at runtime (not just one level, and not deferred to the
95    ///   boot-time orphan sweep). The owns_many array-fixup **SET** product must
96    ///   not descend structurally.
97    /// - `Remote` never cascades (the origin already replicated its products).
98    ///
99    /// Transitive DEL cascade terminates without a depth counter or visited set:
100    /// reduce runs before cascade, so each node is removed from the store before
101    /// its own cascade runs. The store is therefore a monotonically shrinking
102    /// visited-set — a cyclic schema (A→B→A) finds nothing the second time, and
103    /// a cascade-deleted child cannot resurrect its already-removed parent.
104    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    /// Whether this origin's mutations should be produced to persisters/sink.
113    ///
114    /// `Remote` events are already durable and already cascaded at their origin,
115    /// so re-producing them would echo them back around the peer mesh. Everything
116    /// else produces; per-type durability is the persister router's job
117    /// (`BlackholePersister`), not a per-event flag.
118    fn should_produce(self) -> bool {
119        self != Origin::Remote
120    }
121}
122
123/// Weak-ref report cache entry. The cell stays alive as long as someone is
124/// subscribed to it. When all subscribers drop, the weak ref fails to upgrade
125/// and the next request recomputes.
126trait 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    /// Lazily-created typed projections keyed by `TypeId` of the output
166    /// `CellMap<K, V>`. Each value is a type-erased weak cell map that can be
167    /// downcast back to the concrete `WeakCellMap<K, V>`.
168    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    /// Get or create a typed projection of this untyped map.
202    ///
203    /// `F` is called at most once per projection type to create the typed map
204    /// from the untyped source. Subsequent calls return the cached projection.
205    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        // Try to upgrade an existing weak ref
215        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            // Dead — remove stale entry
222            typed.remove(&type_key);
223        }
224
225        // Create from the untyped source
226        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/// Context providing capabilities to server modules.
234///
235/// This is the cell-based equivalent of `MykoServerCtx`, providing:
236/// - Entity store access (read-only, via queries)
237/// - Event publishing (Reduce → Relationships → Persist)
238/// - Server identity
239#[derive(Clone)]
240pub struct CellServerCtx {
241    /// Unique identifier for this server instance
242    pub host_id: Uuid,
243    /// Store registry for entity access
244    pub registry: Arc<StoreRegistry>,
245    /// Handler registry for item parsers
246    pub handler_registry: Arc<HandlerRegistry>,
247    /// Relationship manager - handles cascades
248    relationship_manager: Arc<RelationshipManager>,
249    /// Persister routing (default + per-entity overrides)
250    persisters: Arc<PersisterRouter>,
251    /// Full-text search index
252    search_index: Arc<SearchIndex>,
253    /// Live peer clients by peer server id (populated by peer registry).
254    peer_clients: Arc<DashMap<Arc<str>, Arc<MykoClient>>>,
255    /// Monotonic tick bumped on peer client register/unregister.
256    peer_clients_tick: Cell<u64, CellMutable>,
257    /// Optional event sink used to fan out applied events to saga runtimes.
258    event_sink: Option<flume::Sender<MEvent>>,
259    // AHash on the cache + dispatch maps below: every subscriber and every
260    // applied event goes through one or more of these. Bench: ~1.6× faster
261    // DashMap lookups vs default SipHash.
262    /// Top-level cache for reactive query maps.
263    query_cache: Arc<DashMap<String, MapCacheEntry, ahash::RandomState>>,
264    /// Top-level cache for reactive view maps.
265    view_cache: Arc<DashMap<String, MapCacheEntry, ahash::RandomState>>,
266    /// Top-level cache for reactive report cells with short-lived strong retention.
267    report_cache: Arc<DashMap<String, Arc<dyn ReportCacheEntryDyn>, ahash::RandomState>>,
268    /// Per-key coordination for concurrent report/query/view computation.
269    /// Prevents duplicate computation when multiple threads request the same key.
270    compute_gates: Arc<DashMap<String, Arc<std::sync::Mutex<()>>, ahash::RandomState>>,
271    /// Optional ingest buffers keyed by entity type for opt-in burst smoothing.
272    ingest_buffers: Arc<DashMap<Arc<str>, Arc<BufferedIngestType>, ahash::RandomState>>,
273    /// Optional history replay provider for point-in-time snapshots.
274    history_replay: Option<Arc<dyn crate::server::HistoryReplayProvider>>,
275}
276
277impl CellServerCtx {
278    /// Create a new server context.
279    #[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    /// Get the search index.
322    pub fn search_index(&self) -> &Arc<SearchIndex> {
323        &self.search_index
324    }
325
326    /// Get the history replay provider, if configured.
327    pub fn history_replay(&self) -> Option<&Arc<dyn crate::server::HistoryReplayProvider>> {
328        self.history_replay.as_ref()
329    }
330
331    /// Register or replace a live peer client for a server id.
332    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    /// Remove a live peer client for a server id.
340    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    /// Get a live peer client by server id, if present.
348    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    /// Get a peer's current connection status if the client is present.
355    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    /// Reactive tick that updates whenever peer client membership changes.
361    pub fn peer_clients_tick(&self) -> Cell<u64, CellImmutable> {
362        self.peer_clients_tick.clone().lock()
363    }
364
365    /// Number of currently tracked peer clients.
366    pub fn peer_client_count(&self) -> usize {
367        self.peer_clients.len()
368    }
369
370    /// Get the live persist health counters from the default persister.
371    pub fn persist_health(&self) -> Arc<PersistHealth> {
372        self.persisters.default_health()
373    }
374
375    /// Number of entries in the query cache (includes dead weak refs).
376    pub fn query_cache_len(&self) -> usize {
377        self.query_cache.len()
378    }
379
380    /// Number of entries in the view cache (includes dead weak refs).
381    pub fn view_cache_len(&self) -> usize {
382        self.view_cache.len()
383    }
384
385    /// Number of entries in the report cache (includes dead weak refs).
386    pub fn report_cache_len(&self) -> usize {
387        self.report_cache.len()
388    }
389
390    /// Count live (upgradeable) entries in the report cache.
391    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    /// Count live (upgradeable) entries in the query cache.
399    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    /// Count live (upgradeable) entries in the view cache.
407    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    /// Remove dead weak-ref entries from all caches.
415    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    /// Parse JSON to a typed entity using the registered item parser.
424    ///
425    /// Takes the `Value` by ownership so we don't pay a deep-clone of the
426    /// nested-enum tree on every applied event. Bench `from_value_with_clone`
427    /// shows the clone is ~136 ns/event on a typical entity payload — small
428    /// per event, but multiplied by the apply_event_batch hot path it's the
429    /// cheapest non-breaking win on the ingest path.
430    ///
431    /// Returns None if the entity type is not registered or parsing fails.
432    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    // ─────────────────────────────────────────────────────────────────────────
442    // Typed entity publishing (for server modules)
443    // ─────────────────────────────────────────────────────────────────────────
444
445    /// Publish an entity (SET) with default options.
446    ///
447    /// Default behavior: Reduce + Relationships + Persist
448    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    /// Publish an entity (SET) with options.
456    ///
457    /// **Deprecated.** `EventOptions` are internal loop-guard plumbing (cascade
458    /// and peer-replication markers) and must not be set by callers — use
459    /// [`set`](Self::set) instead.
460    #[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    /// Internal SET: typed reduce (direct `Arc` store insert) followed by the
473    /// shared `apply_effects` tail, gated by `origin`.
474    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    /// Delete an entity (DEL) with default options.
484    ///
485    /// Default behavior: Reduce + Relationships + Persist
486    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    /// Delete an entity (DEL) with options.
494    ///
495    /// **Deprecated.** `EventOptions` are internal plumbing; use [`del`](Self::del).
496    #[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    /// Publish a batch of entities (SET) with default options.
518    ///
519    /// Default behavior: Reduce + Relationships + Persist
520    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    /// Publish a batch of entities (SET) with shared options.
528    ///
529    /// **Deprecated.** `EventOptions` are internal plumbing; use [`batch_set`](Self::batch_set).
530    #[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    /// Publish a batch of entities (SET) with one grouped store insert.
543    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    /// Delete a batch of entities (DEL) with default options.
562    ///
563    /// Default behavior: Reduce + Relationships + Persist
564    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    /// Delete a batch of entities (DEL) with shared options.
572    ///
573    /// **Deprecated.** `EventOptions` are internal plumbing; use [`batch_del`](Self::batch_del).
574    #[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    /// Delete a batch of entities (DEL) with one grouped store remove.
587    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    // ─────────────────────────────────────────────────────────────────────────
606    // Dynamic item publishing (for parsed JSON)
607    // ─────────────────────────────────────────────────────────────────────────
608
609    /// Publish a dynamic item (SET) with default options.
610    ///
611    /// Default behavior: Reduce + Relationships + Persist
612    pub fn set_dyn(&self, item: Arc<dyn AnyItem>) -> Result<(), PersistError> {
613        self.set_dyn_with_origin(item, Origin::Local)
614    }
615
616    /// Publish a dynamic item (SET) with options.
617    ///
618    /// **Deprecated.** `EventOptions` are internal plumbing; use [`set_dyn`](Self::set_dyn).
619    #[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    /// Publish a batch of dynamic items (SET).
638    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    /// Publish a batch of dynamic items (SET) with shared options.
643    ///
644    /// **Deprecated.** `EventOptions` are internal plumbing; use [`batch_set_dyn`](Self::batch_set_dyn).
645    #[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    /// Delete a dynamic item (DEL) with default options.
663    ///
664    /// Default behavior: Reduce + Relationships + Persist
665    pub fn del_dyn(&self, item: Arc<dyn AnyItem>) -> Result<(), PersistError> {
666        self.del_dyn_with_origin(item, Origin::Local)
667    }
668
669    /// Delete a dynamic item (DEL) with options.
670    ///
671    /// **Deprecated.** `EventOptions` are internal plumbing; use [`del_dyn`](Self::del_dyn).
672    #[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    /// Publish a batch of dynamic items (DEL).
691    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    /// Publish a batch of dynamic items (DEL) with shared options.
696    ///
697    /// **Deprecated.** `EventOptions` are internal plumbing; use [`batch_del_dyn`](Self::batch_del_dyn).
698    #[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    /// Delete an entity by type/id and publish DEL even if the item is not present locally.
716    ///
717    /// This is useful for explicit tombstoning of entities (e.g. disconnected peers)
718    /// where we must ensure a DEL event is produced to durable backend.
719    ///
720    /// Note: relationship cascades require the full item and are therefore skipped here.
721    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    /// Delete an entity by type/id with options.
726    ///
727    /// **Deprecated.** `EventOptions` are internal plumbing; use [`del_by_id`](Self::del_by_id).
728    #[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        // Reduce: remove from store
758        self.registry.get_or_create(entity_type).remove(&id_arc);
759
760        // Search: remove from index
761        self.search_index.remove_entity(entity_type, id);
762
763        // Persist: produce unless this origin must not (e.g. a peer tombstone).
764        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    /// Apply a single wire event (parse -> reduce -> relationships -> persist).
781    ///
782    /// Returns `true` when the event was parsed and applied, `false` otherwise.
783    pub fn apply_event(&self, event: MEvent) -> Result<bool, PersistError> {
784        Ok(self.apply_event_batch(vec![event])? == 1)
785    }
786
787    /// Apply a batch of wire events with a single parse pass and grouped store updates.
788    ///
789    /// This reduces overhead versus calling `set_dyn`/`del_dyn` for each event individually.
790    /// Returns the number of successfully parsed/applied events.
791    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        // Ingested wire events are Local (cascade + produce); the shared batch
866        // path groups by type, reduces, then runs the cascade/produce tail.
867        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    // ─────────────────────────────────────────────────────────────────────────
971    // shared emission pipeline (batch is first-class; single is a thin wrapper)
972    // ─────────────────────────────────────────────────────────────────────────
973
974    /// Single-item store reduce — **no allocation**. Records the stat and applies
975    /// the store insert/remove for one item. Paired with
976    /// `apply_effects(slice::from_ref(&item), …)` by the single-item entry points
977    /// so a single mutation never allocates a Vec or groups by type.
978    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    /// The batch emission path (first-class). Groups `items` by entity type,
995    /// applies one grouped store reduce per type (a single store diff each) for
996    /// **all** groups before any cascade runs, then runs the shared
997    /// `apply_effects` tail per (same-type) group.
998    ///
999    /// Every batch entry point and the wire-ingest path funnel through here. The
1000    /// single-item entry points deliberately do **not** — they call
1001    /// `reduce_one` + `apply_effects` directly to avoid the grouping/Vec cost.
1002    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        // Reduce: one store diff per type, across all groups, before any cascade
1022        // (so the store is fully settled — load-bearing for transitive cascade).
1023        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        // Effects: search + cascade + produce, per same-type group.
1047        for group in by_type.values() {
1048            self.apply_effects(group, change, origin)?;
1049        }
1050        Ok(())
1051    }
1052
1053    /// Shared post-reduce tail: search index, relationship cascade (gated by
1054    /// `origin`), and produce (gated by `origin`).
1055    ///
1056    /// Operates on a slice of items **of the same entity type** whose store
1057    /// reduce has already run. Single-item callers pass `slice::from_ref(&item)`
1058    /// (zero alloc); `emit_grouped` passes each type-group. The type-erased
1059    /// produce path is equivalent to the typed one (`MEvent::from_item` ≡
1060    /// `MEvent::set_from_value(item.to_value())`, modulo the fresh `created_at`/`tx`).
1061    fn apply_effects(
1062        &self,
1063        items: &[Arc<dyn AnyItem>],
1064        change: MEventType,
1065        origin: Origin,
1066    ) -> Result<(), PersistError> {
1067        // Search: index searchable fields.
1068        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        // Relationships: run cascades unless this origin must not descend.
1083        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        // Persist: produce to persisters + sink unless this origin must not.
1095        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    // ─────────────────────────────────────────────────────────────────────────
1114    // durable backend production (private)
1115    // ─────────────────────────────────────────────────────────────────────────
1116
1117    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    // ─────────────────────────────────────────────────────────────────────────
1150    // Query methods
1151    // ─────────────────────────────────────────────────────────────────────────
1152
1153    /// Run a reactive query and return a typed map keyed by the item's typed id.
1154    ///
1155    /// The typed projection is cached — multiple callers with the same query
1156    /// share a single underlying map instead of each creating their own copy.
1157    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        // Hold the untyped map alive so the weak ref in the cache entry stays valid.
1176        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        // Concurrent cache sweep may have evicted the entry — re-insert and retry
1185        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    /// Run a reactive query and return a typed map keyed by canonical string ids.
1197    ///
1198    /// Prefer `query_map()` unless you specifically need string ids.
1199    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        // Concurrent cache sweep may have evicted the entry — re-insert and retry
1219        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    /// Run a reactive query.
1231    ///
1232    /// Returns a type-erased map that updates whenever the query results change.
1233    /// The query's `test_entity` is applied with proper server context.
1234    ///
1235    /// # Example
1236    ///
1237    /// ```rust,no_run
1238    /// use std::sync::Arc;
1239    /// use myko::entities::server::GetPeerServers;
1240    /// use myko::request::RequestContext;
1241    /// use myko::server::CellServerCtx;
1242    ///
1243    /// fn demo(ctx: &CellServerCtx, req: Arc<RequestContext>) {
1244    ///     let _peer_servers = ctx.query_map_untyped(GetPeerServers {}, req);
1245    ///     // _peer_servers is CellMap<Arc<str>, Arc<dyn AnyItem>, CellImmutable>
1246    /// }
1247    /// ```
1248    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        // Fast path
1256        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        // Re-check after gate
1268        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    /// Build a reactive view cell map (type-erased for framework internals).
1297    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        // Fast path
1305        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        // Re-check after gate
1317        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    /// Back-compat alias for type-erased view map.
1346    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    /// Build a typed reactive view cell map.
1355    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    /// Get a one-shot typed entity snapshot by id.
1373    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    /// Get one-shot typed entity snapshots for an item type.
1388    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    /// Get one-shot typed entity snapshots for the provided ids.
1402    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    /// Run a one-shot (non-reactive) query.
1416    ///
1417    /// Iterates the store directly and returns matching entities without creating
1418    /// any reactive cells or subscriptions. Use this for command handlers and other
1419    /// contexts where you need a point-in-time snapshot, not a live query.
1420    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        // Fast path: cache hit with live cell.
1465        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        // NOTE(ts): Per-key gate prevents duplicate computation when multiple threads
1477        // request the same report concurrently. First thread computes, others wait.
1478        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        // Re-check after acquiring the gate — another thread may have computed while we waited.
1486        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        // Emit MISS_COMPUTE *before* compute() so the analyze pass can correlate
1498        // the miss with the work that follows even if compute panics or hangs.
1499        // Payload is only serialized when the trace target is enabled.
1500        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        // The trait returns `impl Pipeline<...>`; materialize once here so the
1514        // cache and downstream consumers get a concrete `Cell`. This is the only
1515        // materialization per report, regardless of how deep the inner chain is.
1516        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    /// Try to get a cached report cell. Returns None if missing or dead.
1526    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        // Dead entry — drop the ref before removing to avoid DashMap deadlock
1540        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}