Skip to main content

seam_server/
server.rs

1/* src/server/core/rust/src/server.rs */
2
3use std::collections::BTreeMap;
4use std::path::PathBuf;
5use std::time::Duration;
6
7use crate::build_loader::BuildOutput;
8use crate::build_loader::RpcHashMap;
9use crate::channel::{ChannelDef, ChannelMeta};
10use crate::context::{ContextConfig, ContextFieldDef};
11use crate::page::{I18nConfig, PageDef};
12use crate::procedure::{ProcedureDef, StreamDef, SubscriptionDef, UploadDef};
13use crate::resolve::ResolveStrategy;
14use crate::validation::ValidationMode;
15
16/// Transport reliability configuration shared across all backends.
17pub struct TransportConfig {
18	pub heartbeat_interval: Duration,
19	pub sse_idle_timeout: Duration,
20	pub pong_timeout: Duration,
21}
22
23impl Default for TransportConfig {
24	fn default() -> Self {
25		Self {
26			heartbeat_interval: Duration::from_secs(8),
27			sse_idle_timeout: Duration::from_secs(12),
28			pong_timeout: Duration::from_secs(5),
29		}
30	}
31}
32
33/// Framework-agnostic parts extracted from `SeamServer`.
34/// Adapter crates consume this to build framework-specific routers.
35pub struct SeamParts {
36	pub procedures: Vec<ProcedureDef>,
37	pub subscriptions: Vec<SubscriptionDef>,
38	pub streams: Vec<StreamDef>,
39	pub uploads: Vec<UploadDef>,
40	pub pages: Vec<PageDef>,
41	pub rpc_hash_map: Option<RpcHashMap>,
42	pub i18n_config: Option<I18nConfig>,
43	pub public_dir: Option<PathBuf>,
44	pub strategies: Vec<Box<dyn ResolveStrategy>>,
45	pub channel_metas: BTreeMap<String, ChannelMeta>,
46	pub context_config: ContextConfig,
47	pub validation_mode: ValidationMode,
48	pub transport_config: TransportConfig,
49}
50
51impl SeamParts {
52	pub fn has_url_prefix(&self) -> bool {
53		self.strategies.iter().any(|s| s.kind() == "url_prefix")
54	}
55}
56
57pub struct SeamServer {
58	procedures: Vec<ProcedureDef>,
59	subscriptions: Vec<SubscriptionDef>,
60	streams: Vec<StreamDef>,
61	uploads: Vec<UploadDef>,
62	channels: Vec<ChannelDef>,
63	pages: Vec<PageDef>,
64	rpc_hash_map: Option<RpcHashMap>,
65	i18n_config: Option<I18nConfig>,
66	public_dir: Option<PathBuf>,
67	strategies: Vec<Box<dyn ResolveStrategy>>,
68	context_config: ContextConfig,
69	validation_mode: ValidationMode,
70	transport_config: TransportConfig,
71}
72
73impl SeamServer {
74	pub fn new() -> Self {
75		Self {
76			procedures: Vec::new(),
77			subscriptions: Vec::new(),
78			streams: Vec::new(),
79			uploads: Vec::new(),
80			channels: Vec::new(),
81			pages: Vec::new(),
82			rpc_hash_map: None,
83			i18n_config: None,
84			public_dir: None,
85			strategies: Vec::new(),
86			context_config: ContextConfig::new(),
87			validation_mode: ValidationMode::Dev,
88			transport_config: TransportConfig::default(),
89		}
90	}
91
92	pub fn procedure(mut self, proc: ProcedureDef) -> Self {
93		self.procedures.push(proc);
94		self
95	}
96
97	pub fn subscription(mut self, sub: SubscriptionDef) -> Self {
98		self.subscriptions.push(sub);
99		self
100	}
101
102	pub fn stream(mut self, stream: StreamDef) -> Self {
103		self.streams.push(stream);
104		self
105	}
106
107	pub fn upload(mut self, upload: UploadDef) -> Self {
108		self.uploads.push(upload);
109		self
110	}
111
112	pub fn channel(mut self, channel: ChannelDef) -> Self {
113		self.channels.push(channel);
114		self
115	}
116
117	/// Register procedures under a dot-separated namespace prefix (e.g. "blog" -> "blog.getPost").
118	pub fn namespace(mut self, prefix: &str, procedures: Vec<ProcedureDef>) -> Self {
119		for mut p in procedures {
120			p.name = format!("{prefix}.{}", p.name);
121			self.procedures.push(p);
122		}
123		self
124	}
125
126	/// Register subscriptions under a dot-separated namespace prefix.
127	pub fn namespace_subs(mut self, prefix: &str, subs: Vec<SubscriptionDef>) -> Self {
128		for mut s in subs {
129			s.name = format!("{prefix}.{}", s.name);
130			self.subscriptions.push(s);
131		}
132		self
133	}
134
135	/// Register streams under a dot-separated namespace prefix.
136	pub fn namespace_streams(mut self, prefix: &str, streams: Vec<StreamDef>) -> Self {
137		for mut s in streams {
138			s.name = format!("{prefix}.{}", s.name);
139			self.streams.push(s);
140		}
141		self
142	}
143
144	pub fn page(mut self, page: PageDef) -> Self {
145		self.pages.push(page);
146		self
147	}
148
149	pub fn rpc_hash_map(mut self, map: RpcHashMap) -> Self {
150		self.rpc_hash_map = Some(map);
151		self
152	}
153
154	pub fn i18n_config(mut self, config: I18nConfig) -> Self {
155		self.i18n_config = Some(config);
156		self
157	}
158
159	pub fn public_dir(mut self, dir: PathBuf) -> Self {
160		self.public_dir = Some(dir);
161		self
162	}
163
164	pub fn build(mut self, build: BuildOutput) -> Self {
165		self.pages.extend(build.pages);
166		if let Some(map) = build.rpc_hash_map {
167			self.rpc_hash_map = Some(map);
168		}
169		if let Some(config) = build.i18n_config {
170			self.i18n_config = Some(config);
171		}
172		if let Some(dir) = build.public_dir {
173			self.public_dir = Some(dir);
174		}
175		self
176	}
177
178	pub fn resolve_strategies(mut self, strategies: Vec<Box<dyn ResolveStrategy>>) -> Self {
179		self.strategies = strategies;
180		self
181	}
182
183	pub fn context(mut self, key: &str, field: ContextFieldDef) -> Self {
184		self.context_config.insert(key.to_string(), field);
185		self
186	}
187
188	pub fn validation_mode(mut self, mode: ValidationMode) -> Self {
189		self.validation_mode = mode;
190		self
191	}
192
193	pub fn transport_config(mut self, config: TransportConfig) -> Self {
194		self.transport_config = config;
195		self
196	}
197
198	/// Consume the builder, returning framework-agnostic parts for an adapter.
199	/// Channels are expanded into their Level 0 primitives (commands + subscriptions).
200	pub fn into_parts(self) -> SeamParts {
201		let mut procedures = self.procedures;
202		let mut subscriptions = self.subscriptions;
203		let mut channel_metas = BTreeMap::new();
204
205		for channel in self.channels {
206			let name = channel.name.clone();
207			let (procs, subs, meta) = channel.expand();
208			procedures.extend(procs);
209			subscriptions.extend(subs);
210			channel_metas.insert(name, meta);
211		}
212
213		SeamParts {
214			procedures,
215			subscriptions,
216			streams: self.streams,
217			uploads: self.uploads,
218			pages: self.pages,
219			rpc_hash_map: self.rpc_hash_map,
220			i18n_config: self.i18n_config,
221			public_dir: self.public_dir,
222			strategies: self.strategies,
223			channel_metas,
224			context_config: self.context_config,
225			validation_mode: self.validation_mode,
226			transport_config: self.transport_config,
227		}
228	}
229}
230
231impl Default for SeamServer {
232	fn default() -> Self {
233		Self::new()
234	}
235}
236
237#[cfg(test)]
238mod tests {
239	use super::TransportConfig;
240	use std::time::Duration;
241
242	#[test]
243	fn transport_config_uses_8_second_heartbeat_and_12_second_idle_timeout_by_default() {
244		let config = TransportConfig::default();
245		assert_eq!(config.heartbeat_interval, Duration::from_secs(8));
246		assert_eq!(config.sse_idle_timeout, Duration::from_secs(12));
247	}
248}