1pub 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
111pub 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
122pub 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
180pub mod token_discovery;
182
183pub struct Server;
184
185impl Server {
186 pub fn builder() -> ServerBuilder {
188 ServerBuilder::new()
189 }
190
191 pub fn solana_gateway(target_id: impl Into<String>) -> SolanaGatewayBuilder {
193 SolanaGatewayBuilder::new(target_id.into())
194 }
195}
196
197pub 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 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 pub fn auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
223 self.inner.http_auth_plugin = Some(plugin);
224 self
225 }
226
227 pub fn http_limits(mut self, config: HttpLimitsConfig) -> Self {
229 self.inner.config.http_limits = Some(config);
230 self
231 }
232
233 pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
235 self.inner.config.transactions = Some(config);
236 self
237 }
238
239 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 pub fn build(self) -> Result<Runtime> {
275 self.finalize()?.build()
276 }
277
278 pub async fn start(self) -> Result<()> {
280 self.finalize()?.start().await
281 }
282}
283
284pub 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 pub fn spec(mut self, spec: Spec) -> Self {
324 self.spec = Some(spec);
325 self
326 }
327
328 pub fn views(mut self, views: ViewIndex) -> Self {
330 self.views = Some(views);
331 self
332 }
333
334 #[cfg(feature = "otel")]
336 pub fn metrics(mut self, metrics: Metrics) -> Self {
337 self.metrics = Some(Arc::new(metrics));
338 self
339 }
340
341 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 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 pub fn websocket_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
359 self.websocket_auth_plugin = Some(plugin);
360 self
361 }
362
363 pub fn http_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
365 self.http_auth_plugin = Some(plugin);
366 self
367 }
368
369 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 pub fn websocket_max_clients(mut self, max_clients: usize) -> Self {
385 self.websocket_max_clients = Some(max_clients);
386 self
387 }
388
389 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 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 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 pub fn websocket_limits(mut self, config: WebSocketLimitsConfig) -> Self {
420 self.config.websocket_limits = Some(config);
421 self
422 }
423
424 pub fn http_limits(mut self, config: HttpLimitsConfig) -> Self {
426 self.config.http_limits = Some(config);
427 self
428 }
429
430 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 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 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 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 pub fn snapshots(mut self, config: crate::snapshot::SnapshotConfig) -> Self {
466 self.config.snapshots = Some(config);
467 self
468 }
469
470 pub fn journal(mut self, config: crate::journal::JournalConfig) -> Self {
473 self.config.journal = Some(config);
474 self
475 }
476
477 pub fn reconnection(mut self) -> Self {
479 self.config.reconnection = Some(ReconnectionConfig::default());
480 self
481 }
482
483 pub fn reconnection_config(mut self, config: ReconnectionConfig) -> Self {
485 self.config.reconnection = Some(config);
486 self
487 }
488
489 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 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 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 pub fn runtime_plan(mut self, plan: RuntimePlan) -> Self {
520 self.config.runtime_plan = plan;
521 self
522 }
523
524 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 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 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 pub fn http_health(self) -> Self {
582 self.http()
583 }
584
585 pub fn http_health_config(self, config: HttpHealthConfig) -> Self {
587 self.http_config(config)
588 }
589
590 pub fn health_bind(self, addr: impl Into<SocketAddr>) -> Self {
592 self.http_bind(addr)
593 }
594
595 pub async fn start(self) -> Result<()> {
597 self.build()?.run().await
598 }
599
600 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 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 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 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 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}