Skip to main content

tauri_plugin_window_system/
message.rs

1use crate::events::emit_to_window as emit_to_window_impl;
2use crate::registry::{window_system_error, WindowRegistry, WindowSystemErrorKind};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashSet;
6use std::time::{SystemTime, UNIX_EPOCH};
7use tauri::{AppHandle, Emitter, Manager, Runtime, State, Window};
8
9pub const WINDOW_MESSAGE_EVENT: &str = "window-system:message";
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum WindowMessageScope {
14    Direct,
15    Broadcast,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "kebab-case")]
20pub enum WindowMessageKind {
21    Event,
22    Request,
23    Response,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct WindowMessageEnvelope {
29    pub from: String,
30    pub to: Option<String>,
31    pub scope: WindowMessageScope,
32    pub kind: WindowMessageKind,
33    pub topic: String,
34    pub correlation_id: Option<String>,
35    pub payload: Value,
36    pub timestamp_ms: u64,
37}
38
39#[derive(Debug, Clone, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct SendWindowMessageRequest {
42    pub to: String,
43    pub kind: WindowMessageKind,
44    pub topic: String,
45    pub correlation_id: Option<String>,
46    pub payload: Value,
47}
48
49#[derive(Debug, Clone, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct BroadcastWindowMessageRequest {
52    pub kind: WindowMessageKind,
53    pub topic: String,
54    pub correlation_id: Option<String>,
55    pub payload: Value,
56}
57
58#[derive(Debug, Clone, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct WindowMessageDispatchResult {
61    pub envelope: WindowMessageEnvelope,
62    pub delivered_to: Vec<String>,
63}
64
65pub fn current_timestamp_ms() -> u64 {
66    SystemTime::now()
67        .duration_since(UNIX_EPOCH)
68        .map(|duration| duration.as_millis() as u64)
69        .unwrap_or_default()
70}
71
72pub fn current_live_window_labels<R: Runtime>(
73    handle: &AppHandle<R>,
74    registry: &WindowRegistry,
75) -> Result<HashSet<String>, String> {
76    let mut live_labels: HashSet<String> = handle.webview_windows().into_keys().collect();
77    let active_windows = registry
78        .list()?
79        .into_iter()
80        .map(|descriptor| descriptor.label)
81        .collect::<HashSet<_>>();
82    live_labels.extend(active_windows);
83    Ok(live_labels)
84}
85
86pub fn build_window_message_envelope(
87    from: impl Into<String>,
88    to: Option<String>,
89    scope: WindowMessageScope,
90    kind: WindowMessageKind,
91    topic: impl Into<String>,
92    correlation_id: Option<String>,
93    payload: Value,
94) -> WindowMessageEnvelope {
95    WindowMessageEnvelope {
96        from: from.into(),
97        to,
98        scope,
99        kind,
100        topic: topic.into(),
101        correlation_id,
102        payload,
103        timestamp_ms: current_timestamp_ms(),
104    }
105}
106
107pub fn send_window_message<R: Runtime>(
108    window: Window<R>,
109    handle: AppHandle<R>,
110    registry: State<'_, WindowRegistry>,
111    request: SendWindowMessageRequest,
112) -> Result<WindowMessageDispatchResult, String> {
113    let sender = window.label().to_string();
114    let live_labels = current_live_window_labels(&handle, &*registry)?;
115    if !live_labels.contains(&request.to) {
116        return Err(window_system_error(
117            WindowSystemErrorKind::WindowNotFound,
118            format!("window not found: {}", request.to),
119        ));
120    }
121
122    let envelope = build_window_message_envelope(
123        sender,
124        Some(request.to.clone()),
125        WindowMessageScope::Direct,
126        request.kind,
127        request.topic,
128        request.correlation_id,
129        request.payload,
130    );
131
132    emit_to_window_impl(
133        &handle,
134        &request.to,
135        WINDOW_MESSAGE_EVENT,
136        serde_json::to_value(&envelope).map_err(|err| err.to_string())?,
137    )?;
138
139    Ok(WindowMessageDispatchResult {
140        envelope,
141        delivered_to: vec![request.to],
142    })
143}
144
145pub fn broadcast_window_message<R: Runtime>(
146    window: Window<R>,
147    handle: AppHandle<R>,
148    registry: State<'_, WindowRegistry>,
149    request: BroadcastWindowMessageRequest,
150) -> Result<WindowMessageDispatchResult, String> {
151    let sender = window.label().to_string();
152    let live_labels = current_live_window_labels(&handle, &*registry)?;
153    let delivered_to: Vec<String> = live_labels.iter().cloned().collect();
154    let envelope = build_window_message_envelope(
155        sender,
156        None,
157        WindowMessageScope::Broadcast,
158        request.kind,
159        request.topic,
160        request.correlation_id,
161        request.payload,
162    );
163
164    <AppHandle<R> as Emitter<R>>::emit(
165        &handle,
166        WINDOW_MESSAGE_EVENT,
167        serde_json::to_value(&envelope).map_err(|err| err.to_string())?,
168    )
169    .map_err(|err: tauri::Error| err.to_string())?;
170
171    Ok(WindowMessageDispatchResult {
172        envelope,
173        delivered_to,
174    })
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use serde_json::json;
181
182    #[test]
183    fn build_window_message_envelope_uses_consistent_shape() {
184        let envelope = build_window_message_envelope(
185            "main",
186            Some("child".into()),
187            WindowMessageScope::Direct,
188            WindowMessageKind::Request,
189            "window-system:ping",
190            Some("corr-1".into()),
191            json!({"hello": "world"}),
192        );
193
194        assert_eq!(envelope.from, "main");
195        assert_eq!(envelope.to.as_deref(), Some("child"));
196        assert_eq!(envelope.topic, "window-system:ping");
197        assert_eq!(envelope.correlation_id.as_deref(), Some("corr-1"));
198        assert_eq!(envelope.payload["hello"], "world");
199        assert!(envelope.timestamp_ms > 0);
200    }
201
202    #[test]
203    fn window_message_envelope_serializes_with_camel_case_fields() {
204        let envelope = build_window_message_envelope(
205            "main",
206            None,
207            WindowMessageScope::Broadcast,
208            WindowMessageKind::Event,
209            "window-system:status",
210            None,
211            json!({"windows": 3}),
212        );
213        let value = serde_json::to_value(envelope).expect("envelope should serialize");
214
215        assert_eq!(value["scope"], "broadcast");
216        assert_eq!(value["kind"], "event");
217        assert_eq!(value["correlationId"], serde_json::Value::Null);
218        assert!(value["timestampMs"].as_u64().unwrap_or_default() > 0);
219    }
220}