Skip to main content

arete_server/
lib.rs

1//! # arete-server
2//!
3//! WebSocket server and projection handlers for Arete streaming pipelines.
4//!
5//! This crate provides a builder API for creating Arete servers that:
6//!
7//! - Process Solana blockchain data via Yellowstone gRPC
8//! - Transform data using the Arete VM
9//! - Stream entity updates over WebSockets to connected clients
10//! - Support multiple streaming modes (State, List, Append)
11//!
12//! ## Quick Start
13//!
14//! ```rust,ignore
15//! use arete_server::{Server, Spec};
16//!
17//! #[tokio::main]
18//! async fn main() -> anyhow::Result<()> {
19//!     Server::builder()
20//!         .spec(my_spec())
21//!         .websocket()
22//!         .bind("[::]:8877".parse()?)
23//!         .health_monitoring()
24//!         .start()
25//!         .await
26//! }
27//! ```
28//!
29//! ## Feature Flags
30//!
31//! - `otel` - OpenTelemetry integration for metrics and distributed tracing
32
33pub mod account_policy;
34pub mod bus;
35pub mod cache;
36pub mod compression;
37pub mod config;
38pub mod health;
39mod http;
40pub mod http_health;
41pub mod http_server;
42pub mod journal;
43pub mod materialized_view;
44#[cfg(feature = "otel")]
45pub mod metrics;
46pub mod mutation_batch;
47pub mod program_runtime;
48pub mod projector;
49pub mod row_scope;
50pub mod runtime;
51pub mod shared_entity;
52pub mod snapshot;
53pub mod solana_gateway_usage;
54pub mod sorted_cache;
55pub mod telemetry;
56pub mod view;
57pub mod websocket;
58
59pub use arete_auth::{
60    AsyncVerifier, KeyLoader, Limits, SolanaGatewayAuthorization, SolanaGatewayAuthorizationError,
61    SolanaGatewayScope, TargetKind, TokenVerifier, VerifyingKey, SCOPE_FORK_READ, SCOPE_READ,
62    SCOPE_TRANSACTION_INSPECT, SCOPE_TRANSACTION_SEND, SOLANA_GATEWAY_AUDIENCE,
63};
64pub use bus::{BusManager, BusMessage, StateUpdate};
65pub use cache::{CacheWrite, EntityCache, EntityCacheConfig, PatchOrigin};
66pub use config::{
67    HealthConfig, HttpHealthConfig, HttpLimitsConfig, HttpServerConfig, ReconnectionConfig,
68    RuntimePlan, ServerConfig, TransactionConfig, WebSocketConfig, WebSocketDeliveryConfig,
69    WebSocketLimitsConfig, YellowstoneConfig,
70};
71pub use health::{HealthMonitor, SlotTracker, StreamStatus};
72pub use http_health::HttpHealthServer;
73pub use http_server::HttpServer;
74pub use journal::{EventJournal, JournalConfig, ReplayWindow};
75pub use materialized_view::{MaterializedView, MaterializedViewRegistry, ViewEffect};
76#[cfg(feature = "otel")]
77pub use metrics::Metrics;
78pub use mutation_batch::{EventContext, MutationBatch, SlotContext, SlotIndexDomain};
79pub use program_runtime::{
80    IdlContentHash, NormalizedIdlHash, ProgramAccountReaderFn, ProgramReleaseHash,
81    ProgramRuntimeCatalog, ProgramRuntimeDefinition, ProgramSpecHash,
82};
83pub use projector::{EntityResync, Projector};
84pub use row_scope::{RowScope, ScopeField, DEFAULT_MAX_SCOPE_PARTITIONS};
85pub use runtime::{ConnectionServer, Runtime, RuntimeHandle};
86pub use shared_entity::{EntityFields, SharedEntity};
87pub use snapshot::{SnapshotConfig, SnapshotService};
88pub use solana_gateway_usage::{
89    SolanaGatewayUsageObservation, SolanaGatewayUsageObserver, SolanaGatewayUsageSurface,
90};
91pub use telemetry::{init as init_telemetry, TelemetryConfig};
92#[cfg(feature = "otel")]
93pub use telemetry::{init_with_otel, TelemetryGuard};
94pub use view::{Delivery, Filters, Projection, ViewIndex, ViewSpec};
95pub use websocket::{
96    AllowAllAuthPlugin, AuthContext, AuthDecision, AuthDeny, AuthErrorDetails, ChannelUsageEmitter,
97    ClientInfo, ClientManager, ConnectionAuthRequest, ErrorResponse, Frame, HttpUsageEmitter, Mode,
98    RateLimitConfig, RateLimitResult, RateLimiterConfig, RefreshAuthRequest, RefreshAuthResponse,
99    RetryPolicy, SignedSessionAuthPlugin, SnapshotOptions, SocketIssueMessage,
100    StaticTokenAuthPlugin, Subscription, SubscriptionQuery, WebSocketAuthPlugin,
101    WebSocketRateLimiter, WebSocketServer, WebSocketUsageBatch, WebSocketUsageEmitter,
102    WebSocketUsageEnvelope, WebSocketUsageEvent,
103};
104pub use websocket::{WebSocketAdmissionProvider, WebSocketConnectionPermit};
105
106use anyhow::Result;
107use arete_interpreter::ast::ViewDef;
108use std::net::SocketAddr;
109use std::sync::Arc;
110
111/// Type alias for a parser setup function.
112pub type ParserSetupFn = Arc<
113    dyn Fn(
114            tokio::sync::mpsc::Sender<MutationBatch>,
115            Option<HealthMonitor>,
116            ReconnectionConfig,
117        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
118        + Send
119        + Sync,
120>;
121
122/// Specification for a Arete server
123/// Contains bytecode, parsers, and program information
124pub struct Spec {
125    pub bytecode: arete_interpreter::compiler::MultiEntityBytecode,
126    pub program_ids: Vec<String>,
127    pub parser_setup: Option<ParserSetupFn>,
128    pub program_runtime_definitions: Vec<ProgramRuntimeDefinition>,
129    pub entity_specs: Vec<arete_interpreter::ast::SerializableStreamSpec>,
130    pub views: Vec<ViewDef>,
131}
132
133impl Spec {
134    pub fn new(
135        bytecode: arete_interpreter::compiler::MultiEntityBytecode,
136        program_id: impl Into<String>,
137    ) -> Self {
138        Self {
139            bytecode,
140            program_ids: vec![program_id.into()],
141            parser_setup: None,
142            program_runtime_definitions: Vec::new(),
143            entity_specs: Vec::new(),
144            views: Vec::new(),
145        }
146    }
147
148    pub fn with_parser_setup(mut self, setup_fn: ParserSetupFn) -> Self {
149        self.parser_setup = Some(setup_fn);
150        self
151    }
152
153    pub fn with_program_runtime_definitions(
154        mut self,
155        definitions: Vec<ProgramRuntimeDefinition>,
156    ) -> Self {
157        for definition in &definitions {
158            if !self.program_ids.contains(&definition.program_id) {
159                self.program_ids.push(definition.program_id.clone());
160            }
161        }
162        self.program_runtime_definitions = definitions;
163        self
164    }
165
166    pub fn with_entity_specs(
167        mut self,
168        entity_specs: Vec<arete_interpreter::ast::SerializableStreamSpec>,
169    ) -> Self {
170        self.entity_specs = entity_specs;
171        self
172    }
173
174    pub fn with_views(mut self, views: Vec<ViewDef>) -> Self {
175        self.views = views;
176        self
177    }
178}
179
180/// Main server interface with fluent builder API
181pub mod token_discovery;
182
183pub struct Server;
184
185impl Server {
186    /// Create a new server builder
187    pub fn builder() -> ServerBuilder {
188        ServerBuilder::new()
189    }
190
191    /// Build a standalone HTTP gateway without stack or live-stream capabilities.
192    pub fn solana_gateway(target_id: impl Into<String>) -> SolanaGatewayBuilder {
193        SolanaGatewayBuilder::new(target_id.into())
194    }
195}
196
197/// Constrained builder for health, chain reads, and fixed transaction routes.
198///
199/// The final runtime plan is fixed when `build` or `start` is called. This
200/// builder intentionally has no Spec, WebSocket, stack-query, program-read, or
201/// live-runtime configuration surface.
202pub struct SolanaGatewayBuilder {
203    inner: ServerBuilder,
204}
205
206impl SolanaGatewayBuilder {
207    fn new(target_id: String) -> Self {
208        let mut inner = ServerBuilder::new();
209        inner.config.http_health = Some(HttpHealthConfig::default());
210        inner.config.runtime_plan = RuntimePlan::solana_gateway();
211        inner.config.solana_gateway_target_id = Some(target_id);
212        Self { inner }
213    }
214
215    /// Set the gateway HTTP bind address.
216    pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
217        self.inner.config.http_health = Some(HttpHealthConfig::new(addr));
218        self
219    }
220
221    /// Set the auth plugin for chain and transaction requests.
222    pub fn auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
223        self.inner.http_auth_plugin = Some(plugin);
224        self
225    }
226
227    /// Configure HTTP body, header and read RPC limits.
228    pub fn http_limits(mut self, config: HttpLimitsConfig) -> Self {
229        self.inner.config.http_limits = Some(config);
230        self
231    }
232
233    /// Configure the existing fixed `/transactions/v1/*` handlers.
234    pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
235        self.inner.config.transactions = Some(config);
236        self
237    }
238
239    /// Observe completed chain and transaction operations.
240    pub fn owner_token_accounts_provider(
241        mut self,
242        provider: Arc<dyn crate::token_discovery::OwnerTokenAccountsProvider>,
243    ) -> Self {
244        self.inner.token_accounts_provider = Some(provider);
245        self
246    }
247
248    pub fn usage_observer(mut self, observer: Arc<dyn SolanaGatewayUsageObserver>) -> Self {
249        self.inner.solana_gateway_usage_observer = Some(observer);
250        self
251    }
252
253    fn finalize(mut self) -> Result<ServerBuilder> {
254        if self
255            .inner
256            .config
257            .solana_gateway_target_id
258            .as_deref()
259            .is_none_or(|target_id| target_id.trim().is_empty())
260        {
261            anyhow::bail!("the Solana gateway target ID must not be empty");
262        }
263        if let Some(config) = self.inner.config.transactions.as_ref() {
264            config.validate()?;
265            if !config.enabled {
266                anyhow::bail!("Solana gateway transaction configuration must be enabled");
267            }
268        }
269        self.inner.config.runtime_plan = RuntimePlan::solana_gateway();
270        Ok(self.inner)
271    }
272
273    /// Build the reusable runtime without starting it.
274    pub fn build(self) -> Result<Runtime> {
275        self.finalize()?.build()
276    }
277
278    /// Start the gateway and wait for shutdown.
279    pub async fn start(self) -> Result<()> {
280        self.finalize()?.start().await
281    }
282}
283
284/// Builder for configuring and creating a Arete server
285pub struct ServerBuilder {
286    spec: Option<Spec>,
287    views: Option<ViewIndex>,
288    materialized_views: Option<MaterializedViewRegistry>,
289    config: ServerConfig,
290    websocket_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
291    http_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
292    websocket_usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
293    token_accounts_provider: Option<Arc<dyn crate::token_discovery::OwnerTokenAccountsProvider>>,
294    solana_gateway_usage_observer: Option<Arc<dyn SolanaGatewayUsageObserver>>,
295    websocket_max_clients: Option<usize>,
296    websocket_rate_limit_config: Option<crate::websocket::client_manager::RateLimitConfig>,
297    websocket_admission_provider: Option<Arc<dyn WebSocketAdmissionProvider>>,
298    #[cfg(feature = "otel")]
299    metrics: Option<Arc<Metrics>>,
300}
301
302impl ServerBuilder {
303    fn new() -> Self {
304        Self {
305            spec: None,
306            views: None,
307            materialized_views: None,
308            config: ServerConfig::new(),
309            websocket_auth_plugin: None,
310            http_auth_plugin: None,
311            websocket_usage_emitter: None,
312            token_accounts_provider: None,
313            solana_gateway_usage_observer: None,
314            websocket_max_clients: None,
315            websocket_rate_limit_config: None,
316            websocket_admission_provider: None,
317            #[cfg(feature = "otel")]
318            metrics: None,
319        }
320    }
321
322    /// Set the specification (bytecode, parsers, program_ids)
323    pub fn spec(mut self, spec: Spec) -> Self {
324        self.spec = Some(spec);
325        self
326    }
327
328    /// Set custom view index
329    pub fn views(mut self, views: ViewIndex) -> Self {
330        self.views = Some(views);
331        self
332    }
333
334    /// Enable metrics collection (requires 'otel' feature)
335    #[cfg(feature = "otel")]
336    pub fn metrics(mut self, metrics: Metrics) -> Self {
337        self.metrics = Some(Arc::new(metrics));
338        self
339    }
340
341    /// Enable WebSocket server with default configuration
342    pub fn websocket(mut self) -> Self {
343        self.config.websocket = Some(WebSocketConfig::default());
344        self.config.runtime_plan.websocket = true;
345        self.config.runtime_plan.live_runtime = true;
346        self
347    }
348
349    /// Configure WebSocket server
350    pub fn websocket_config(mut self, config: WebSocketConfig) -> Self {
351        self.config.websocket = Some(config);
352        self.config.runtime_plan.websocket = true;
353        self.config.runtime_plan.live_runtime = true;
354        self
355    }
356
357    /// Set a WebSocket auth plugin used to authorize inbound connections.
358    pub fn websocket_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
359        self.websocket_auth_plugin = Some(plugin);
360        self
361    }
362
363    /// Set an HTTP auth plugin used to authorize inbound read requests.
364    pub fn http_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
365        self.http_auth_plugin = Some(plugin);
366        self
367    }
368
369    /// Set an async usage emitter for billing-grade websocket usage events.
370    pub fn owner_token_accounts_provider(
371        mut self,
372        provider: Arc<dyn crate::token_discovery::OwnerTokenAccountsProvider>,
373    ) -> Self {
374        self.token_accounts_provider = Some(provider);
375        self
376    }
377
378    pub fn websocket_usage_emitter(mut self, emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
379        self.websocket_usage_emitter = Some(emitter);
380        self
381    }
382
383    /// Set the maximum number of concurrent WebSocket clients.
384    pub fn websocket_max_clients(mut self, max_clients: usize) -> Self {
385        self.websocket_max_clients = Some(max_clients);
386        self
387    }
388
389    /// Configure rate limiting for WebSocket connections.
390    ///
391    /// This sets global rate limits such as maximum connections per IP,
392    /// timeouts, and rate windows. Per-subject limits are controlled
393    /// via AuthContext.Limits from the authentication token.
394    pub fn websocket_rate_limit_config(
395        mut self,
396        config: crate::websocket::client_manager::RateLimitConfig,
397    ) -> Self {
398        self.websocket_rate_limit_config = Some(config);
399        self
400    }
401
402    /// Install an embedder-owned provider for cross-runtime admission.
403    pub fn websocket_admission_provider(
404        mut self,
405        provider: Arc<dyn WebSocketAdmissionProvider>,
406    ) -> Self {
407        self.websocket_admission_provider = Some(provider);
408        self
409    }
410
411    /// Configure list-view buffering and latest-state collection delivery.
412    pub fn websocket_delivery_config(mut self, config: WebSocketDeliveryConfig) -> Self {
413        self.config.websocket_delivery = Some(config);
414        self.config.runtime_plan.live_runtime = true;
415        self
416    }
417
418    /// Configure WebSocket message, frame and handshake limits.
419    pub fn websocket_limits(mut self, config: WebSocketLimitsConfig) -> Self {
420        self.config.websocket_limits = Some(config);
421        self
422    }
423
424    /// Configure HTTP body, header and read RPC limits.
425    pub fn http_limits(mut self, config: HttpLimitsConfig) -> Self {
426        self.config.http_limits = Some(config);
427        self
428    }
429
430    /// Set the bind address for WebSocket server
431    pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
432        if let Some(ws_config) = &mut self.config.websocket {
433            ws_config.bind_address = addr.into();
434        } else {
435            self.config.websocket = Some(WebSocketConfig::new(addr.into()));
436        }
437        self.config.runtime_plan.websocket = true;
438        self.config.runtime_plan.live_runtime = true;
439        self
440    }
441
442    /// Configure Yellowstone gRPC connection
443    pub fn yellowstone(mut self, config: YellowstoneConfig) -> Self {
444        self.config.yellowstone = Some(config);
445        self.config.runtime_plan.live_runtime = true;
446        self
447    }
448
449    /// Enable health monitoring with default configuration
450    pub fn health_monitoring(mut self) -> Self {
451        self.config.health = Some(HealthConfig::default());
452        self.config.runtime_plan.health = true;
453        self
454    }
455
456    /// Configure health monitoring
457    pub fn health_config(mut self, config: HealthConfig) -> Self {
458        self.config.health = Some(config);
459        self.config.runtime_plan.health = true;
460        self
461    }
462
463    /// Configure state snapshots (restart recovery). Without this call the
464    /// server falls back to `SnapshotConfig::from_env()` (`ARETE_SNAPSHOT_*`).
465    pub fn snapshots(mut self, config: crate::snapshot::SnapshotConfig) -> Self {
466        self.config.snapshots = Some(config);
467        self
468    }
469
470    /// Retain published events for replayable append subscriptions on this
471    /// runtime, overriding `ARETE_JOURNAL_*` for this instance only.
472    pub fn journal(mut self, config: crate::journal::JournalConfig) -> Self {
473        self.config.journal = Some(config);
474        self
475    }
476
477    /// Enable reconnection with default configuration
478    pub fn reconnection(mut self) -> Self {
479        self.config.reconnection = Some(ReconnectionConfig::default());
480        self
481    }
482
483    /// Configure reconnection behavior
484    pub fn reconnection_config(mut self, config: ReconnectionConfig) -> Self {
485        self.config.reconnection = Some(config);
486        self
487    }
488
489    /// Enable the HTTP server with default configuration (port 8081).
490    ///
491    /// This serves health endpoints plus stack-scoped HTTP reads.
492    pub fn http(mut self) -> Self {
493        self.config.http_health = Some(HttpHealthConfig::default());
494        self.config.runtime_plan.health = true;
495        self.config.runtime_plan.chain_reads = true;
496        self.config.runtime_plan.program_reads = true;
497        self.config.runtime_plan.stack_queries = true;
498        self
499    }
500
501    /// Configure the HTTP server.
502    pub fn http_config(mut self, config: crate::http_server::HttpServerConfig) -> Self {
503        self.config.http_health = Some(config);
504        self.config.runtime_plan.health = true;
505        self.config.runtime_plan.chain_reads = true;
506        self.config.runtime_plan.program_reads = true;
507        self.config.runtime_plan.stack_queries = true;
508        self
509    }
510
511    /// Configure and explicitly enable the fixed transaction HTTP routes.
512    pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
513        self.config.runtime_plan.transactions = config.enabled;
514        self.config.transactions = Some(config);
515        self
516    }
517
518    /// Replace the inferred capability set with an explicit runtime plan.
519    pub fn runtime_plan(mut self, plan: RuntimePlan) -> Self {
520        self.config.runtime_plan = plan;
521        self
522    }
523
524    /// Enable only health and release-pinned program reads over HTTP.
525    pub fn program_reads(mut self) -> Self {
526        if self.config.http_health.is_none() {
527            self.config.http_health = Some(HttpHealthConfig::default());
528        }
529        self.config.runtime_plan.health = true;
530        self.config.runtime_plan.program_reads = true;
531        self
532    }
533
534    /// Enable program reads bound to one exact hosted program-read target.
535    pub fn program_read_binding(mut self, target_id: impl Into<String>) -> Self {
536        if self.config.http_health.is_none() {
537            self.config.http_health = Some(HttpHealthConfig::default());
538        }
539        self.config.runtime_plan.health = true;
540        self.config.runtime_plan.program_reads = true;
541        self.config.program_read_binding_target_id = Some(target_id.into());
542        self
543    }
544
545    pub fn chain_reads(mut self) -> Self {
546        if self.config.http_health.is_none() {
547            self.config.http_health = Some(HttpHealthConfig::default());
548        }
549        self.config.runtime_plan.chain_reads = true;
550        self
551    }
552
553    pub fn stack_queries(mut self) -> Self {
554        if self.config.http_health.is_none() {
555            self.config.http_health = Some(HttpHealthConfig::default());
556        }
557        self.config.runtime_plan.stack_queries = true;
558        self
559    }
560
561    pub fn live_runtime(mut self) -> Self {
562        self.config.runtime_plan.live_runtime = true;
563        self
564    }
565
566    /// Set the bind address for the HTTP server.
567    pub fn http_bind(mut self, addr: impl Into<SocketAddr>) -> Self {
568        if let Some(http_config) = &mut self.config.http_health {
569            http_config.bind_address = addr.into();
570        } else {
571            self.config.http_health = Some(HttpHealthConfig::new(addr.into()));
572        }
573        self.config.runtime_plan.health = true;
574        self.config.runtime_plan.chain_reads = true;
575        self.config.runtime_plan.program_reads = true;
576        self.config.runtime_plan.stack_queries = true;
577        self
578    }
579
580    /// Enable HTTP health server with default configuration (port 8081)
581    pub fn http_health(self) -> Self {
582        self.http()
583    }
584
585    /// Configure HTTP health server
586    pub fn http_health_config(self, config: HttpHealthConfig) -> Self {
587        self.http_config(config)
588    }
589
590    /// Set the bind address for HTTP health server
591    pub fn health_bind(self, addr: impl Into<SocketAddr>) -> Self {
592        self.http_bind(addr)
593    }
594
595    /// Build, start and block until shutdown. See [`Runtime::run`].
596    pub async fn start(self) -> Result<()> {
597        self.build()?.run().await
598    }
599
600    /// Build the [`Runtime`] without starting it.
601    ///
602    /// For callers that embed the server: `build` then [`Runtime::spawn`]
603    /// gives a [`runtime::RuntimeHandle`] that serves connections the caller
604    /// accepted and can be shut down on demand.
605    pub fn build(self) -> Result<Runtime> {
606        let (view_index, materialized_registry) =
607            Self::build_view_index_and_registry(self.views, self.materialized_views, &self.spec);
608
609        view_index.validate_scope_projections()?;
610        #[cfg(feature = "otel")]
611        let mut runtime = Runtime::new(self.config, view_index, self.metrics);
612        #[cfg(not(feature = "otel"))]
613        let mut runtime = Runtime::new(self.config, view_index);
614
615        if let Some(plugin) = self.websocket_auth_plugin {
616            runtime = runtime.with_websocket_auth_plugin(plugin);
617        }
618
619        if let Some(plugin) = self.http_auth_plugin {
620            runtime = runtime.with_http_auth_plugin(plugin);
621        }
622
623        if let Some(emitter) = self.websocket_usage_emitter {
624            runtime = runtime.with_websocket_usage_emitter(emitter);
625        }
626
627        if let Some(provider) = self.token_accounts_provider {
628            runtime = runtime.with_owner_token_accounts_provider(provider);
629        }
630
631        if let Some(observer) = self.solana_gateway_usage_observer {
632            runtime = runtime.with_solana_gateway_usage_observer(observer);
633        }
634
635        if let Some(max_clients) = self.websocket_max_clients {
636            runtime = runtime.with_websocket_max_clients(max_clients);
637        }
638
639        if let Some(rate_limit_config) = self.websocket_rate_limit_config {
640            runtime = runtime.with_websocket_rate_limit_config(rate_limit_config);
641        }
642
643        if let Some(provider) = self.websocket_admission_provider {
644            runtime = runtime.with_websocket_admission_provider(provider);
645        }
646
647        if let Some(registry) = materialized_registry {
648            runtime = runtime.with_materialized_views(registry);
649        }
650
651        if let Some(spec) = self.spec {
652            runtime = runtime.with_spec(spec)?;
653        }
654
655        Ok(runtime)
656    }
657
658    fn build_view_index_and_registry(
659        views: Option<ViewIndex>,
660        materialized_views: Option<MaterializedViewRegistry>,
661        spec: &Option<Spec>,
662    ) -> (ViewIndex, Option<MaterializedViewRegistry>) {
663        let mut index = views.unwrap_or_default();
664        let mut registry = materialized_views;
665
666        if let Some(ref spec) = spec {
667            let entity_wire_formats = spec
668                .entity_specs
669                .iter()
670                .map(|entity_spec| {
671                    (
672                        entity_spec.state_name.clone(),
673                        ViewSpec::wire_format_from_entity_spec(entity_spec),
674                    )
675                })
676                .collect::<std::collections::HashMap<_, _>>();
677
678            // Row scoping comes with the entity spec, so a scoped entity is
679            // never served without it.
680            for entity_spec in &spec.entity_specs {
681                if let Some(path) = entity_spec.scope_field.as_deref() {
682                    index.set_entity_scope(
683                        entity_spec.state_name.clone(),
684                        row_scope::ScopeField::new(path),
685                    );
686                }
687            }
688
689            for entity_name in spec.bytecode.entities.keys() {
690                let wire_format = entity_wire_formats
691                    .get(entity_name)
692                    .cloned()
693                    .unwrap_or_default();
694                index.add_spec(ViewSpec {
695                    id: format!("{}/list", entity_name),
696                    export: entity_name.clone(),
697                    mode: Mode::List,
698                    wire_format: wire_format.clone(),
699                    projection: Projection::all(),
700                    filters: Filters::all(),
701                    delivery: Delivery::default(),
702                    pipeline: None,
703                    source_view: None,
704                });
705
706                index.add_spec(ViewSpec {
707                    id: format!("{}/state", entity_name),
708                    export: entity_name.clone(),
709                    mode: Mode::State,
710                    wire_format: wire_format.clone(),
711                    projection: Projection::all(),
712                    filters: Filters::all(),
713                    delivery: Delivery::default(),
714                    pipeline: None,
715                    source_view: None,
716                });
717
718                index.add_spec(ViewSpec {
719                    id: format!("{}/append", entity_name),
720                    export: entity_name.clone(),
721                    mode: Mode::Append,
722                    wire_format,
723                    projection: Projection::all(),
724                    filters: Filters::all(),
725                    delivery: Delivery::default(),
726                    pipeline: None,
727                    source_view: None,
728                });
729            }
730
731            if !spec.views.is_empty() {
732                let reg = registry.get_or_insert_with(MaterializedViewRegistry::new);
733
734                for view_def in &spec.views {
735                    let export = match &view_def.source {
736                        arete_interpreter::ast::ViewSource::Entity { name } => name.clone(),
737                        arete_interpreter::ast::ViewSource::View { id } => {
738                            id.split('/').next().unwrap_or(id).to_string()
739                        }
740                    };
741
742                    // The compiler includes the canonical entity list/state views in
743                    // `spec.views`. They are already registered above as native views
744                    // backed directly by EntityCache. Registering them again through
745                    // `from_view_def` marks them as derived and replaces the native
746                    // by-id entry, so subscribers read an unpopulated derived cache
747                    // while the projector continues writing to the native cache.
748                    if Self::is_canonical_entity_view(view_def, &export) {
749                        tracing::debug!(
750                            view_id = %view_def.id,
751                            "Keeping canonical entity view backed by the native cache"
752                        );
753                        continue;
754                    }
755
756                    let wire_format = entity_wire_formats
757                        .get(&export)
758                        .cloned()
759                        .unwrap_or_default();
760                    let view_spec = ViewSpec::from_view_def(view_def, &export, wire_format);
761                    let pipeline = view_spec.pipeline.clone().unwrap_or_default();
762                    let source_id = view_spec.source_view.clone().unwrap_or_default();
763                    tracing::debug!(
764                        view_id = %view_def.id,
765                        source = %source_id,
766                        "Registering derived view"
767                    );
768
769                    index.add_spec(view_spec);
770
771                    let materialized =
772                        MaterializedView::new(view_def.id.clone(), source_id, pipeline);
773                    reg.register(materialized);
774                }
775            }
776        }
777
778        (index, registry)
779    }
780
781    fn is_canonical_entity_view(view_def: &ViewDef, export: &str) -> bool {
782        use arete_interpreter::ast::{ViewOutput, ViewSource};
783
784        let ViewSource::Entity { name } = &view_def.source else {
785            return false;
786        };
787        if name != export || !view_def.pipeline.is_empty() {
788            return false;
789        }
790
791        match &view_def.output {
792            ViewOutput::Collection => view_def.id == format!("{export}/list"),
793            ViewOutput::Single | ViewOutput::Keyed { .. } => {
794                view_def.id == format!("{export}/state")
795            }
796        }
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    #[test]
805    fn test_builder_pattern() {
806        let _builder = Server::builder()
807            .websocket()
808            .bind("[::]:8877".parse::<SocketAddr>().unwrap());
809    }
810
811    #[test]
812    fn test_spec_creation() {
813        let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
814        let spec = Spec::new(bytecode, "test_program");
815        assert_eq!(
816            spec.program_ids.first().map(String::as_str),
817            Some("test_program")
818        );
819    }
820
821    #[test]
822    fn http_without_websocket_has_a_read_only_runtime_plan() {
823        let builder = Server::builder().http();
824        assert!(builder.config.runtime_plan.program_reads);
825        assert!(!builder.config.runtime_plan.live_runtime_enabled());
826    }
827
828    #[test]
829    fn program_read_binding_configures_the_exact_auth_target() {
830        let builder = Server::builder().program_read_binding("binding-1");
831
832        assert!(builder.config.runtime_plan.program_reads);
833        assert_eq!(
834            builder.config.program_read_binding_target_id.as_deref(),
835            Some("binding-1")
836        );
837    }
838
839    #[test]
840    fn websocket_and_http_preserve_all_in_one_runtime_behavior() {
841        let builder = Server::builder().websocket().http();
842        assert!(builder.config.runtime_plan.websocket);
843        assert!(builder.config.runtime_plan.live_runtime_enabled());
844    }
845
846    #[test]
847    fn explicit_hosted_plan_disables_program_reads_after_http_helpers() {
848        let plan = RuntimePlan {
849            health: true,
850            chain_reads: true,
851            program_reads: false,
852            stack_queries: true,
853            transactions: true,
854            websocket: true,
855            live_runtime: true,
856        };
857        let builder = Server::builder()
858            .websocket()
859            .http_health()
860            .health_bind("[::]:8081".parse::<SocketAddr>().unwrap())
861            .runtime_plan(plan);
862
863        assert_eq!(builder.config.runtime_plan, plan);
864    }
865
866    #[test]
867    fn solana_gateway_builder_excludes_stack_and_live_capabilities() {
868        let builder = Server::solana_gateway("gateway-us-east-1");
869
870        assert_eq!(
871            builder.inner.config.runtime_plan,
872            RuntimePlan::solana_gateway()
873        );
874        assert!(builder.inner.config.http_health.is_some());
875        assert_eq!(
876            builder.inner.config.solana_gateway_target_id.as_deref(),
877            Some("gateway-us-east-1")
878        );
879        assert!(builder.inner.config.websocket.is_none());
880        assert!(builder.inner.config.yellowstone.is_none());
881        assert!(builder.inner.spec.is_none());
882        assert!(builder.inner.views.is_none());
883        assert!(builder.inner.materialized_views.is_none());
884        assert!(builder.inner.solana_gateway_usage_observer.is_none());
885        assert!(!builder.inner.config.runtime_plan.websocket);
886        assert!(!builder.inner.config.runtime_plan.live_runtime_enabled());
887        assert!(!builder.inner.config.runtime_plan.stack_queries);
888        assert!(!builder.inner.config.runtime_plan.program_reads);
889    }
890
891    #[test]
892    fn solana_gateway_builder_rejects_invalid_gateway_configuration() {
893        assert!(Server::solana_gateway("").build().is_err());
894        assert!(Server::solana_gateway("gateway-us-east-1")
895            .transactions_config(TransactionConfig::default())
896            .build()
897            .is_err());
898    }
899
900    #[test]
901    fn builder_rejects_mismatched_program_release_definitions() {
902        let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
903        let definition = ProgramRuntimeDefinition {
904            program_id: "Program111".to_string(),
905            program_spec_hash: ProgramSpecHash::from_digest([1; 32]),
906            idl_content_hash: IdlContentHash::from_digest([2; 32]),
907            normalized_idl_hash: NormalizedIdlHash::from_digest([3; 32]),
908            program_release_hash: ProgramReleaseHash::from_digest([4; 32]),
909            account_reader: Arc::new(|_, _| Ok(serde_json::Value::Null)),
910        };
911        let spec =
912            Spec::new(bytecode, "Program111").with_program_runtime_definitions(vec![definition]);
913
914        assert!(Server::builder().spec(spec).build().is_err());
915    }
916
917    #[test]
918    fn canonical_entity_views_are_not_reclassified_as_derived_views() {
919        use arete_interpreter::ast::{IdentitySpec, TypedStreamSpec, ViewDef, ViewSource};
920
921        let list = ViewDef::list("OreBoard");
922        let state = ViewDef::state("OreBoard", &["id", "address"]);
923        assert!(ServerBuilder::is_canonical_entity_view(&list, "OreBoard"));
924        assert!(ServerBuilder::is_canonical_entity_view(&state, "OreBoard"));
925
926        let mut named_view = ViewDef::list("OreBoard");
927        named_view.id = "OreBoard/all".to_string();
928        assert!(!ServerBuilder::is_canonical_entity_view(
929            &named_view,
930            "OreBoard"
931        ));
932
933        let mut chained_view = ViewDef::list("OreBoard");
934        chained_view.source = ViewSource::View {
935            id: "OreBoard/list".to_string(),
936        };
937        assert!(!ServerBuilder::is_canonical_entity_view(
938            &chained_view,
939            "OreBoard"
940        ));
941
942        let entity_spec = TypedStreamSpec::<serde_json::Value>::new(
943            "OreBoard".to_string(),
944            IdentitySpec {
945                primary_keys: vec!["id.address".to_string()],
946                lookup_indexes: Vec::new(),
947            },
948            Vec::new(),
949        );
950        let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new()
951            .add_entity("OreBoard".to_string(), entity_spec, 1)
952            .build();
953        let spec = Some(Spec::new(bytecode, "Program111").with_views(vec![list, state]));
954
955        let (index, _) = ServerBuilder::build_view_index_and_registry(None, None, &spec);
956        assert!(!index
957            .get_view("OreBoard/list")
958            .expect("list view should exist")
959            .is_derived());
960        assert!(!index
961            .get_view("OreBoard/state")
962            .expect("state view should exist")
963            .is_derived());
964        assert_eq!(index.by_export("OreBoard").len(), 3);
965    }
966
967    #[test]
968    fn an_entity_specs_scope_field_scopes_every_view_of_the_entity() {
969        use arete_interpreter::ast::{
970            FieldPath, IdentitySpec, SortOrder, TypedStreamSpec, ViewDef, ViewOutput, ViewSource,
971            ViewTransform,
972        };
973
974        let identity = IdentitySpec {
975            primary_keys: vec!["id.address".to_string()],
976            lookup_indexes: Vec::new(),
977        };
978        let scoped = TypedStreamSpec::<serde_json::Value>::new(
979            "Round".to_string(),
980            identity.clone(),
981            Vec::new(),
982        );
983        let plain =
984            TypedStreamSpec::<serde_json::Value>::new("Board".to_string(), identity, Vec::new());
985        let mut scoped_spec = scoped.to_serializable();
986        scoped_spec.scope_field = Some("meta.scope".to_string());
987        let plain_spec = plain.to_serializable();
988        let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new()
989            .add_entity("Round".to_string(), scoped, 1)
990            .add_entity("Board".to_string(), plain, 1)
991            .build();
992        // A derived view whose id does not name its entity.
993        let top = ViewDef {
994            id: "Leaders/top".to_string(),
995            source: ViewSource::Entity {
996                name: "Round".to_string(),
997            },
998            pipeline: vec![ViewTransform::Sort {
999                key: FieldPath::new(&["amount"]),
1000                order: SortOrder::Desc,
1001            }],
1002            output: ViewOutput::Collection,
1003        };
1004        let spec = Some(
1005            Spec::new(bytecode, "Program111")
1006                .with_entity_specs(vec![scoped_spec, plain_spec])
1007                .with_views(vec![top]),
1008        );
1009
1010        let (index, _) = ServerBuilder::build_view_index_and_registry(None, None, &spec);
1011        for view in ["Round/list", "Round/state", "Round/append", "Leaders/top"] {
1012            assert_eq!(
1013                index
1014                    .scope_field_for_view(view)
1015                    .map(|field| field.path().to_vec()),
1016                Some(vec!["meta".to_string(), "scope".to_string()]),
1017                "{view}"
1018            );
1019        }
1020        assert!(index.scope_field_for_view("Board/list").is_none());
1021        // A custom projection cannot strip the scope, including when its
1022        // field is nested. Reject it at build time, before serving any data.
1023        let mut custom = index.get_view("Round/list").unwrap().clone();
1024        custom.id = "Round/custom".into();
1025        custom.projection.fields = Some(vec!["amount".into()]);
1026        let mut invalid = ViewIndex::new();
1027        invalid.set_entity_scope("Round", row_scope::ScopeField::new("meta.scope"));
1028        invalid.add_spec(custom.clone());
1029        assert!(ServerBuilder::new().views(invalid).build().is_err());
1030        custom.projection.fields = Some(vec!["amount".into(), "meta".into()]);
1031        let mut valid = ViewIndex::new();
1032        valid.set_entity_scope("Round", row_scope::ScopeField::new("meta.scope"));
1033        valid.add_spec(custom);
1034        assert!(ServerBuilder::new().views(valid).build().is_ok());
1035        let caches = index.sorted_caches();
1036        let caches = caches.try_read().unwrap();
1037        assert!(caches.get("Leaders/top").unwrap().is_scope_partitioned());
1038    }
1039}