use std::sync::Arc;
use std::time::Duration;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio::task::JoinHandle;
use tokio::time::timeout;
use aimdb_core::session::aimx::AimxCodec;
use aimdb_core::session::{
run_client, BoxStream, ClientConfig, ClientHandle, Dialer, Payload, RpcError,
};
use aimdb_tokio_adapter::TokioAdapter;
use crate::error::{ClientError, ClientResult};
use crate::protocol::{version_compatible, RecordMetadata, WelcomeMessage, PROTOCOL_VERSION};
pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DrainResponse {
pub record_name: String,
pub values: Vec<serde_json::Value>,
pub count: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecordUpdate {
pub topic: Option<String>,
pub value: Option<serde_json::Value>,
pub skipped: u64,
pub snapshot_end: bool,
}
impl RecordUpdate {
pub fn has_gap(&self) -> bool {
self.skipped > 0
}
}
pub struct AimxConnection {
handle: ClientHandle,
engine: JoinHandle<()>,
server_info: WelcomeMessage,
}
impl AimxConnection {
pub async fn connect(endpoint: &str) -> ClientResult<Self> {
Self::connect_with_timeout(endpoint, DEFAULT_CONNECT_TIMEOUT).await
}
pub async fn connect_with_timeout(
endpoint: &str,
connect_timeout: Duration,
) -> ClientResult<Self> {
let dialer = crate::endpoint::dial(endpoint)?;
Self::spin_up(dialer, endpoint, connect_timeout).await
}
pub async fn connect_over(dialer: impl Dialer + 'static) -> ClientResult<Self> {
Self::connect_over_with_timeout(dialer, DEFAULT_CONNECT_TIMEOUT).await
}
pub async fn connect_over_with_timeout(
dialer: impl Dialer + 'static,
connect_timeout: Duration,
) -> ClientResult<Self> {
Self::spin_up(dialer, "remote peer", connect_timeout).await
}
async fn spin_up(
dialer: impl Dialer + 'static,
label: &str,
connect_timeout: Duration,
) -> ClientResult<Self> {
let config = ClientConfig {
reconnect: false,
sends_hello: false,
..ClientConfig::default()
};
let (handle, engine_fut) = run_client(dialer, AimxCodec, config, Arc::new(TokioAdapter));
let engine = tokio::spawn(engine_fut);
let server_info = async {
let hello = json!({ "client": "aimdb-client", "version": PROTOCOL_VERSION });
let reply = timeout(connect_timeout, handle.call("hello", to_payload(&hello)?))
.await
.map_err(|_| ClientError::connection_failed(label, "handshake timed out"))?
.map_err(|e| match e {
RpcError::VersionMismatch => ClientError::connection_failed(
label,
format!(
"protocol version mismatch: server rejected client version {PROTOCOL_VERSION}"
),
),
_ => ClientError::connection_failed(
label,
"handshake failed (engine could not reach server)",
),
})?;
let welcome = from_payload::<WelcomeMessage>(&reply)?;
if !version_compatible(&welcome.version) {
return Err(ClientError::connection_failed(
label,
format!(
"protocol version mismatch: server speaks {}, client speaks {PROTOCOL_VERSION}",
welcome.version
),
));
}
Ok(welcome)
}
.await;
match server_info {
Ok(server_info) => Ok(Self {
handle,
engine,
server_info,
}),
Err(e) => {
engine.abort();
Err(e)
}
}
}
pub fn handle(&self) -> &ClientHandle {
&self.handle
}
pub fn server_info(&self) -> &WelcomeMessage {
&self.server_info
}
pub async fn list_records(&self) -> ClientResult<Vec<RecordMetadata>> {
let reply = self.call("record.list", null_payload()).await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn get_record(&self, name: &str) -> ClientResult<serde_json::Value> {
let reply = self
.call("record.get", to_payload(&json!({ "name": name }))?)
.await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn set_record(
&self,
name: &str,
value: serde_json::Value,
) -> ClientResult<serde_json::Value> {
let reply = self
.call(
"record.set",
to_payload(&json!({ "name": name, "value": value }))?,
)
.await?;
Ok(serde_json::from_slice(&reply)?)
}
pub fn subscribe(
&self,
pattern: &str,
) -> ClientResult<BoxStream<'static, Result<RecordUpdate, ClientError>>> {
let raw = self.handle.subscribe(pattern).map_err(rpc_err)?;
Ok(decode_updates(raw))
}
pub fn write_record(&self, name: &str, value: serde_json::Value) -> ClientResult<()> {
self.handle
.write(name, to_payload(&json!({ "value": value }))?)
.map_err(rpc_err)
}
pub async fn drain_record(&self, name: &str) -> ClientResult<DrainResponse> {
let reply = self
.call("record.drain", to_payload(&json!({ "name": name }))?)
.await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn drain_record_with_limit(
&self,
name: &str,
limit: u32,
) -> ClientResult<DrainResponse> {
let reply = self
.call(
"record.drain",
to_payload(&json!({ "name": name, "limit": limit }))?,
)
.await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn query(&self, params: serde_json::Value) -> ClientResult<serde_json::Value> {
let reply = self.call("record.query", to_payload(¶ms)?).await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn graph_nodes(&self) -> ClientResult<Vec<serde_json::Value>> {
let reply = self.call("graph.nodes", null_payload()).await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn graph_edges(&self) -> ClientResult<Vec<serde_json::Value>> {
let reply = self.call("graph.edges", null_payload()).await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn graph_topo_order(&self) -> ClientResult<Vec<String>> {
let reply = self.call("graph.topo_order", null_payload()).await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn reset_stage_profiling(&self) -> ClientResult<serde_json::Value> {
let reply = self.call("profiling.reset", null_payload()).await?;
Ok(serde_json::from_slice(&reply)?)
}
pub async fn reset_buffer_metrics(&self) -> ClientResult<serde_json::Value> {
let reply = self.call("buffer_metrics.reset", null_payload()).await?;
Ok(serde_json::from_slice(&reply)?)
}
async fn call(&self, method: &str, params: Payload) -> ClientResult<Payload> {
self.handle.call(method, params).await.map_err(rpc_err)
}
}
impl Drop for AimxConnection {
fn drop(&mut self) {
self.engine.abort();
}
}
fn decode_updates(
raw: BoxStream<'static, Result<aimdb_core::session::SubUpdate, RpcError>>,
) -> BoxStream<'static, Result<RecordUpdate, ClientError>> {
let decoded = raw.scan(false, |terminated, item| {
if *terminated {
return futures::future::ready(None);
}
let out = match item {
Ok(u) => Ok(RecordUpdate {
topic: u.topic.as_deref().map(String::from),
value: serde_json::from_slice(&u.data).ok(),
skipped: u.skipped,
snapshot_end: u.snapshot_end,
}),
Err(e) => {
*terminated = true;
Err(rpc_err(e))
}
};
futures::future::ready(Some(out))
});
Box::pin(decoded)
}
fn to_payload<T: Serialize>(value: &T) -> ClientResult<Payload> {
Ok(Payload::from(serde_json::to_vec(value)?.as_slice()))
}
fn null_payload() -> Payload {
Payload::from(&b"null"[..])
}
fn from_payload<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> ClientResult<T> {
Ok(serde_json::from_slice(bytes)?)
}
fn rpc_err(e: RpcError) -> ClientError {
match e {
RpcError::NotFound => {
ClientError::server_error("not_found", "method or record not found", None)
}
RpcError::Denied => ClientError::server_error("denied", "permission denied", None),
_ => ClientError::server_error("internal", "engine/transport failure", None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use aimdb_core::session::SubUpdate;
use serde_json::json;
fn raw_stream(
items: Vec<Result<SubUpdate, RpcError>>,
) -> BoxStream<'static, Result<SubUpdate, RpcError>> {
Box::pin(futures::stream::iter(items))
}
fn ok_update(payload: &str, skipped: u64) -> Result<SubUpdate, RpcError> {
Ok(SubUpdate::new(Payload::from(payload.as_bytes())).with_skipped(skipped))
}
async fn decoded(items: Vec<Result<SubUpdate, RpcError>>) -> Vec<RecordUpdate> {
decode_updates(raw_stream(items))
.map(|r| r.expect("no terminal error in this stream"))
.collect()
.await
}
#[tokio::test]
async fn gap_counts_reach_the_caller() {
let out = decoded(vec![ok_update(r#"{"n":1}"#, 0), ok_update(r#"{"n":2}"#, 7)]).await;
assert_eq!(out.len(), 2);
assert_eq!(out[0].value, Some(json!({ "n": 1 })));
assert_eq!(out[0].skipped, 0);
assert!(!out[0].has_gap());
assert_eq!(out[1].value, Some(json!({ "n": 2 })));
assert_eq!(out[1].skipped, 7, "the engine's gap count is preserved");
assert!(out[1].has_gap());
}
#[tokio::test]
async fn snapshot_end_and_its_loss_reach_the_caller() {
let out = decoded(vec![
ok_update(r#"{"n":1}"#, 0),
Ok(SubUpdate::new(Payload::from(&b"{\"n\":2}"[..]))
.with_skipped(41)
.with_snapshot_end()),
ok_update(r#"{"n":3}"#, 0), ])
.await;
assert_eq!(out.len(), 3);
assert!(!out[0].snapshot_end);
assert!(out[1].snapshot_end, "the burst's final update is flagged");
assert_eq!(out[1].skipped, 41, "carrying the burst's whole loss");
assert!(!out[2].snapshot_end, "a live event does not close a burst");
}
#[tokio::test]
async fn an_undecodable_burst_final_frame_still_closes_the_burst() {
let out = decoded(vec![
ok_update(r#"{"n":1}"#, 0),
Ok(SubUpdate::new(Payload::from(&b"not json"[..]))
.with_skipped(3)
.with_snapshot_end()),
])
.await;
assert_eq!(out.len(), 2, "an undecodable frame is still delivered");
assert_eq!(out[1].value, None, "its payload did not decode");
assert!(
out[1].snapshot_end,
"burst completion must not depend on a later update that may never arrive"
);
assert_eq!(out[1].skipped, 3, "and its gap count stays exact");
}
#[tokio::test]
async fn topics_ride_along_and_rejection_ends_the_stream() {
let raw = raw_stream(vec![
Ok(
SubUpdate::tagged(Arc::from("temp.vienna"), Payload::from(&b"1"[..]))
.with_skipped(2),
),
Err(RpcError::Denied),
ok_update("99", 0), ]);
let out: Vec<_> = decode_updates(raw).collect().await;
assert_eq!(out.len(), 2, "the rejection is the stream's last item");
let first = out[0].as_ref().expect("delivered update");
assert_eq!(first.topic.as_deref(), Some("temp.vienna"));
assert_eq!(first.value, Some(json!(1)));
assert_eq!(first.skipped, 2);
let err = out[1]
.as_ref()
.expect_err("the rejection reaches the caller");
assert!(
matches!(err, ClientError::ServerError { code, .. } if code == "denied"),
"unexpected terminal error: {err}"
);
}
}