1use alloc::boxed::Box;
22use alloc::string::{String, ToString};
23use alloc::sync::Arc;
24use alloc::vec;
25use alloc::vec::Vec;
26use hashbrown::HashMap;
27
28use serde_json::{json, Value};
29
30use futures_util::StreamExt;
31
32use crate::buffer::JsonBufferReader;
33use crate::remote::{AimxConfig, RecordMetadata, SecurityPolicy, WelcomeMessage, PROTOCOL_VERSION};
34use crate::session::{
35 is_wildcard, topic_matches, AuthError, BoxFut, BoxStream, Dispatch, Payload, PeerInfo,
36 RpcError, Session, SessionCtx, SubUpdate,
37};
38use crate::{AimDb, DbError};
39
40pub struct AimxDispatch {
43 db: Arc<AimDb>,
44 config: Arc<AimxConfig>,
45}
46
47impl AimxDispatch {
48 pub fn new(db: Arc<AimDb>, config: AimxConfig) -> Self {
50 Self {
51 db,
52 config: Arc::new(config),
53 }
54 }
55}
56
57impl Dispatch for AimxDispatch {
58 fn authenticate<'a>(
59 &'a self,
60 _peer: &'a PeerInfo,
61 _first: Option<&'a [u8]>,
62 ) -> BoxFut<'a, Result<SessionCtx, AuthError>> {
63 Box::pin(async { Ok(SessionCtx::default()) })
67 }
68
69 fn open(&self, _ctx: &SessionCtx) -> Box<dyn Session> {
70 Box::new(AimxSession {
71 db: self.db.clone(),
72 config: self.config.clone(),
73 drain_readers: HashMap::new(),
74 })
75 }
76}
77
78struct AimxSession {
82 db: Arc<AimDb>,
83 config: Arc<AimxConfig>,
84 drain_readers: HashMap<String, Box<dyn JsonBufferReader + Send>>,
86}
87
88enum DispatchReply {
91 Value(Value),
92 RawJson(Payload),
93}
94
95impl DispatchReply {
96 fn into_payload(self) -> Payload {
97 match self {
98 Self::Value(value) => to_payload(&value),
99 Self::RawJson(payload) => payload,
100 }
101 }
102}
103
104impl Session for AimxSession {
105 fn call<'a>(
106 &'a mut self,
107 method: &'a str,
108 params: Payload,
109 ) -> BoxFut<'a, Result<Payload, RpcError>> {
110 Box::pin(async move {
111 let params: Value = serde_json::from_slice(¶ms).unwrap_or(Value::Null);
112 self.dispatch_call(method, params)
113 .await
114 .map(DispatchReply::into_payload)
115 })
116 }
117
118 fn subscribe<'a>(
119 &'a mut self,
120 topic: &'a str,
121 ) -> BoxFut<'a, Result<BoxStream<'static, SubUpdate>, RpcError>> {
122 Box::pin(async move {
125 if is_wildcard(topic) {
126 return Ok(self.subscribe_wildcard(topic));
127 }
128 let stream = crate::remote::stream::stream_record_updates(&self.db, topic)
130 .map_err(map_db_err)?;
131 Ok(
132 Box::pin(stream.map(|(data, skipped)| SubUpdate::new(data).with_skipped(skipped)))
133 as BoxStream<'static, SubUpdate>,
134 )
135 })
136 }
137
138 fn snapshots(&mut self, topic: &str) -> Vec<(String, Payload)> {
139 if !is_wildcard(topic) {
143 return Vec::new();
144 }
145 self.matched_keys(topic)
146 .into_iter()
147 .filter_map(|key| {
148 let value = self.db.try_latest_as_json(&key)?;
149 Some((key, to_payload(&value)))
150 })
151 .collect()
152 }
153
154 fn write<'a>(
155 &'a mut self,
156 topic: &'a str,
157 payload: Payload,
158 ) -> BoxFut<'a, Result<(), RpcError>> {
159 Box::pin(async move {
160 self.ensure_writable(topic)?;
161 let v: Value = serde_json::from_slice(&payload).unwrap_or(Value::Null);
164 let value = v.get("value").cloned().unwrap_or(v);
165 self.db
168 .set_record_from_json(topic, value)
169 .map_err(map_db_err)
170 })
171 }
172}
173
174impl AimxSession {
175 fn matched_keys(&self, pattern: &str) -> Vec<String> {
179 self.db
180 .list_records()
181 .into_iter()
182 .map(|m: RecordMetadata| m.record_key)
183 .filter(|key| topic_matches(pattern, key))
184 .collect()
185 }
186
187 fn subscribe_wildcard(&self, pattern: &str) -> BoxStream<'static, SubUpdate> {
191 let mut streams: Vec<BoxStream<'static, SubUpdate>> = Vec::new();
192 for key in self.matched_keys(pattern) {
193 match crate::remote::stream::stream_record_updates(&self.db, &key) {
194 Ok(stream) => {
195 let tag: Arc<str> = Arc::from(key.as_str());
196 streams.push(Box::pin(stream.map(move |(data, skipped)| {
197 SubUpdate::tagged(tag.clone(), data).with_skipped(skipped)
198 })));
199 }
200 Err(_e) => {
201 log_warn!(
202 "wildcard subscribe '{}': skipping '{}': {:?}",
203 pattern,
204 key,
205 _e
206 );
207 }
208 }
209 }
210 Box::pin(futures_util::stream::select_all(streams))
213 }
214
215 async fn dispatch_call(
217 &mut self,
218 method: &str,
219 params: Value,
220 ) -> Result<DispatchReply, RpcError> {
221 if method == "record.get" {
224 let name = str_field(¶ms, "name").ok_or(RpcError::NotFound)?;
225 return self.record_get(&name).map(DispatchReply::RawJson);
226 }
227
228 let value = match method {
229 "hello" => self.hello(params),
232 "record.list" => Ok(json!(self.db.list_records())),
233 "record.set" => self.record_set(params),
234 "record.drain" => self.record_drain(params),
235 "record.query" => self
236 .record_query(params)?
237 .await
238 .map_err(|_| RpcError::Internal),
239 "graph.nodes" => Ok(json!(self.db.inner().dependency_graph().nodes)),
240 "graph.edges" => Ok(json!(self.db.inner().dependency_graph().edges)),
241 "graph.topo_order" => Ok(json!(self.db.inner().dependency_graph().topo_order())),
242 #[cfg(feature = "observability")]
243 "profiling.reset" => {
244 self.ensure_write_permission()?;
245 self.db.reset_stage_profiling();
246 Ok(json!({ "reset": true }))
247 }
248 #[cfg(feature = "observability")]
249 "buffer_metrics.reset" => {
250 self.ensure_write_permission()?;
251 self.db.reset_buffer_metrics();
252 Ok(json!({ "reset": true }))
253 }
254 _ => Err(RpcError::NotFound),
255 }?;
256
257 Ok(DispatchReply::Value(value))
258 }
259
260 fn record_set(&self, params: Value) -> Result<Value, RpcError> {
262 let name = str_field(¶ms, "name").ok_or(RpcError::Internal)?;
263 let value = params.get("value").cloned().ok_or(RpcError::Internal)?;
264 self.ensure_writable(&name)?;
265 self.db
266 .set_record_from_json(&name, value)
267 .map_err(map_db_err)?;
268 Ok(match self.db.try_latest_as_json(&name) {
270 Some(updated) => json!({ "status": "success", "value": updated }),
271 None => json!({ "status": "success" }),
272 })
273 }
274
275 fn record_get(&mut self, name: &str) -> Result<Payload, RpcError> {
288 if let Some(bytes) = self.db.try_latest_as_json_bytes(name) {
289 return Ok(Payload::from(bytes));
290 }
291 self.drain_values(name, usize::MAX)?
293 .pop()
294 .map(|value| to_payload(&value))
295 .ok_or(RpcError::NotFound)
296 }
297
298 fn record_drain(&mut self, params: Value) -> Result<Value, RpcError> {
301 let name = str_field(¶ms, "name").ok_or(RpcError::Internal)?;
302 let limit = params
303 .get("limit")
304 .and_then(|v| v.as_u64())
305 .map(|v| usize::try_from(v).unwrap_or(usize::MAX))
306 .unwrap_or(usize::MAX);
307 let values = self.drain_values(&name, limit)?;
308 let count = values.len();
309 Ok(json!({ "record_name": name, "values": values, "count": count }))
310 }
311
312 fn drain_values(&mut self, name: &str, limit: usize) -> Result<Vec<Value>, RpcError> {
317 if !self.drain_readers.contains_key(name) {
318 let id = self
319 .db
320 .inner()
321 .resolve_str(name)
322 .ok_or(RpcError::NotFound)?;
323 let record = self.db.inner().storage(id).ok_or(RpcError::NotFound)?;
324 let reader = record
327 .json_access()
328 .ok_or_else(|| {
329 map_db_err(DbError::runtime_error(alloc::format!(
330 "Record '{name}' does not support JSON remote access"
331 )))
332 })?
333 .subscribe_json()
334 .map_err(map_db_err)?;
335 self.drain_readers.insert(name.to_string(), reader);
336 }
337
338 let reader = self.drain_readers.get_mut(name).ok_or_else(|| {
341 map_db_err(DbError::runtime_error(alloc::format!(
342 "Record '{name}' drain reader vanished between insert and use"
343 )))
344 })?;
345 let mut values = Vec::new();
346 while values.len() < limit {
347 match reader.try_recv_json() {
348 Ok(val) => values.push(val),
349 Err(DbError::BufferEmpty) => break,
350 Err(DbError::BufferLagged { .. }) => continue,
352 Err(_) => break,
353 }
354 }
355 Ok(values)
356 }
357
358 #[allow(clippy::type_complexity)]
368 fn record_query(
369 &self,
370 params: Value,
371 ) -> Result<
372 core::pin::Pin<
373 Box<dyn core::future::Future<Output = Result<Value, String>> + Send + 'static>,
374 >,
375 RpcError,
376 > {
377 let handler = self
378 .db
379 .extensions()
380 .get::<crate::remote::QueryHandlerFn>()
381 .ok_or(RpcError::Internal)?;
382 let name = params
383 .get("name")
384 .and_then(|v| v.as_str())
385 .unwrap_or(crate::remote::QUERY_ALL_PATTERN)
386 .to_string();
387 let limit = params
388 .get("limit")
389 .and_then(|v| v.as_u64())
390 .and_then(|v| usize::try_from(v).ok());
391 let start = params.get("start").and_then(|v| v.as_u64());
392 let end = params.get("end").and_then(|v| v.as_u64());
393 Ok(handler(crate::remote::QueryHandlerParams {
394 name,
395 limit,
396 start,
397 end,
398 }))
399 }
400
401 fn hello(&self, params: Value) -> Result<Value, RpcError> {
410 let version = str_field(¶ms, "version").unwrap_or_default();
411 if !crate::remote::version_compatible(&version) {
412 log_warn!(
413 "refusing handshake: client protocol version {:?} incompatible with server {}",
414 version,
415 PROTOCOL_VERSION
416 );
417 return Err(RpcError::VersionMismatch);
418 }
419 Ok(self.welcome())
420 }
421
422 fn welcome(&self) -> Value {
427 let (permissions, writable_records) = match &self.config.security_policy {
428 SecurityPolicy::ReadOnly => (vec!["read".to_string()], Vec::new()),
429 SecurityPolicy::ReadWrite { writable_records } => {
430 let existing: hashbrown::HashSet<String> = self
431 .db
432 .list_records()
433 .into_iter()
434 .map(|m: RecordMetadata| m.record_key)
435 .collect();
436 let writable = writable_records
437 .iter()
438 .filter(|name| existing.contains(name.as_str()))
439 .cloned()
440 .collect();
441 (vec!["read".to_string(), "write".to_string()], writable)
442 }
443 };
444 let welcome = WelcomeMessage {
445 version: PROTOCOL_VERSION.to_string(),
446 server: "aimdb".to_string(),
447 permissions,
448 writable_records,
449 max_subscriptions: Some(self.config.max_subs_per_connection),
450 authenticated: Some(false),
451 };
452 json!(welcome)
453 }
454
455 fn ensure_writable(&self, name: &str) -> Result<(), RpcError> {
457 match &self.config.security_policy {
458 SecurityPolicy::ReadWrite { writable_records } if writable_records.contains(name) => {
459 Ok(())
460 }
461 _ => Err(RpcError::Denied),
462 }
463 }
464
465 #[cfg(feature = "observability")]
467 fn ensure_write_permission(&self) -> Result<(), RpcError> {
468 match self.config.security_policy {
469 SecurityPolicy::ReadOnly => Err(RpcError::Denied),
470 SecurityPolicy::ReadWrite { .. } => Ok(()),
471 }
472 }
473}
474
475fn to_payload(v: &Value) -> Payload {
478 Payload::from(serde_json::to_vec(v).unwrap_or_default().as_slice())
479}
480
481fn str_field(params: &Value, key: &str) -> Option<String> {
483 params.get(key).and_then(|v| v.as_str()).map(String::from)
484}
485
486fn map_db_err(e: DbError) -> RpcError {
488 match e {
489 DbError::RecordKeyNotFound { .. } | DbError::InvalidRecordId { .. } => RpcError::NotFound,
490 DbError::PermissionDenied { .. } => RpcError::Denied,
491 _ => RpcError::Internal,
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::remote::{QueryHandlerFn, QUERY_ALL_PATTERN};
499 use std::sync::Mutex;
500
501 #[tokio::test]
512 async fn an_omitted_query_name_spans_every_record() {
513 static SEEN: Mutex<Option<String>> = Mutex::new(None);
514
515 let handler: QueryHandlerFn = Box::new(|params| {
516 *SEEN.lock().expect("uncontended") = Some(params.name.clone());
517 Box::pin(async { Ok(json!({ "records": [], "total": 0 })) })
518 });
519
520 let mut builder = crate::AimDbBuilder::new()
521 .runtime(Arc::new(crate::executor::test_support::NoopRuntimeOps));
522 builder.extensions_mut().insert(handler);
523 let (db, _runner) = builder.build().await.expect("build db");
524
525 let dispatch = AimxDispatch::new(Arc::new(db), AimxConfig::uds_default());
526 let mut session = dispatch.open(&SessionCtx::default());
527 session
528 .call("record.query", to_payload(&json!({})))
529 .await
530 .expect("the query resolves");
531
532 assert_eq!(
533 SEEN.lock().expect("uncontended").as_deref(),
534 Some(QUERY_ALL_PATTERN),
535 "an omitted name must span every record, at every depth"
536 );
537 }
538}