Skip to main content

reifydb_sub_server/
actor.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::collections::HashMap;
5
6use reifydb_auth::service::{AuthResponse, AuthService};
7use reifydb_core::{
8	actors::server::{
9		ServerAuthResponse, ServerLogoutResponse, ServerMessage, ServerResponse, ServerSubscribeResponse,
10	},
11	execution::ExecutionResult,
12	interface::catalog::procedure::Procedure,
13};
14use reifydb_engine::{engine::StandardEngine, session::RetryStrategy};
15use reifydb_runtime::{
16	actor::{
17		context::Context,
18		reply::Reply,
19		traits::{Actor, Directive},
20	},
21	context::clock::Clock,
22};
23use reifydb_value::{
24	error::Diagnostic,
25	params::Params,
26	value::{duration::Duration, identity::IdentityId},
27};
28
29pub struct ServerActor {
30	engine: StandardEngine,
31	auth_service: AuthService,
32	clock: Clock,
33	retry: RetryStrategy,
34}
35
36impl ServerActor {
37	pub fn new(engine: StandardEngine, auth_service: AuthService, clock: Clock) -> Self {
38		Self {
39			engine,
40			auth_service,
41			clock,
42			retry: RetryStrategy::default(),
43		}
44	}
45
46	fn dispatch_execute(
47		&self,
48		identity: IdentityId,
49		rql: String,
50		params: Params,
51		reply: Reply<ServerResponse>,
52		execute: impl Fn(&StandardEngine, IdentityId, &str, Params) -> ExecutionResult,
53	) {
54		let t = self.clock.instant();
55		let result = self
56			.retry
57			.execute(self.engine.rng(), &rql, || execute(&self.engine, identity, &rql, params.clone()));
58		if let Some(err) = result.error {
59			reply.send(ServerResponse::EngineError {
60				diagnostic: Box::new(err.diagnostic()),
61				rql,
62			});
63		} else {
64			reply.send(ServerResponse::Success {
65				frames: result.frames,
66				duration: Duration::from_std(t.elapsed()),
67				metrics: result.metrics,
68			});
69		}
70	}
71
72	#[inline]
73	fn handle_call(&self, identity: IdentityId, name: String, params: Params, reply: Reply<ServerResponse>) {
74		let catalog = self.engine.catalog();
75		let cache = catalog.cache();
76		let binding = match cache.find_ws_binding_by_name(&name) {
77			Some(binding) => binding,
78			None => {
79				return reply.send(call_error(
80					"NOT_FOUND",
81					format!("no WS binding named `{}`", name),
82					name,
83				));
84			}
85		};
86		let procedure = match cache.find_procedure(binding.procedure_id) {
87			Some(procedure) => procedure,
88			None => {
89				return reply.send(call_error(
90					"INTERNAL_ERROR",
91					"binding references missing procedure".to_string(),
92					name,
93				));
94			}
95		};
96		let namespace = match cache.find_namespace(binding.namespace) {
97			Some(namespace) => namespace,
98			None => {
99				return reply.send(call_error(
100					"INTERNAL_ERROR",
101					"binding references missing namespace".to_string(),
102					name,
103				));
104			}
105		};
106		if let Err(diagnostic) = validate_call_params(&procedure, &params) {
107			return reply.send(ServerResponse::EngineError {
108				diagnostic,
109				rql: name,
110			});
111		}
112		let rql = format!("CALL {}::{}()", namespace.name(), procedure.name());
113		self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| e.command_as(id, s, p));
114	}
115
116	#[inline]
117	fn handle_subscribe(
118		&self,
119		identity: IdentityId,
120		rql: String,
121		params: Params,
122		reply: Reply<ServerSubscribeResponse>,
123	) {
124		let t = self.clock.instant();
125		let result = self.engine.subscribe_as(identity, &rql, params);
126		if let Some(err) = result.error {
127			reply.send(ServerSubscribeResponse::EngineError {
128				diagnostic: Box::new(err.diagnostic()),
129				rql,
130			});
131		} else {
132			reply.send(ServerSubscribeResponse::Subscribed {
133				frames: result.frames,
134				duration: Duration::from_std(t.elapsed()),
135				metrics: result.metrics,
136			});
137		}
138	}
139
140	#[inline]
141	fn handle_authenticate(
142		&self,
143		method: String,
144		credentials: HashMap<String, String>,
145		reply: Reply<ServerAuthResponse>,
146	) {
147		match self.auth_service.authenticate(&method, credentials) {
148			Ok(AuthResponse::Authenticated {
149				identity,
150				token,
151			}) => reply.send(ServerAuthResponse::Authenticated {
152				identity,
153				token,
154			}),
155			Ok(AuthResponse::Challenge {
156				challenge_id,
157				payload,
158			}) => reply.send(ServerAuthResponse::Challenge {
159				challenge_id,
160				payload,
161			}),
162			Ok(AuthResponse::Failed {
163				reason,
164			}) => reply.send(ServerAuthResponse::Failed {
165				reason,
166			}),
167			Err(e) => reply.send(ServerAuthResponse::Error(e.to_string())),
168		}
169	}
170
171	#[inline]
172	fn handle_logout(&self, token: String, reply: Reply<ServerLogoutResponse>) {
173		if self.auth_service.revoke_token(&token) {
174			reply.send(ServerLogoutResponse::Ok);
175		} else {
176			reply.send(ServerLogoutResponse::InvalidToken);
177		}
178	}
179}
180
181impl Actor for ServerActor {
182	type State = ();
183	type Message = ServerMessage;
184
185	fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {}
186
187	fn handle(&self, _state: &mut (), msg: ServerMessage, _ctx: &Context<ServerMessage>) -> Directive {
188		match msg {
189			ServerMessage::Query {
190				identity,
191				rql,
192				params,
193				reply,
194			} => {
195				self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| e.query_as(id, s, p));
196			}
197			ServerMessage::Command {
198				identity,
199				rql,
200				params,
201				reply,
202			} => {
203				self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| {
204					e.command_as(id, s, p)
205				});
206			}
207			ServerMessage::Call {
208				identity,
209				name,
210				params,
211				reply,
212			} => {
213				self.handle_call(identity, name, params, reply);
214			}
215			ServerMessage::Admin {
216				identity,
217				rql,
218				params,
219				reply,
220			} => {
221				self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| e.admin_as(id, s, p));
222			}
223			ServerMessage::Subscribe {
224				identity,
225				rql,
226				params,
227				reply,
228			} => {
229				self.handle_subscribe(identity, rql, params, reply);
230			}
231			ServerMessage::Authenticate {
232				method,
233				credentials,
234				reply,
235			} => {
236				self.handle_authenticate(method, credentials, reply);
237			}
238			ServerMessage::Logout {
239				token,
240				reply,
241			} => {
242				self.handle_logout(token, reply);
243			}
244		}
245		Directive::Continue
246	}
247}
248
249fn call_error(code: &str, message: String, rql: String) -> ServerResponse {
250	ServerResponse::EngineError {
251		diagnostic: call_diagnostic(code, message),
252		rql,
253	}
254}
255
256fn call_diagnostic(code: &str, message: String) -> Box<Diagnostic> {
257	Box::new(Diagnostic {
258		code: code.to_string(),
259		message,
260		..Default::default()
261	})
262}
263
264fn validate_call_params(procedure: &Procedure, params: &Params) -> Result<(), Box<Diagnostic>> {
265	match params {
266		Params::None => {
267			if let Some(p) = procedure.params().first() {
268				return Err(call_diagnostic(
269					"INVALID_PARAMS",
270					format!("missing required parameter `{}`", p.name),
271				));
272			}
273		}
274		Params::Named(map) => {
275			for key in map.keys() {
276				if !procedure.params().iter().any(|p| &p.name == key) {
277					return Err(call_diagnostic(
278						"INVALID_PARAMS",
279						format!("unknown parameter `{}`", key),
280					));
281				}
282			}
283			for p in procedure.params() {
284				if !map.contains_key(&p.name) {
285					return Err(call_diagnostic(
286						"INVALID_PARAMS",
287						format!("missing required parameter `{}`", p.name),
288					));
289				}
290			}
291		}
292		Params::Positional(_) => {
293			return Err(call_diagnostic("INVALID_PARAMS", "Call requires named params".to_string()));
294		}
295	}
296	Ok(())
297}