use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use hashbrown::HashMap;
use serde_json::{json, Value};
use futures_util::StreamExt;
use crate::buffer::JsonBufferReader;
use crate::remote::{AimxConfig, RecordMetadata, SecurityPolicy, WelcomeMessage, PROTOCOL_VERSION};
use crate::session::{
is_wildcard, topic_matches, AuthError, BoxFut, BoxStream, Dispatch, Payload, PeerInfo,
RpcError, Session, SessionCtx, SubUpdate,
};
use crate::{AimDb, DbError};
pub struct AimxDispatch {
db: Arc<AimDb>,
config: Arc<AimxConfig>,
}
impl AimxDispatch {
pub fn new(db: Arc<AimDb>, config: AimxConfig) -> Self {
Self {
db,
config: Arc::new(config),
}
}
}
impl Dispatch for AimxDispatch {
fn authenticate<'a>(
&'a self,
_peer: &'a PeerInfo,
_first: Option<&'a [u8]>,
) -> BoxFut<'a, Result<SessionCtx, AuthError>> {
Box::pin(async { Ok(SessionCtx::default()) })
}
fn open(&self, _ctx: &SessionCtx) -> Box<dyn Session> {
Box::new(AimxSession {
db: self.db.clone(),
config: self.config.clone(),
drain_readers: HashMap::new(),
})
}
}
struct AimxSession {
db: Arc<AimDb>,
config: Arc<AimxConfig>,
drain_readers: HashMap<String, Box<dyn JsonBufferReader + Send>>,
}
enum DispatchReply {
Value(Value),
RawJson(Payload),
}
impl DispatchReply {
fn into_payload(self) -> Payload {
match self {
Self::Value(value) => to_payload(&value),
Self::RawJson(payload) => payload,
}
}
}
impl Session for AimxSession {
fn call<'a>(
&'a mut self,
method: &'a str,
params: Payload,
) -> BoxFut<'a, Result<Payload, RpcError>> {
Box::pin(async move {
let params: Value = serde_json::from_slice(¶ms).unwrap_or(Value::Null);
self.dispatch_call(method, params)
.await
.map(DispatchReply::into_payload)
})
}
fn subscribe<'a>(
&'a mut self,
topic: &'a str,
) -> BoxFut<'a, Result<BoxStream<'static, SubUpdate>, RpcError>> {
Box::pin(async move {
if is_wildcard(topic) {
return Ok(self.subscribe_wildcard(topic));
}
let stream = crate::remote::stream::stream_record_updates(&self.db, topic)
.map_err(map_db_err)?;
Ok(
Box::pin(stream.map(|(data, skipped)| SubUpdate::new(data).with_skipped(skipped)))
as BoxStream<'static, SubUpdate>,
)
})
}
fn snapshots(&mut self, topic: &str) -> Vec<(String, Payload)> {
if !is_wildcard(topic) {
return Vec::new();
}
self.matched_keys(topic)
.into_iter()
.filter_map(|key| {
let value = self.db.try_latest_as_json(&key)?;
Some((key, to_payload(&value)))
})
.collect()
}
fn write<'a>(
&'a mut self,
topic: &'a str,
payload: Payload,
) -> BoxFut<'a, Result<(), RpcError>> {
Box::pin(async move {
self.ensure_writable(topic)?;
let v: Value = serde_json::from_slice(&payload).unwrap_or(Value::Null);
let value = v.get("value").cloned().unwrap_or(v);
self.db
.set_record_from_json(topic, value)
.map_err(map_db_err)
})
}
}
impl AimxSession {
fn matched_keys(&self, pattern: &str) -> Vec<String> {
self.db
.list_records()
.into_iter()
.map(|m: RecordMetadata| m.record_key)
.filter(|key| topic_matches(pattern, key))
.collect()
}
fn subscribe_wildcard(&self, pattern: &str) -> BoxStream<'static, SubUpdate> {
let mut streams: Vec<BoxStream<'static, SubUpdate>> = Vec::new();
for key in self.matched_keys(pattern) {
match crate::remote::stream::stream_record_updates(&self.db, &key) {
Ok(stream) => {
let tag: Arc<str> = Arc::from(key.as_str());
streams.push(Box::pin(stream.map(move |(data, skipped)| {
SubUpdate::tagged(tag.clone(), data).with_skipped(skipped)
})));
}
Err(_e) => {
log_warn!(
"wildcard subscribe '{}': skipping '{}': {:?}",
pattern,
key,
_e
);
}
}
}
Box::pin(futures_util::stream::select_all(streams))
}
async fn dispatch_call(
&mut self,
method: &str,
params: Value,
) -> Result<DispatchReply, RpcError> {
if method == "record.get" {
let name = str_field(¶ms, "name").ok_or(RpcError::NotFound)?;
return self.record_get(&name).map(DispatchReply::RawJson);
}
let value = match method {
"hello" => self.hello(params),
"record.list" => Ok(json!(self.db.list_records())),
"record.set" => self.record_set(params),
"record.drain" => self.record_drain(params),
"record.query" => self
.record_query(params)?
.await
.map_err(|_| RpcError::Internal),
"graph.nodes" => Ok(json!(self.db.inner().dependency_graph().nodes)),
"graph.edges" => Ok(json!(self.db.inner().dependency_graph().edges)),
"graph.topo_order" => Ok(json!(self.db.inner().dependency_graph().topo_order())),
#[cfg(feature = "observability")]
"profiling.reset" => {
self.ensure_write_permission()?;
self.db.reset_stage_profiling();
Ok(json!({ "reset": true }))
}
#[cfg(feature = "observability")]
"buffer_metrics.reset" => {
self.ensure_write_permission()?;
self.db.reset_buffer_metrics();
Ok(json!({ "reset": true }))
}
_ => Err(RpcError::NotFound),
}?;
Ok(DispatchReply::Value(value))
}
fn record_set(&self, params: Value) -> Result<Value, RpcError> {
let name = str_field(¶ms, "name").ok_or(RpcError::Internal)?;
let value = params.get("value").cloned().ok_or(RpcError::Internal)?;
self.ensure_writable(&name)?;
self.db
.set_record_from_json(&name, value)
.map_err(map_db_err)?;
Ok(match self.db.try_latest_as_json(&name) {
Some(updated) => json!({ "status": "success", "value": updated }),
None => json!({ "status": "success" }),
})
}
fn record_get(&mut self, name: &str) -> Result<Payload, RpcError> {
if let Some(bytes) = self.db.try_latest_as_json_bytes(name) {
return Ok(Payload::from(bytes));
}
self.drain_values(name, usize::MAX)?
.pop()
.map(|value| to_payload(&value))
.ok_or(RpcError::NotFound)
}
fn record_drain(&mut self, params: Value) -> Result<Value, RpcError> {
let name = str_field(¶ms, "name").ok_or(RpcError::Internal)?;
let limit = params
.get("limit")
.and_then(|v| v.as_u64())
.map(|v| usize::try_from(v).unwrap_or(usize::MAX))
.unwrap_or(usize::MAX);
let values = self.drain_values(&name, limit)?;
let count = values.len();
Ok(json!({ "record_name": name, "values": values, "count": count }))
}
fn drain_values(&mut self, name: &str, limit: usize) -> Result<Vec<Value>, RpcError> {
if !self.drain_readers.contains_key(name) {
let id = self
.db
.inner()
.resolve_str(name)
.ok_or(RpcError::NotFound)?;
let record = self.db.inner().storage(id).ok_or(RpcError::NotFound)?;
let reader = record
.json_access()
.ok_or_else(|| {
map_db_err(DbError::runtime_error(alloc::format!(
"Record '{name}' does not support JSON remote access"
)))
})?
.subscribe_json()
.map_err(map_db_err)?;
self.drain_readers.insert(name.to_string(), reader);
}
let reader = self.drain_readers.get_mut(name).ok_or_else(|| {
map_db_err(DbError::runtime_error(alloc::format!(
"Record '{name}' drain reader vanished between insert and use"
)))
})?;
let mut values = Vec::new();
while values.len() < limit {
match reader.try_recv_json() {
Ok(val) => values.push(val),
Err(DbError::BufferEmpty) => break,
Err(DbError::BufferLagged { .. }) => continue,
Err(_) => break,
}
}
Ok(values)
}
#[allow(clippy::type_complexity)]
fn record_query(
&self,
params: Value,
) -> Result<
core::pin::Pin<
Box<dyn core::future::Future<Output = Result<Value, String>> + Send + 'static>,
>,
RpcError,
> {
let handler = self
.db
.extensions()
.get::<crate::remote::QueryHandlerFn>()
.ok_or(RpcError::Internal)?;
let name = params
.get("name")
.and_then(|v| v.as_str())
.unwrap_or(crate::remote::QUERY_ALL_PATTERN)
.to_string();
let limit = params
.get("limit")
.and_then(|v| v.as_u64())
.and_then(|v| usize::try_from(v).ok());
let start = params.get("start").and_then(|v| v.as_u64());
let end = params.get("end").and_then(|v| v.as_u64());
Ok(handler(crate::remote::QueryHandlerParams {
name,
limit,
start,
end,
}))
}
fn hello(&self, params: Value) -> Result<Value, RpcError> {
let version = str_field(¶ms, "version").unwrap_or_default();
if !crate::remote::version_compatible(&version) {
log_warn!(
"refusing handshake: client protocol version {:?} incompatible with server {}",
version,
PROTOCOL_VERSION
);
return Err(RpcError::VersionMismatch);
}
Ok(self.welcome())
}
fn welcome(&self) -> Value {
let (permissions, writable_records) = match &self.config.security_policy {
SecurityPolicy::ReadOnly => (vec!["read".to_string()], Vec::new()),
SecurityPolicy::ReadWrite { writable_records } => {
let existing: hashbrown::HashSet<String> = self
.db
.list_records()
.into_iter()
.map(|m: RecordMetadata| m.record_key)
.collect();
let writable = writable_records
.iter()
.filter(|name| existing.contains(name.as_str()))
.cloned()
.collect();
(vec!["read".to_string(), "write".to_string()], writable)
}
};
let welcome = WelcomeMessage {
version: PROTOCOL_VERSION.to_string(),
server: "aimdb".to_string(),
permissions,
writable_records,
max_subscriptions: Some(self.config.max_subs_per_connection),
authenticated: Some(false),
};
json!(welcome)
}
fn ensure_writable(&self, name: &str) -> Result<(), RpcError> {
match &self.config.security_policy {
SecurityPolicy::ReadWrite { writable_records } if writable_records.contains(name) => {
Ok(())
}
_ => Err(RpcError::Denied),
}
}
#[cfg(feature = "observability")]
fn ensure_write_permission(&self) -> Result<(), RpcError> {
match self.config.security_policy {
SecurityPolicy::ReadOnly => Err(RpcError::Denied),
SecurityPolicy::ReadWrite { .. } => Ok(()),
}
}
}
fn to_payload(v: &Value) -> Payload {
Payload::from(serde_json::to_vec(v).unwrap_or_default().as_slice())
}
fn str_field(params: &Value, key: &str) -> Option<String> {
params.get(key).and_then(|v| v.as_str()).map(String::from)
}
fn map_db_err(e: DbError) -> RpcError {
match e {
DbError::RecordKeyNotFound { .. } | DbError::InvalidRecordId { .. } => RpcError::NotFound,
DbError::PermissionDenied { .. } => RpcError::Denied,
_ => RpcError::Internal,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::remote::{QueryHandlerFn, QUERY_ALL_PATTERN};
use std::sync::Mutex;
#[tokio::test]
async fn an_omitted_query_name_spans_every_record() {
static SEEN: Mutex<Option<String>> = Mutex::new(None);
let handler: QueryHandlerFn = Box::new(|params| {
*SEEN.lock().expect("uncontended") = Some(params.name.clone());
Box::pin(async { Ok(json!({ "records": [], "total": 0 })) })
});
let mut builder = crate::AimDbBuilder::new()
.runtime(Arc::new(crate::executor::test_support::NoopRuntimeOps));
builder.extensions_mut().insert(handler);
let (db, _runner) = builder.build().await.expect("build db");
let dispatch = AimxDispatch::new(Arc::new(db), AimxConfig::uds_default());
let mut session = dispatch.open(&SessionCtx::default());
session
.call("record.query", to_payload(&json!({})))
.await
.expect("the query resolves");
assert_eq!(
SEEN.lock().expect("uncontended").as_deref(),
Some(QUERY_ALL_PATTERN),
"an omitted name must span every record, at every depth"
);
}
}