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