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, subscription::SubscribeOptions},
13	retry::RetryStrategy,
14};
15use reifydb_engine::engine::StandardEngine;
16use reifydb_runtime::{
17	actor::{
18		context::Context,
19		reply::Reply,
20		traits::{Actor, Directive},
21	},
22	context::clock::Clock,
23};
24use reifydb_value::{
25	error::Diagnostic,
26	params::Params,
27	value::{duration::Duration, identity::IdentityId},
28};
29use tracing::error;
30
31pub struct ServerActor {
32	engine: StandardEngine,
33	auth_service: AuthService,
34	clock: Clock,
35	retry: RetryStrategy,
36}
37
38impl ServerActor {
39	pub fn new(engine: StandardEngine, auth_service: AuthService, clock: Clock) -> Self {
40		Self {
41			engine,
42			auth_service,
43			clock,
44			retry: RetryStrategy::default(),
45		}
46	}
47
48	fn dispatch_execute(
49		&self,
50		identity: IdentityId,
51		rql: String,
52		params: Params,
53		reply: Reply<ServerResponse>,
54		execute: impl Fn(&StandardEngine, IdentityId, &str, Params) -> ExecutionResult,
55	) {
56		let t = self.clock.instant();
57		let result = self
58			.retry
59			.execute(self.engine.rng(), &rql, || execute(&self.engine, identity, &rql, params.clone()));
60		if let Some(err) = result.error {
61			reply.send(ServerResponse::EngineError {
62				diagnostic: Box::new(err.diagnostic()),
63				rql,
64			});
65		} else {
66			reply.send(ServerResponse::Success {
67				frames: result.frames,
68				duration: Duration::from_std(t.elapsed()),
69				metrics: result.metrics,
70			});
71		}
72	}
73
74	#[inline]
75	fn handle_call(&self, identity: IdentityId, name: String, params: Params, reply: Reply<ServerResponse>) {
76		let catalog = self.engine.catalog();
77		let cache = catalog.cache();
78		let binding = match cache.find_ws_binding_by_name(&name) {
79			Some(binding) => binding,
80			None => {
81				return reply.send(call_error(
82					"NOT_FOUND",
83					format!("no WS binding named `{}`", name),
84					name,
85				));
86			}
87		};
88		let procedure = match cache.find_procedure(binding.procedure_id) {
89			Some(procedure) => procedure,
90			None => {
91				return reply.send(call_error(
92					"INTERNAL_ERROR",
93					"binding references missing procedure".to_string(),
94					name,
95				));
96			}
97		};
98		let namespace = match cache.find_namespace(binding.namespace) {
99			Some(namespace) => namespace,
100			None => {
101				return reply.send(call_error(
102					"INTERNAL_ERROR",
103					"binding references missing namespace".to_string(),
104					name,
105				));
106			}
107		};
108		if let Err(diagnostic) = validate_call_params(&procedure, &params) {
109			return reply.send(ServerResponse::EngineError {
110				diagnostic,
111				rql: name,
112			});
113		}
114		let rql = format!("CALL {}::{}()", namespace.name(), procedure.name());
115		self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| e.command_as(id, s, p));
116	}
117
118	#[inline]
119	fn handle_subscribe(
120		&self,
121		identity: IdentityId,
122		rql: String,
123		params: Params,
124		options: SubscribeOptions,
125		reply: Reply<ServerSubscribeResponse>,
126	) {
127		let t = self.clock.instant();
128		match self.engine.subscribe_as(identity, &rql, params, options) {
129			Err(err) => reply.send(ServerSubscribeResponse::EngineError {
130				diagnostic: Box::new(err.diagnostic()),
131				rql,
132			}),
133			Ok(outcome) => reply.send(ServerSubscribeResponse::Subscribed {
134				outcome,
135				duration: Duration::from_std(t.elapsed()),
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		match self.auth_service.revoke_token(&token) {
174			Ok(true) => reply.send(ServerLogoutResponse::Ok),
175			Ok(false) => reply.send(ServerLogoutResponse::InvalidToken),
176			Err(e) => {
177				error!("logout could not revoke the session token: {e}");
178				reply.send(ServerLogoutResponse::Error(e.to_string()));
179			}
180		}
181	}
182}
183
184impl Actor for ServerActor {
185	type State = ();
186	type Message = ServerMessage;
187
188	fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {}
189
190	fn handle(&self, _state: &mut (), msg: ServerMessage, _ctx: &Context<ServerMessage>) -> Directive {
191		match msg {
192			ServerMessage::Query {
193				identity,
194				rql,
195				params,
196				reply,
197			} => {
198				self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| e.query_as(id, s, p));
199			}
200			ServerMessage::Command {
201				identity,
202				rql,
203				params,
204				reply,
205			} => {
206				self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| {
207					e.command_as(id, s, p)
208				});
209			}
210			ServerMessage::Call {
211				identity,
212				name,
213				params,
214				reply,
215			} => {
216				self.handle_call(identity, name, params, reply);
217			}
218			ServerMessage::Admin {
219				identity,
220				rql,
221				params,
222				reply,
223			} => {
224				self.dispatch_execute(identity, rql, params, reply, |e, id, s, p| e.admin_as(id, s, p));
225			}
226			ServerMessage::Subscribe {
227				identity,
228				rql,
229				params,
230				options,
231				reply,
232			} => {
233				self.handle_subscribe(identity, rql, params, options, 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}