tauri_plugin_window_system/
message.rs1use 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}