agentlink-wasm 0.1.1

AgentLink SDK WASM - WebAssembly bindings for browser/Node.js
//! WASM Bindings
//!
//! Exports Rust functions to JavaScript/TypeScript using wasm-bindgen.

use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use js_sys::Promise;

use agentlink_core::{
    http::{HttpClient, HttpClientExt},
    mqtt::{MqttClient, MqttConfig, MqttQoS, MqttMessage},
    protocols::auth::{LoginRequest, LoginResponse},
};

use crate::http::WasmHttpClient;
use crate::mqtt::WasmMqttClient;

/// Initialize logging for WASM
#[wasm_bindgen(start)]
pub fn start() {
    console_error_panic_hook::set_once();
    wasm_logger::init(wasm_logger::Config::default());
}

/// HTTP Client wrapper for JavaScript
#[wasm_bindgen]
pub struct JsHttpClient {
    inner: WasmHttpClient,
}

#[wasm_bindgen]
impl JsHttpClient {
    #[wasm_bindgen(constructor)]
    pub fn new(base_url: String) -> Self {
        Self {
            inner: WasmHttpClient::new(base_url),
        }
    }

    #[wasm_bindgen(js_name = setAuthToken)]
    pub fn set_auth_token(&mut self, token: String) {
        self.inner.set_auth_token(token);
    }

    #[wasm_bindgen(js_name = getAuthToken)]
    pub fn get_auth_token(&self) -> Option<String> {
        self.inner.auth_token().map(|s| s.to_string())
    }

    /// Perform a GET request
    #[wasm_bindgen(js_name = get)]
    pub fn get(&self, path: String) -> Promise {
        let client = self.inner.clone();
        future_to_promise(async move {
            let result: Result<JsValue, JsValue> = client
                .get::<serde_json::Value>(&path)
                .await
                .map(|v| serde_wasm_bindgen::to_value(&v).unwrap())
                .map_err(|e| JsValue::from_str(&e.to_string()));
            result
        })
    }

    /// Perform a POST request
    #[wasm_bindgen(js_name = post)]
    pub fn post(&self, path: String, body: JsValue) -> Promise {
        let client = self.inner.clone();
        future_to_promise(async move {
            let body: serde_json::Value = serde_wasm_bindgen::from_value(body)
                .map_err(|e| JsValue::from_str(&format!("Invalid JSON: {:?}", e)))?;

            let result: Result<JsValue, JsValue> = client
                .post::<serde_json::Value, _>(&path, &body)
                .await
                .map(|v| serde_wasm_bindgen::to_value(&v).unwrap())
                .map_err(|e| JsValue::from_str(&e.to_string()));
            result
        })
    }

    /// Perform a PUT request
    #[wasm_bindgen(js_name = put)]
    pub fn put(&self, path: String, body: JsValue) -> Promise {
        let client = self.inner.clone();
        future_to_promise(async move {
            let body: serde_json::Value = serde_wasm_bindgen::from_value(body)
                .map_err(|e| JsValue::from_str(&format!("Invalid JSON: {:?}", e)))?;

            let result: Result<JsValue, JsValue> = client
                .put::<serde_json::Value, _>(&path, &body)
                .await
                .map(|v| serde_wasm_bindgen::to_value(&v).unwrap())
                .map_err(|e| JsValue::from_str(&e.to_string()));
            result
        })
    }

    /// Perform a DELETE request
    #[wasm_bindgen(js_name = delete)]
    pub fn delete(&self, path: String) -> Promise {
        let client = self.inner.clone();
        future_to_promise(async move {
            let result: Result<JsValue, JsValue> = client
                .delete::<serde_json::Value>(&path)
                .await
                .map(|v| serde_wasm_bindgen::to_value(&v).unwrap())
                .map_err(|e| JsValue::from_str(&e.to_string()));
            result
        })
    }
}

/// MQTT Client wrapper for JavaScript
#[wasm_bindgen]
pub struct JsMqttClient {
    inner: WasmMqttClient,
}

#[wasm_bindgen]
impl JsMqttClient {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        Self {
            inner: WasmMqttClient::new(),
        }
    }

    /// Connect to MQTT broker
    #[wasm_bindgen(js_name = connect)]
    pub fn connect(&self, broker_url: String, client_id: String, username: Option<String>) -> Promise {
        let client = self.inner.clone();
        let config = MqttConfig {
            broker_url,
            client_id,
            username,
            password: None,
            keep_alive_secs: 30,
            clean_session: true,
        };

        future_to_promise(async move {
            client.connect(config).await
                .map(|_| JsValue::UNDEFINED)
                .map_err(|e| JsValue::from_str(&e.to_string()))
        })
    }

    /// Disconnect from broker
    #[wasm_bindgen(js_name = disconnect)]
    pub fn disconnect(&self) -> Promise {
        let client = self.inner.clone();
        future_to_promise(async move {
            client.disconnect().await
                .map(|_| JsValue::UNDEFINED)
                .map_err(|e| JsValue::from_str(&e.to_string()))
        })
    }

    /// Subscribe to a topic
    #[wasm_bindgen(js_name = subscribe)]
    pub fn subscribe(&self, topic: String, qos: u8) -> Promise {
        let qos = match qos {
            0 => MqttQoS::AtMostOnce,
            1 => MqttQoS::AtLeastOnce,
            2 => MqttQoS::ExactlyOnce,
            _ => MqttQoS::AtLeastOnce,
        };
        let client = self.inner.clone();
        future_to_promise(async move {
            client.subscribe(&topic, qos).await
                .map(|_| JsValue::UNDEFINED)
                .map_err(|e| JsValue::from_str(&e.to_string()))
        })
    }

    /// Unsubscribe from a topic
    #[wasm_bindgen(js_name = unsubscribe)]
    pub fn unsubscribe(&self, topic: String) -> Promise {
        let client = self.inner.clone();
        future_to_promise(async move {
            client.unsubscribe(&topic).await
                .map(|_| JsValue::UNDEFINED)
                .map_err(|e| JsValue::from_str(&e.to_string()))
        })
    }

    /// Publish a message
    #[wasm_bindgen(js_name = publish)]
    pub fn publish(&self, topic: String, payload: Vec<u8>, qos: u8) -> Promise {
        let qos = match qos {
            0 => MqttQoS::AtMostOnce,
            1 => MqttQoS::AtLeastOnce,
            2 => MqttQoS::ExactlyOnce,
            _ => MqttQoS::AtLeastOnce,
        };
        let message = MqttMessage::new(topic, payload).with_qos(qos);
        let client = self.inner.clone();
        future_to_promise(async move {
            client.publish(message).await
                .map(|_| JsValue::UNDEFINED)
                .map_err(|e| JsValue::from_str(&e.to_string()))
        })
    }

    /// Get connection state
    #[wasm_bindgen(js_name = getConnectionState)]
    pub fn get_connection_state(&self) -> String {
        match self.inner.connection_state() {
            agentlink_core::mqtt::MqttConnectionState::Disconnected => "disconnected".to_string(),
            agentlink_core::mqtt::MqttConnectionState::Connecting => "connecting".to_string(),
            agentlink_core::mqtt::MqttConnectionState::Connected => "connected".to_string(),
            agentlink_core::mqtt::MqttConnectionState::Reconnecting => "reconnecting".to_string(),
            agentlink_core::mqtt::MqttConnectionState::Disconnecting => "disconnecting".to_string(),
            agentlink_core::mqtt::MqttConnectionState::Failed => "failed".to_string(),
        }
    }

    /// Set event callback
    #[wasm_bindgen(js_name = onEvent)]
    pub fn on_event(&self, callback: js_sys::Function) {
        self.inner.on_event(move |event| {
            let event_obj = match event {
                agentlink_core::mqtt::MqttEvent::Connected => {
                    js_sys::Object::new()
                }
                agentlink_core::mqtt::MqttEvent::Disconnected => {
                    js_sys::Object::new()
                }
                agentlink_core::mqtt::MqttEvent::MessageReceived(msg) => {
                    let obj = js_sys::Object::new();
                    js_sys::Reflect::set(&obj, &"topic".into(), &msg.topic.into()).unwrap();
                    js_sys::Reflect::set(&obj, &"payload".into(), &js_sys::Uint8Array::from(&msg.payload[..])).unwrap();
                    obj
                }
                _ => js_sys::Object::new(),
            };

            let _ = callback.call1(&JsValue::NULL, &event_obj);
        });
    }
}

/// SDK version
#[wasm_bindgen(js_name = getVersion)]
pub fn get_version() -> String {
    agentlink_core::VERSION.to_string()
}

/// Login with email code
#[wasm_bindgen(js_name = loginWithEmailCode)]
pub fn login_with_email_code(base_url: String, email: String, code: String) -> Promise {
    future_to_promise(async move {
        let client = WasmHttpClient::new(base_url);
        let request = LoginRequest {
            identifier: email,
            code,
        };

        let result: Result<JsValue, JsValue> = client
            .post::<LoginResponse, _>("/auth/login/email-code", &request)
            .await
            .map(|v| serde_wasm_bindgen::to_value(&v).unwrap())
            .map_err(|e| JsValue::from_str(&e.to_string()));
        result
    })
}