Skip to main content

myko_server/
lib.rs

1//! Myko server runtime — WebSocket, durable event backends, peer federation.
2//!
3//! This crate contains the tokio-dependent parts of the Myko server:
4//! - `CellServer` — server lifecycle (durable catch-up init, WS accept loop)
5//! - `postgres` — PostgreSQL producer/consumer (event-table + LISTEN/NOTIFY)
6//! - `ws_handler` — WebSocket connection handling
7//! - `peer_registry` — federation with other servers
8//! - `mcp` — Model Context Protocol server
9//!
10//! Tokio-free server types (CellServerCtx, HandlerRegistry, etc.) live in `myko::server`.
11
12pub mod mcp;
13pub mod peer_persister;
14pub mod peer_registry;
15pub mod postgres;
16pub mod router;
17pub mod server_ownership;
18pub mod telemetry;
19pub mod ws_handler;
20pub mod ws_timing;
21
22// Re-export all tokio-free server types from myko
23use std::{
24    collections::HashMap,
25    net::SocketAddr,
26    sync::{
27        Arc, RwLock,
28        atomic::{AtomicBool, Ordering},
29    },
30    time::Duration,
31};
32
33use futures_util::StreamExt;
34pub use myko::server::*;
35use myko::{
36    client::MykoClient, command::CommandContext, request::RequestContext, saga::SagaRegistration,
37    search::SearchIndex, store::StoreRegistry, wire::MEvent,
38};
39pub use peer_persister::PeerPersister;
40pub use server_ownership::ServerOwnershipManager;
41use uuid::Uuid;
42
43use crate::postgres::{
44    CellPostgresConsumer, CellPostgresProducer, PostgresConfig, PostgresHistoryReplayProvider,
45    PostgresHistoryStore, PostgresProducerHandle,
46};
47
48/// Cell-based Myko server configuration.
49#[derive(Clone)]
50pub struct CellServerConfig {
51    /// Address to bind the WebSocket server
52    pub bind_addr: SocketAddr,
53    /// Disable Nagle's algorithm (set `TCP_NODELAY`) on accepted connections.
54    /// Myko's traffic is small, frequent, latency-sensitive messages (e.g.
55    /// ~60Hz pulses); with Nagle on, TCP coalesces successive small writes
56    /// into fewer segments that arrive together, so an even send cadence is
57    /// delivered as bursts. Defaults to `true`.
58    pub tcp_nodelay: bool,
59    /// Optional Postgres configuration for event persistence/distribution
60    pub postgres: Option<PostgresConfig>,
61    /// Server host ID (auto-generated if not provided)
62    pub host_id: Option<Uuid>,
63    /// Optional peer registry configuration for federation
64    pub peer_registry: Option<peer_registry::PeerRegistryConfig>,
65    /// Default persister override
66    pub default_persister: Option<Arc<dyn Persister>>,
67    /// Per-entity persister overrides keyed by entity type name
68    pub persister_overrides: HashMap<String, Arc<dyn Persister>>,
69    /// Optional pre-constructed peer-client map. When provided, it will be
70    /// used as-is (so any `PeerPersister` built against the same `Arc`
71    /// shares the live map). If `None`, the server creates its own.
72    pub peer_clients: Option<Arc<dashmap::DashMap<Arc<str>, Arc<MykoClient>>>>,
73}
74
75/// Builder for creating a CellServer.
76#[derive(Default)]
77pub struct CellServerBuilder {
78    bind_addr: Option<SocketAddr>,
79    tcp_nodelay: Option<bool>,
80    host_id: Option<Uuid>,
81    postgres: Option<PostgresConfig>,
82    peer_registry: Option<peer_registry::PeerRegistryConfig>,
83    default_persister: Option<Arc<dyn Persister>>,
84    persister_overrides: HashMap<String, Arc<dyn Persister>>,
85    /// Optional pre-constructed peer-client map — useful when a
86    /// `PeerPersister` must reference the same map the server will use.
87    /// Defaults to a fresh empty map if not provided.
88    peer_clients: Option<Arc<dashmap::DashMap<Arc<str>, Arc<MykoClient>>>>,
89    after_init: Option<AfterInitCallback>,
90    /// Optional MCP `ServerInfo`. Defaults to `ServerInfo::default()` if not
91    /// set; binaries override this to advertise their own name / version /
92    /// instructions on the `/myko/mcp` endpoint.
93    server_info: Option<mcp::dispatch::ServerInfo>,
94}
95
96type AfterInitCallback = Box<dyn FnOnce(&CellServer) + Send>;
97
98impl CellServerBuilder {
99    /// Create a new server builder.
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Set the WebSocket bind address.
105    pub fn with_bind_addr(mut self, addr: SocketAddr) -> Self {
106        self.bind_addr = Some(addr);
107        self
108    }
109
110    /// Set whether to disable Nagle's algorithm (`TCP_NODELAY`) on accepted
111    /// connections. Defaults to `true` (Nagle off) — recommended for myko's
112    /// small, frequent, latency-sensitive messages so an even send cadence
113    /// isn't delivered as coalesced bursts.
114    pub fn with_tcp_nodelay(mut self, enabled: bool) -> Self {
115        self.tcp_nodelay = Some(enabled);
116        self
117    }
118
119    /// Set the server host ID (auto-generated if not set).
120    pub fn with_host_id(mut self, id: Uuid) -> Self {
121        self.host_id = Some(id);
122        self
123    }
124
125    /// Configure Postgres for event persistence/distribution.
126    pub fn with_postgres(mut self, config: PostgresConfig) -> Self {
127        self.postgres = Some(config);
128        self
129    }
130
131    /// Configure peer registry for federation.
132    pub fn with_peer_registry(mut self, config: peer_registry::PeerRegistryConfig) -> Self {
133        self.peer_registry = Some(config);
134        self
135    }
136
137    /// Set the default persister used for all entity types without explicit overrides.
138    pub fn with_default_persister(mut self, persister: Arc<dyn Persister>) -> Self {
139        self.default_persister = Some(persister);
140        self
141    }
142
143    /// Override persister for a specific entity type (e.g. "Pulse").
144    pub fn with_persister_override(
145        mut self,
146        entity_type: impl Into<String>,
147        persister: Arc<dyn Persister>,
148    ) -> Self {
149        self.persister_overrides
150            .insert(entity_type.into(), persister);
151        self
152    }
153
154    /// Provide a pre-constructed peer-client map. The server's peer
155    /// registry will populate it as peers connect. Pass the same `Arc`
156    /// into `PeerPersister::new(...)` when you register a
157    /// `with_persister_override(..., PeerPersister)` so the persister
158    /// shares the live map.
159    pub fn with_peer_clients(
160        mut self,
161        peer_clients: Arc<dashmap::DashMap<Arc<str>, Arc<MykoClient>>>,
162    ) -> Self {
163        self.peer_clients = Some(peer_clients);
164        self
165    }
166
167    /// Register a callback to run after initialization and relation establishment,
168    /// but before the WebSocket accept loop starts. Use this for starting subsystems
169    /// that need entity data (e.g., scene engine).
170    pub fn after_init(mut self, f: impl FnOnce(&CellServer) + Send + 'static) -> Self {
171        self.after_init = Some(Box::new(f));
172        self
173    }
174
175    /// Set the MCP `ServerInfo` advertised on the `/myko/mcp` `initialize`
176    /// response. Defaults to `ServerInfo::default()` (`myko-mcp` /
177    /// `CARGO_PKG_VERSION` / no instructions).
178    pub fn with_server_info(mut self, info: mcp::dispatch::ServerInfo) -> Self {
179        self.server_info = Some(info);
180        self
181    }
182
183    /// Build the server.
184    pub fn build(self) -> CellServer {
185        let bind_addr = self
186            .bind_addr
187            .unwrap_or_else(|| "127.0.0.1:5155".parse().unwrap());
188
189        let server_info = Arc::new(self.server_info.unwrap_or_default());
190
191        let mut server = CellServer::new(CellServerConfig {
192            bind_addr,
193            tcp_nodelay: self.tcp_nodelay.unwrap_or(true),
194            postgres: self.postgres,
195            host_id: self.host_id,
196            peer_registry: self.peer_registry,
197            default_persister: self.default_persister,
198            persister_overrides: self.persister_overrides,
199            peer_clients: self.peer_clients,
200        });
201        server.after_init = std::sync::Mutex::new(self.after_init);
202        server.server_info = server_info;
203        server
204    }
205}
206
207/// Cell-based Myko server.
208///
209/// Uses hyphae cells for reactive queries and reports instead of actors.
210pub struct CellServer {
211    /// Central entity store registry
212    pub registry: Arc<StoreRegistry>,
213    /// Handler registry for items, queries, and reports
214    pub handler_registry: Arc<HandlerRegistry>,
215    /// Relationship manager for cascade operations
216    pub relationship_manager: Arc<RelationshipManager>,
217    /// Optional Postgres producer handle
218    pub postgres_producer: Option<PostgresProducerHandle>,
219    /// Full-text search index
220    pub search_index: Arc<SearchIndex>,
221    /// Persister routing (default + per-entity overrides)
222    pub persisters: Arc<PersisterRouter>,
223    /// Server host ID
224    pub host_id: Uuid,
225    /// Server configuration
226    config: CellServerConfig,
227    /// Postgres producer (kept alive)
228    _postgres_producer_owner: Option<CellPostgresProducer>,
229    /// Postgres consumer (kept alive)
230    postgres_consumer: Option<CellPostgresConsumer>,
231    /// Whether the server is ready to accept connections
232    ready: Arc<AtomicBool>,
233    /// Peer registry for federation (initialized after catch-up)
234    peer_registry_instance: RwLock<Option<peer_registry::PeerRegistry>>,
235    /// Live peer clients shared with report context.
236    peer_clients: Arc<dashmap::DashMap<Arc<str>, Arc<MykoClient>>>,
237    /// Callback to run after init (catch-up + relations) but before WS loop
238    after_init: std::sync::Mutex<Option<AfterInitCallback>>,
239    /// MCP `ServerInfo` advertised on the `/myko/mcp` `initialize` response.
240    /// Set via [`CellServerBuilder::with_server_info`]; defaults to
241    /// `ServerInfo::default()`.
242    server_info: Arc<mcp::dispatch::ServerInfo>,
243    /// Sender for local+replicated event fan-out to saga runtime.
244    saga_event_tx: flume::Sender<MEvent>,
245    /// Receiver consumed when saga runtime starts.
246    saga_event_rx: std::sync::Mutex<Option<flume::Receiver<MEvent>>>,
247    /// Saga tasks kept alive for server lifetime.
248    saga_tasks: std::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>,
249    /// Server ownership death-watch guard (kept alive for server lifetime).
250    _server_ownership_guard: std::sync::Mutex<Option<hyphae::SubscriptionGuard>>,
251    /// Memoized server context. Built once on first `ctx()` and shared by
252    /// every caller — the caches (`query_cache`/`view_cache`/`report_cache`/
253    /// `compute_gates`/`ingest_buffers`) and the `peer_clients_tick` cell are
254    /// then genuinely process-wide, so N connections subscribing to the same
255    /// query share one reactive cell graph instead of N copies, cache sweeps
256    /// actually reach live entries, and every peer-death watcher observes the
257    /// same tick. Every `CellServerCtx` field is an `Arc`/`Cell`/`Uuid`, so a
258    /// clone is a handful of refcount bumps that share the underlying state.
259    ctx_cache: std::sync::OnceLock<CellServerCtx>,
260}
261
262impl CellServer {
263    /// Create a new server builder.
264    pub fn builder() -> CellServerBuilder {
265        CellServerBuilder::new()
266    }
267
268    /// Create a new cell-based server.
269    pub fn new(config: CellServerConfig) -> Self {
270        let host_id = config.host_id.unwrap_or_else(Uuid::new_v4);
271        let registry = Arc::new(StoreRegistry::new());
272        let handler_registry = Arc::new(HandlerRegistry::new());
273        let relationship_manager = Arc::new(RelationshipManager::new());
274
275        // Initialize the client registry for WebSocket client message dispatch
276        init_client_registry();
277
278        let (saga_event_tx, saga_event_rx) = flume::unbounded::<MEvent>();
279        let (postgres_producer_owner, postgres_producer, postgres_consumer) =
280            if let Some(ref postgres_config) = config.postgres {
281                match CellPostgresProducer::new(postgres_config, host_id) {
282                    Ok(producer) => {
283                        let handle = producer.handle();
284                        let consumer = match CellPostgresConsumer::start(
285                            postgres_config,
286                            host_id,
287                            handler_registry.clone(),
288                            registry.clone(),
289                        ) {
290                            Ok(c) => Some(c),
291                            Err(e) => {
292                                tracing::error!("Failed to start Postgres consumer: {}", e);
293                                None
294                            }
295                        };
296                        (Some(producer), Some(handle), consumer)
297                    }
298                    Err(e) => {
299                        tracing::error!("Failed to create Postgres producer: {}", e);
300                        (None, None, None)
301                    }
302                }
303            } else {
304                (None, None, None)
305            };
306
307        // If no durable consumer, server is immediately ready
308        let ready = Arc::new(AtomicBool::new(postgres_consumer.is_none()));
309
310        // Initialize full-text search index
311        let search_index = Arc::new(SearchIndex::new());
312
313        // Build persister routing:
314        // - explicit default from config if provided
315        // - otherwise Postgres producer handle when available
316        // - explicit per-entity overrides always win
317        let mut persister_router = PersisterRouter::default();
318        if let Some(default_persister) = config.default_persister.clone() {
319            persister_router.set_default(Some(default_persister));
320        } else if let Some(handle) = postgres_producer.clone() {
321            persister_router.set_default(Some(Arc::new(handle) as Arc<dyn Persister>));
322        }
323        for (entity_type, persister) in &config.persister_overrides {
324            persister_router.set_override(entity_type.clone(), persister.clone());
325        }
326        let persisters = Arc::new(persister_router);
327
328        let peer_clients = config
329            .peer_clients
330            .clone()
331            .unwrap_or_else(|| Arc::new(dashmap::DashMap::new()));
332
333        Self {
334            registry,
335            handler_registry,
336            relationship_manager,
337            postgres_producer,
338            search_index,
339            persisters,
340            host_id,
341            config,
342            _postgres_producer_owner: postgres_producer_owner,
343            postgres_consumer,
344            ready,
345            peer_registry_instance: RwLock::new(None),
346            peer_clients,
347            after_init: std::sync::Mutex::new(None),
348            server_info: Arc::new(mcp::dispatch::ServerInfo::default()),
349            saga_event_tx,
350            saga_event_rx: std::sync::Mutex::new(Some(saga_event_rx)),
351            saga_tasks: std::sync::Mutex::new(Vec::new()),
352            _server_ownership_guard: std::sync::Mutex::new(None),
353            ctx_cache: std::sync::OnceLock::new(),
354        }
355    }
356
357    /// Start the peer registry for federation.
358    pub fn start_peer_registry(&self, config: Option<peer_registry::PeerRegistryConfig>) {
359        let peer_config = config.or_else(|| self.config.peer_registry.clone());
360
361        if let Some(peer_config) = peer_config {
362            tracing::info!("Starting peer registry");
363            let pr = peer_registry::PeerRegistry::new(self.ctx(), peer_config);
364            *self.peer_registry_instance.write().unwrap() = Some(pr);
365        }
366    }
367
368    /// Check if peer registry is running.
369    pub fn has_peer_registry(&self) -> bool {
370        self.peer_registry_instance.read().unwrap().is_some()
371    }
372
373    /// Get the store registry.
374    pub fn registry(&self) -> Arc<StoreRegistry> {
375        self.registry.clone()
376    }
377
378    /// Get the handler registry.
379    pub fn handler_registry(&self) -> Arc<HandlerRegistry> {
380        self.handler_registry.clone()
381    }
382
383    /// Get the MCP `ServerInfo` advertised on the `/myko/mcp` `initialize`
384    /// response.
385    pub fn server_info(&self) -> Arc<mcp::dispatch::ServerInfo> {
386        self.server_info.clone()
387    }
388
389    /// Get a server context for module use.
390    ///
391    /// Returns a clone of the one memoized [`CellServerCtx`] (see `ctx_cache`)
392    /// so all callers share the same caches and peer-tick cell. Building a
393    /// fresh context per call — the old behavior — gave every connection its
394    /// own caches: no cross-client cache sharing (an N× reactive-graph
395    /// memory multiplier), sweeps that never reached connection caches, and a
396    /// `peer_clients_tick` that register/unregister bumped on one context
397    /// while a watcher built on another never observed.
398    pub fn ctx(&self) -> CellServerCtx {
399        self.ctx_cache
400            .get_or_init(|| {
401                let history_replay: Option<Arc<dyn myko::server::HistoryReplayProvider>> =
402                    self.config.postgres.as_ref().map(|pg| {
403                        Arc::new(PostgresHistoryReplayProvider::new(pg.clone()))
404                            as Arc<dyn myko::server::HistoryReplayProvider>
405                    });
406                CellServerCtx::new(
407                    self.host_id,
408                    self.registry.clone(),
409                    self.handler_registry.clone(),
410                    self.relationship_manager.clone(),
411                    self.persisters.clone(),
412                    self.search_index.clone(),
413                    self.peer_clients.clone(),
414                    Some(self.saga_event_tx.clone()),
415                    history_replay,
416                )
417            })
418            .clone()
419    }
420
421    fn start_saga_runtime(&self) {
422        let registrations: Vec<_> = inventory::iter::<SagaRegistration>().collect();
423        if registrations.is_empty() {
424            return;
425        }
426        let Some(rx) = self
427            .saga_event_rx
428            .lock()
429            .expect("saga_event_rx mutex poisoned")
430            .take()
431        else {
432            return;
433        };
434
435        tracing::info!("Starting saga runtime with {} saga(s)", registrations.len());
436
437        // NOTE(ts): One unbounded flume channel per saga, with dispatch-side filtering
438        // so sagas only receive events matching their entity type and change type.
439        struct SagaChannel {
440            tx: flume::Sender<MEvent>,
441            entity_type: &'static str,
442            change_type: myko::event::MEventType,
443        }
444        let mut saga_channels: Vec<SagaChannel> = Vec::new();
445
446        for registration in registrations {
447            let saga = (registration.create)();
448            let saga_name = saga.name().to_string();
449            let (saga_tx, saga_rx) = flume::unbounded::<MEvent>();
450            saga_channels.push(SagaChannel {
451                tx: saga_tx,
452                entity_type: registration.event_entity_type,
453                change_type: registration.event_change_type,
454            });
455            let events: myko::saga::EventStream = Box::pin(futures_util::stream::unfold(
456                saga_rx,
457                move |saga_rx| async move {
458                    saga_rx
459                        .recv_async()
460                        .await
461                        .ok()
462                        .map(|event| (event, saga_rx))
463                },
464            ));
465
466            let saga_ctx = Arc::new(myko::saga::SagaContext::with_event_sink(
467                self.host_id,
468                self.registry.clone(),
469                self.saga_event_tx.clone(),
470            ));
471            let mut command_stream = saga.build_boxed(events, saga_ctx);
472
473            let host_id = self.host_id;
474            let registry = self.registry.clone();
475            let handler_registry = self.handler_registry.clone();
476            let relationship_manager = self.relationship_manager.clone();
477            let persisters = self.persisters.clone();
478            let search_index = self.search_index.clone();
479            let peer_clients = self.peer_clients.clone();
480            let saga_event_tx = self.saga_event_tx.clone();
481
482            let handle = tokio::spawn(async move {
483                while let Some(command) = command_stream.next().await {
484                    let command_name = command.command_name();
485                    tracing::debug!("Saga {} executing command {}", saga_name, command_name);
486                    let req = Arc::new(RequestContext::internal(
487                        Arc::from(Uuid::new_v4().to_string()),
488                        host_id,
489                        &format!("saga:{saga_name}"),
490                    ));
491
492                    let cmd_ctx = CommandContext::new(
493                        Arc::from(command_name),
494                        req,
495                        Arc::new(CellServerCtx::new(
496                            host_id,
497                            registry.clone(),
498                            handler_registry.clone(),
499                            relationship_manager.clone(),
500                            persisters.clone(),
501                            search_index.clone(),
502                            peer_clients.clone(),
503                            Some(saga_event_tx.clone()),
504                            None,
505                        )),
506                    );
507
508                    if let Err(err) = command.execute_boxed(cmd_ctx) {
509                        tracing::error!(
510                            "Saga {} command {} failed: {}",
511                            saga_name,
512                            command_name,
513                            err.message
514                        );
515                    }
516                }
517            });
518
519            self.saga_tasks
520                .lock()
521                .expect("saga_tasks mutex poisoned")
522                .push(handle);
523        }
524
525        // NOTE(ts): Dispatcher fans out events to saga channels, filtering by
526        // entity type and change type so each saga only receives relevant events.
527        let dispatcher = tokio::spawn(async move {
528            while let Ok(event) = rx.recv_async().await {
529                for ch in &saga_channels {
530                    if event.item_type == ch.entity_type && event.change_type == ch.change_type {
531                        let _ = ch.tx.send(event.clone());
532                    }
533                }
534            }
535        });
536        self.saga_tasks
537            .lock()
538            .expect("saga_tasks mutex poisoned")
539            .push(dispatcher);
540    }
541
542    /// Create a Postgres-backed history store for replay/windback operations.
543    pub fn postgres_history_store(&self) -> Result<Option<PostgresHistoryStore>, String> {
544        self.config
545            .postgres
546            .clone()
547            .map(PostgresHistoryStore::new)
548            .transpose()
549    }
550
551    /// Initialize Postgres replay/listener and wait for catch-up.
552    pub fn init_postgres_and_wait(&self, timeout: Duration) -> Result<(), String> {
553        if self.config.postgres.is_some() && self.postgres_consumer.is_none() {
554            return Err(
555                "Postgres is configured but the Postgres consumer is not running".to_string(),
556            );
557        }
558
559        if let Some(ref consumer) = self.postgres_consumer {
560            consumer.wait_until_caught_up(timeout)?;
561            self.ready.store(true, Ordering::SeqCst);
562        }
563        Ok(())
564    }
565
566    /// Establish relationship invariants.
567    pub fn establish_relations(&self) {
568        if let Err(e) = self.relationship_manager.establish_relations(&self.ctx()) {
569            tracing::error!("Failed to establish relations: {e}");
570        }
571    }
572
573    /// Check if the server is ready to accept connections.
574    pub fn is_ready(&self) -> bool {
575        if let Some(ref consumer) = self.postgres_consumer {
576            if consumer.is_caught_up() {
577                self.ready.store(true, Ordering::SeqCst);
578                return true;
579            }
580            return false;
581        }
582        true
583    }
584
585    /// Run the server with full initialization.
586    pub async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
587        use tokio::net::TcpListener;
588
589        // Persisters can veto startup via startup healthchecks.
590        let entity_types: Vec<&str> = self
591            .handler_registry
592            .entity_types()
593            .map(|t| t.as_ref())
594            .collect();
595        self.persisters
596            .startup_healthcheck(&entity_types)
597            .map_err(|reason| format!("Persister startup healthcheck failed: {reason}"))?;
598
599        if self.config.postgres.is_some() && self.postgres_consumer.is_none() {
600            return Err("Postgres is configured but the Postgres consumer failed to start".into());
601        }
602
603        // Wait for Postgres catch-up if configured
604        if self.postgres_consumer.is_some() {
605            tracing::info!("Waiting for Postgres event consumer to catch up...");
606            let timeout = std::time::Duration::from_secs(300);
607            self.init_postgres_and_wait(timeout)
608                .map_err(|reason| format!("Postgres startup catch-up failed: {reason}"))?;
609            tracing::info!("Postgres caught up, ready to accept connections");
610        }
611
612        // Build search index from store data (after catch-up)
613        tracing::info!("Building search index...");
614        self.search_index.build_from_registry(&self.registry);
615
616        // Establish relations (cleanup orphans, ensure required entities)
617        tracing::info!("Establishing relations...");
618        self.establish_relations();
619
620        // Claim orphaned server-owned items and start death watch
621        tracing::info!("Checking server-owned item ownership...");
622        if let Err(e) = ServerOwnershipManager::claim_orphaned(&self.ctx()) {
623            tracing::error!("Failed to claim orphaned server-owned items: {}", e);
624        }
625        let ownership_guard = ServerOwnershipManager::watch_peer_deaths(&self.ctx());
626        *self
627            ._server_ownership_guard
628            .lock()
629            .expect("server_ownership_guard mutex poisoned") = Some(ownership_guard);
630
631        // Run after_init hook (e.g., scene engine startup) BEFORE binding the
632        // listener. This hook runs synchronously and can be slow (e.g.
633        // rship's scene-editor-view warmup, which materializes a view per
634        // scene) — binding first and accepting later left a window where the
635        // OS would complete TCP handshakes and queue them in the accept
636        // backlog while nothing in the process was reading them yet. A
637        // client connecting in that window would see an established TCP
638        // connection that never got a WebSocket-upgrade response: not
639        // rejected (so no clean retry trigger), not served (so no response
640        // ever arrives) — stuck rather than cleanly failing. Binding late
641        // means a connection attempt during startup gets a prompt
642        // ECONNREFUSED instead, which every client/proxy already retries.
643        if let Some(hook) = self
644            .after_init
645            .lock()
646            .expect("after_init mutex poisoned")
647            .take()
648        {
649            hook(self);
650        }
651
652        self.start_saga_runtime();
653
654        // WS message-throughput summary thread. Emits a single log line every
655        // 250ms with inbound/outbound counts per message kind. Used for
656        // diagnosing server-vs-client pacing during slow loads.
657        crate::ws_timing::start_periodic_logger();
658
659        // Report-cache hit/miss summary thread. Replaces the per-call debug
660        // log spam that was dominating I/O during loads.
661        myko::server::report_cache_stats::start_periodic_logger();
662
663        // Entity-SET summary thread. Replaces the per-`set` "[entity] SET ..."
664        // debug spam (Pulse SETs dominate under pulse-heavy workloads).
665        myko::server::entity_set_stats::start_periodic_logger();
666
667        // Per-search summary thread. One log line per window listing each
668        // search that completed (entity_type, result count, elapsed).
669        myko::search::search_stats::start_periodic_logger();
670
671        // Live per-entity-type item-count gauge, sampled by the OTLP metrics
672        // exporter's own periodic reader (see telemetry::init_from_env) —
673        // no-op when telemetry isn't configured.
674        crate::telemetry::register_item_count_gauge(self.registry.clone());
675
676        // Opt-in malloc_trim probe (MYKO_MALLOC_TRIM_INTERVAL_SECS): logs RSS
677        // before/after returning free glibc arena pages, to tell allocator
678        // page retention apart from real memory retention in deployments.
679        crate::telemetry::start_malloc_trim_probe();
680
681        // Bind WebSocket listener last, once all synchronous startup work
682        // (including after_init) is done, so peer publication only happens
683        // once the gateway is actually available to serve requests, not just
684        // listening.
685        let listener = TcpListener::bind(&self.config.bind_addr).await?;
686        tracing::info!("CellServer listening on {}", self.config.bind_addr);
687        tracing::info!(
688            "Myko gateway: ws://{}/myko | MCP: /myko/mcp (POST + WS + SSE)",
689            self.config.bind_addr
690        );
691
692        // Start peer registry if configured
693        if self.config.peer_registry.is_some() {
694            self.start_peer_registry(None);
695        }
696
697        tracing::info!("Server started");
698        self.run_ws_accept_loop(listener).await
699    }
700
701    /// Run just the accept loop (no Postgres / relations / saga startup).
702    pub async fn run_ws_loop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
703        use tokio::net::TcpListener;
704
705        let listener = TcpListener::bind(&self.config.bind_addr).await?;
706        tracing::info!("CellServer listening on {}", self.config.bind_addr);
707        tracing::info!(
708            "Myko gateway: ws://{}/myko | MCP: /myko/mcp (POST + WS + SSE)",
709            self.config.bind_addr
710        );
711        self.run_ws_accept_loop(listener).await
712    }
713
714    async fn run_ws_accept_loop(
715        &self,
716        listener: tokio::net::TcpListener,
717    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
718        let ready = self.ready.clone();
719
720        loop {
721            let (stream, addr) = listener.accept().await?;
722
723            // Disable Nagle (unless configured off): our writes are small,
724            // frequent, latency-sensitive messages (e.g. ~60Hz pulses). With
725            // Nagle on (tokio's default), TCP coalesces successive small writes
726            // into fewer segments that land together, so an even 60Hz send
727            // arrives at the client as ~15-20Hz bursts of 3-4 — which downstream
728            // latest-wins consumers (e.g. the pulse-unreal transform apply) then
729            // collapse to one update per burst, producing visibly choppy motion.
730            // Ship each write promptly instead.
731            if self.config.tcp_nodelay
732                && let Err(e) = stream.set_nodelay(true)
733            {
734                tracing::warn!("failed to set TCP_NODELAY on connection from {addr}: {e}");
735            }
736
737            // Check if server is ready (durable backend caught up)
738            if !ready.load(Ordering::SeqCst) {
739                if self.is_ready() {
740                    tracing::info!("Server is now ready to accept connections");
741                } else {
742                    tracing::warn!(
743                        "Rejecting connection from {} - server not ready (durable backend catching up)",
744                        addr
745                    );
746                    drop(stream);
747                    continue;
748                }
749            }
750
751            tracing::debug!("New connection from {}", addr);
752
753            let ctx = Arc::new(self.ctx());
754            let server_info = self.server_info.clone();
755
756            tokio::spawn(async move {
757                if let Err(e) = router::route_connection(stream, addr, ctx, server_info).await {
758                    tracing::error!("Connection error from {}: {}", addr, e);
759                }
760            });
761        }
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768
769    #[test]
770    fn test_server_creation() {
771        let config = CellServerConfig {
772            bind_addr: "127.0.0.1:0".parse().unwrap(),
773            tcp_nodelay: true,
774            postgres: None,
775            host_id: None,
776            peer_registry: None,
777            default_persister: None,
778            persister_overrides: HashMap::new(),
779            peer_clients: None,
780        };
781        let server = CellServer::new(config);
782        assert!(Arc::strong_count(&server.registry) >= 1);
783    }
784
785    #[test]
786    fn test_server_with_host_id() {
787        let host_id = Uuid::new_v4();
788        let config = CellServerConfig {
789            bind_addr: "127.0.0.1:0".parse().unwrap(),
790            tcp_nodelay: true,
791            postgres: None,
792            host_id: Some(host_id),
793            peer_registry: None,
794            default_persister: None,
795            persister_overrides: HashMap::new(),
796            peer_clients: None,
797        };
798        let server = CellServer::new(config);
799        assert_eq!(server.host_id, host_id);
800    }
801
802    #[test]
803    fn ctx_is_memoized_and_shares_caches() {
804        // Regression for lv-38b7: every `ctx()` must return the SAME shared
805        // context, so a query cached through one call is visible through the
806        // next. Before the fix, each `ctx()` allocated its own caches, so a
807        // second call reported an empty query cache (and per-connection
808        // caches leaked, never swept).
809        let config = CellServerConfig {
810            bind_addr: "127.0.0.1:0".parse().unwrap(),
811            tcp_nodelay: true,
812            postgres: None,
813            host_id: None,
814            peer_registry: None,
815            default_persister: None,
816            persister_overrides: HashMap::new(),
817            peer_clients: None,
818        };
819        let server = CellServer::new(config);
820        let ctx1 = server.ctx();
821        let ctx2 = server.ctx();
822
823        let req = Arc::new(RequestContext::internal(
824            Arc::from("test"),
825            server.host_id,
826            "ctx_sharing_test",
827        ));
828        // Hold the map so its cache entry (weak-referenced) stays live.
829        let _held = ctx1.query_map(myko::entities::client::GetAllClients {}, req);
830
831        assert!(
832            ctx1.query_cache_len() >= 1,
833            "querying through ctx1 should populate the shared query cache"
834        );
835        assert_eq!(
836            ctx2.query_cache_len(),
837            ctx1.query_cache_len(),
838            "both ctx() calls must share one query cache"
839        );
840    }
841}