vendor: update cargo-cxai-bridge-0.1.0

This commit is contained in:
cx-git-agent
2026-04-26 16:48:10 +00:00
committed by GitHub
parent 653161fdd3
commit 1ec7d814eb
9 changed files with 2507 additions and 3 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "d878deb8441897ecdd416011b49d2d2f6112e867"
},
"path_in_vcs": "crates/cxai-bridge"
}
Generated
+2071
View File
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "cxai-bridge"
version = "0.1.0"
authors = ["CxAI-LLM <agent@cxai-studio.com>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "FFI bridge for C++ interop, JSON-RPC proxy, and SignalR WebSocket client"
homepage = "https://cxllm.io"
readme = false
license = "MIT"
repository = "https://git.cxllm-studio.com/CxAI-LLM/CxAI.Rust"
resolver = "2"
[lib]
name = "cxai_bridge"
path = "src/lib.rs"
[dependencies.cxai-sdk]
version = "0.1.0"
registry-index = "sparse+https://git.cxllm-studio.com/api/packages/CxAI-LLM/cargo/"
[dependencies.futures-util]
version = "0.3"
[dependencies.reqwest]
version = "0.12"
features = [
"json",
"rustls-tls",
"stream",
]
default-features = false
[dependencies.serde]
version = "1"
features = ["derive"]
[dependencies.serde_json]
version = "1"
[dependencies.thiserror]
version = "2"
[dependencies.tokio]
version = "1"
features = ["full"]
[dependencies.tokio-tungstenite]
version = "0.26"
features = ["rustls-tls-webpki-roots"]
[dependencies.tracing]
version = "0.1"
[dependencies.uuid]
version = "1"
features = [
"v4",
"serde",
]
[dev-dependencies.tokio]
version = "1"
features = [
"full",
"test-util",
"macros",
]
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "cxai-bridge"
description = "FFI bridge for C++ interop, JSON-RPC proxy, and SignalR WebSocket client"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
authors.workspace = true
[dependencies]
cxai-sdk = { workspace = true }
tokio = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tokio-tungstenite = { workspace = true }
futures-util = { workspace = true }
uuid = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
-3
View File
@@ -1,3 +0,0 @@
# cargo-cxai-bridge-0.1.0
Cargo crate: cxai-bridge-0.1.0
+7
View File
@@ -0,0 +1,7 @@
pub mod pubsub;
pub mod rpc;
pub mod signalr;
pub use pubsub::PubSubClient;
pub use rpc::RpcClient;
pub use signalr::SignalRClient;
+81
View File
@@ -0,0 +1,81 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{RwLock, broadcast};
use tracing::debug;
type MessageHandler = broadcast::Sender<serde_json::Value>;
/// In-process pub/sub hub matching the .NET PubSubService.
pub struct PubSubClient {
topics: Arc<RwLock<HashMap<String, MessageHandler>>>,
channel_capacity: usize,
}
impl PubSubClient {
pub fn new(channel_capacity: usize) -> Self {
Self {
topics: Arc::new(RwLock::new(HashMap::new())),
channel_capacity,
}
}
/// Subscribe to a topic, returning a receiver for messages.
pub async fn subscribe(&self, topic: &str) -> broadcast::Receiver<serde_json::Value> {
let mut topics = self.topics.write().await;
let sender = topics.entry(topic.to_string()).or_insert_with(|| {
debug!(topic, "Creating new topic channel");
let (tx, _) = broadcast::channel(self.channel_capacity);
tx
});
sender.subscribe()
}
/// Publish a message to a topic.
pub async fn publish(&self, topic: &str, message: serde_json::Value) -> usize {
let topics = self.topics.read().await;
if let Some(sender) = topics.get(topic) {
match sender.send(message) {
Ok(count) => {
debug!(topic, count, "Published message");
count
}
Err(_) => 0,
}
} else {
0
}
}
/// List all active topics.
pub async fn topics(&self) -> Vec<String> {
self.topics.read().await.keys().cloned().collect()
}
}
impl Default for PubSubClient {
fn default() -> Self {
Self::new(256)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn pubsub_roundtrip() {
let hub = PubSubClient::default();
let mut rx = hub.subscribe("test.topic").await;
let msg = serde_json::json!({"hello": "world"});
hub.publish("test.topic", msg.clone()).await;
let received = rx.recv().await.unwrap();
assert_eq!(received, msg);
}
#[tokio::test]
async fn pubsub_no_subscribers() {
let hub = PubSubClient::default();
let count = hub.publish("empty.topic", serde_json::json!({})).await;
assert_eq!(count, 0);
}
}
+106
View File
@@ -0,0 +1,106 @@
use serde::{Deserialize, Serialize};
use tracing::{debug, error};
use uuid::Uuid;
/// JSON-RPC 2.0 client for the C++ bridge backend.
pub struct RpcClient {
http: reqwest::Client,
endpoint: String,
}
#[derive(Debug, Serialize)]
struct JsonRpcRequest {
jsonrpc: &'static str,
method: String,
params: Option<serde_json::Value>,
id: String,
}
#[derive(Debug, Deserialize)]
struct JsonRpcResponse {
result: Option<serde_json::Value>,
error: Option<JsonRpcError>,
#[allow(dead_code)]
id: String,
}
#[derive(Debug, Deserialize)]
struct JsonRpcError {
code: i64,
message: String,
}
impl RpcClient {
/// Create a new JSON-RPC client pointing to the C++ bridge backend.
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
http: reqwest::Client::new(),
endpoint: endpoint.into(),
}
}
/// Call an RPC method with optional parameters.
pub async fn call(
&self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value, cxai_sdk::CxError> {
let request_id = Uuid::new_v4().to_string();
let rpc_req = JsonRpcRequest {
jsonrpc: "2.0",
method: method.to_string(),
params,
id: request_id.clone(),
};
debug!(method, id = %request_id, "RPC call");
let resp = self
.http
.post(&self.endpoint)
.json(&rpc_req)
.send()
.await
.map_err(cxai_sdk::CxError::Http)?;
if !resp.status().is_success() {
return Err(cxai_sdk::CxError::from_response(resp).await);
}
let rpc_resp: JsonRpcResponse = resp.json().await.map_err(cxai_sdk::CxError::Http)?;
if let Some(err) = rpc_resp.error {
error!(code = err.code, message = %err.message, "RPC error");
return Err(cxai_sdk::CxError::Api {
status: err.code as u16,
message: err.message,
});
}
rpc_resp.result.ok_or_else(|| cxai_sdk::CxError::Api {
status: 500,
message: "RPC response missing result".into(),
})
}
/// Discover available modules and methods on the C++ server.
pub async fn discover(&self) -> Result<serde_json::Value, cxai_sdk::CxError> {
self.call("system.discover", None).await
}
/// Health check the C++ backend.
pub async fn health(&self) -> Result<serde_json::Value, cxai_sdk::CxError> {
self.call("system.health", None).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rpc_client_constructs() {
let client = RpcClient::new("http://localhost:9090/rpc");
assert_eq!(client.endpoint, "http://localhost:9090/rpc");
}
}
+128
View File
@@ -0,0 +1,128 @@
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::Message;
use tracing::{debug, error, info, warn};
/// SignalR WebSocket client for real-time bridge events.
pub struct SignalRClient {
url: String,
}
#[derive(Debug, Serialize)]
struct SignalRMessage {
#[serde(rename = "type")]
msg_type: u8,
target: String,
arguments: Vec<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct SignalRIncoming {
#[serde(rename = "type")]
msg_type: u8,
#[allow(dead_code)]
target: Option<String>,
arguments: Option<Vec<serde_json::Value>>,
}
impl SignalRClient {
/// Create a new SignalR client for the bridge hub.
pub fn new(base_url: impl Into<String>) -> Self {
let base = base_url.into();
let ws_url = base
.replace("http://", "ws://")
.replace("https://", "wss://");
Self {
url: format!("{ws_url}/hubs/bridge"),
}
}
/// Connect and subscribe to a topic, returning a channel of messages.
pub async fn subscribe(
&self,
topic: &str,
) -> Result<mpsc::Receiver<serde_json::Value>, cxai_sdk::CxError> {
let (tx, rx) = mpsc::channel(256);
let url = self.url.clone();
let topic = topic.to_string();
tokio::spawn(async move {
match tokio_tungstenite::connect_async(&url).await {
Ok((ws_stream, _)) => {
info!(url = %url, "SignalR connected");
let (mut write, mut read) = ws_stream.split();
// Send SignalR handshake
let handshake = "{\"protocol\":\"json\",\"version\":1}\x1e".to_string();
if let Err(e) = write.send(Message::Text(handshake.into())).await {
error!("Handshake failed: {e}");
return;
}
// Subscribe to topic
let sub_msg = SignalRMessage {
msg_type: 1,
target: "Subscribe".into(),
arguments: vec![serde_json::json!(topic)],
};
let payload = format!("{}\x1e", serde_json::to_string(&sub_msg).unwrap());
if let Err(e) = write.send(Message::Text(payload.into())).await {
error!("Subscribe failed: {e}");
return;
}
debug!(topic = %topic, "Subscribed");
// Read messages
while let Some(msg) = read.next().await {
match msg {
Ok(Message::Text(text)) => {
for part in text.split('\x1e').filter(|s| !s.is_empty()) {
if let Ok(incoming) =
serde_json::from_str::<SignalRIncoming>(part)
&& incoming.msg_type == 1
&& let Some(args) = incoming.arguments
{
for arg in args {
if tx.send(arg).await.is_err() {
return;
}
}
}
}
}
Ok(Message::Close(_)) => {
info!("SignalR connection closed");
break;
}
Err(e) => {
warn!("WebSocket error: {e}");
break;
}
_ => {}
}
}
}
Err(e) => {
error!("Failed to connect: {e}");
}
}
});
Ok(rx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signalr_url_construction() {
let client = SignalRClient::new("http://localhost:9100");
assert_eq!(client.url, "ws://localhost:9100/hubs/bridge");
let client = SignalRClient::new("https://api.cxllm.io");
assert_eq!(client.url, "wss://api.cxllm.io/hubs/bridge");
}
}