tauri-plugin-window-system 0.1.1

Tauri plugin for multi-window registry, lifecycle, and routing.
Documentation
use crate::events::emit_to_window as emit_to_window_impl;
use crate::registry::{window_system_error, WindowRegistry, WindowSystemErrorKind};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Emitter, Manager, Runtime, State, Window};

pub const WINDOW_MESSAGE_EVENT: &str = "window-system:message";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WindowMessageScope {
    Direct,
    Broadcast,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WindowMessageKind {
    Event,
    Request,
    Response,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowMessageEnvelope {
    pub from: String,
    pub to: Option<String>,
    pub scope: WindowMessageScope,
    pub kind: WindowMessageKind,
    pub topic: String,
    pub correlation_id: Option<String>,
    pub payload: Value,
    pub timestamp_ms: u64,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendWindowMessageRequest {
    pub to: String,
    pub kind: WindowMessageKind,
    pub topic: String,
    pub correlation_id: Option<String>,
    pub payload: Value,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BroadcastWindowMessageRequest {
    pub kind: WindowMessageKind,
    pub topic: String,
    pub correlation_id: Option<String>,
    pub payload: Value,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowMessageDispatchResult {
    pub envelope: WindowMessageEnvelope,
    pub delivered_to: Vec<String>,
}

pub fn current_timestamp_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis() as u64)
        .unwrap_or_default()
}

pub fn current_live_window_labels<R: Runtime>(
    handle: &AppHandle<R>,
    registry: &WindowRegistry,
) -> Result<HashSet<String>, String> {
    let mut live_labels: HashSet<String> = handle.webview_windows().into_keys().collect();
    let active_windows = registry
        .list()?
        .into_iter()
        .map(|descriptor| descriptor.label)
        .collect::<HashSet<_>>();
    live_labels.extend(active_windows);
    Ok(live_labels)
}

pub fn build_window_message_envelope(
    from: impl Into<String>,
    to: Option<String>,
    scope: WindowMessageScope,
    kind: WindowMessageKind,
    topic: impl Into<String>,
    correlation_id: Option<String>,
    payload: Value,
) -> WindowMessageEnvelope {
    WindowMessageEnvelope {
        from: from.into(),
        to,
        scope,
        kind,
        topic: topic.into(),
        correlation_id,
        payload,
        timestamp_ms: current_timestamp_ms(),
    }
}

pub fn send_window_message<R: Runtime>(
    window: Window<R>,
    handle: AppHandle<R>,
    registry: State<'_, WindowRegistry>,
    request: SendWindowMessageRequest,
) -> Result<WindowMessageDispatchResult, String> {
    let sender = window.label().to_string();
    let live_labels = current_live_window_labels(&handle, &*registry)?;
    if !live_labels.contains(&request.to) {
        return Err(window_system_error(
            WindowSystemErrorKind::WindowNotFound,
            format!("window not found: {}", request.to),
        ));
    }

    let envelope = build_window_message_envelope(
        sender,
        Some(request.to.clone()),
        WindowMessageScope::Direct,
        request.kind,
        request.topic,
        request.correlation_id,
        request.payload,
    );

    emit_to_window_impl(
        &handle,
        &request.to,
        WINDOW_MESSAGE_EVENT,
        serde_json::to_value(&envelope).map_err(|err| err.to_string())?,
    )?;

    Ok(WindowMessageDispatchResult {
        envelope,
        delivered_to: vec![request.to],
    })
}

pub fn broadcast_window_message<R: Runtime>(
    window: Window<R>,
    handle: AppHandle<R>,
    registry: State<'_, WindowRegistry>,
    request: BroadcastWindowMessageRequest,
) -> Result<WindowMessageDispatchResult, String> {
    let sender = window.label().to_string();
    let live_labels = current_live_window_labels(&handle, &*registry)?;
    let delivered_to: Vec<String> = live_labels.iter().cloned().collect();
    let envelope = build_window_message_envelope(
        sender,
        None,
        WindowMessageScope::Broadcast,
        request.kind,
        request.topic,
        request.correlation_id,
        request.payload,
    );

    <AppHandle<R> as Emitter<R>>::emit(
        &handle,
        WINDOW_MESSAGE_EVENT,
        serde_json::to_value(&envelope).map_err(|err| err.to_string())?,
    )
    .map_err(|err: tauri::Error| err.to_string())?;

    Ok(WindowMessageDispatchResult {
        envelope,
        delivered_to,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn build_window_message_envelope_uses_consistent_shape() {
        let envelope = build_window_message_envelope(
            "main",
            Some("child".into()),
            WindowMessageScope::Direct,
            WindowMessageKind::Request,
            "window-system:ping",
            Some("corr-1".into()),
            json!({"hello": "world"}),
        );

        assert_eq!(envelope.from, "main");
        assert_eq!(envelope.to.as_deref(), Some("child"));
        assert_eq!(envelope.topic, "window-system:ping");
        assert_eq!(envelope.correlation_id.as_deref(), Some("corr-1"));
        assert_eq!(envelope.payload["hello"], "world");
        assert!(envelope.timestamp_ms > 0);
    }

    #[test]
    fn window_message_envelope_serializes_with_camel_case_fields() {
        let envelope = build_window_message_envelope(
            "main",
            None,
            WindowMessageScope::Broadcast,
            WindowMessageKind::Event,
            "window-system:status",
            None,
            json!({"windows": 3}),
        );
        let value = serde_json::to_value(envelope).expect("envelope should serialize");

        assert_eq!(value["scope"], "broadcast");
        assert_eq!(value["kind"], "event");
        assert_eq!(value["correlationId"], serde_json::Value::Null);
        assert!(value["timestampMs"].as_u64().unwrap_or_default() > 0);
    }
}