Skip to main content

tauri_plugin_widgets/
apply.rs

1//! Outcomes for `set_widget_config` — no silent `Ok(true)`.
2
3use serde::{Deserialize, Serialize};
4use std::collections::hash_map::DefaultHasher;
5use std::hash::{Hash, Hasher};
6
7/// Why a config write was skipped.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(tag = "reason", rename_all = "camelCase")]
10pub enum SkipReason {
11    /// Store already holds identical JSON for this widget id.
12    Unchanged {
13        /// Hash of the unchanged config bytes.
14        hash: u64,
15    },
16    /// No live widget instances to deliver to.
17    NoInstances,
18    /// Selected transport is unavailable.
19    TransportUnavailable {
20        /// Transport name that was unavailable.
21        name: String,
22    },
23}
24
25/// Result of a WidgetKit / AppWidget reload attempt.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(tag = "outcome", rename_all = "camelCase")]
28pub enum ReloadOutcome {
29    /// Native reload was invoked.
30    Ok,
31    /// Mobile release throttle skipped the reload.
32    Throttled {
33        /// Seconds until the next reload is allowed.
34        #[serde(rename = "remainingSecs")]
35        remaining_secs: u64,
36    },
37    /// Reload intentionally not called.
38    Skipped {
39        /// Human-readable why (`unchanged`, `skip_reload`, …).
40        why: String,
41    },
42    /// Reload was attempted but failed.
43    Failed {
44        /// Error message.
45        error: String,
46    },
47}
48
49/// Full result of [`crate::desktop::Widget::set_widget_config`] /
50/// [`crate::mobile::Widget::set_widget_config`].
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct ApplyOutcome {
54    /// `true` when the config map was written (bytes differed from store).
55    pub written: bool,
56    /// What happened on the native reload path.
57    pub reload: ReloadOutcome,
58    /// Transport names that received the map (empty when `written` is false).
59    pub transports: Vec<String>,
60    /// Present when the write was skipped.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub skip: Option<SkipReason>,
63}
64
65impl ApplyOutcome {
66    /// Unchanged store — push may still fire on desktop; reload skipped.
67    pub fn unchanged(hash: u64) -> Self {
68        Self {
69            written: false,
70            reload: ReloadOutcome::Skipped {
71                why: "unchanged".into(),
72            },
73            transports: Vec::new(),
74            skip: Some(SkipReason::Unchanged { hash }),
75        }
76    }
77}
78
79/// Stable content hash for config JSON bytes (dedup vs store).
80pub fn config_content_hash(json: &str) -> u64 {
81    let mut hasher = DefaultHasher::new();
82    json.hash(&mut hasher);
83    hasher.finish()
84}
85
86/// Seconds remaining until the next reload is allowed, if currently throttled.
87pub fn throttle_remaining_secs(elapsed_secs: u64, min_interval: u64) -> Option<u64> {
88    if min_interval == 0 || elapsed_secs >= min_interval {
89        None
90    } else {
91        Some(min_interval.saturating_sub(elapsed_secs))
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn unchanged_serializes_camel_case() {
101        let o = ApplyOutcome::unchanged(42);
102        let v = serde_json::to_value(&o).unwrap();
103        assert_eq!(v["written"], false);
104        assert_eq!(v["reload"]["outcome"], "skipped");
105        assert_eq!(v["skip"]["reason"], "unchanged");
106        assert_eq!(v["skip"]["hash"], 42);
107    }
108
109    #[test]
110    fn throttled_remaining_secs_camel() {
111        let r = ReloadOutcome::Throttled {
112            remaining_secs: 840,
113        };
114        let v = serde_json::to_value(&r).unwrap();
115        assert_eq!(v["outcome"], "throttled");
116        assert_eq!(v["remainingSecs"], 840);
117    }
118
119    #[test]
120    fn throttle_remaining_math() {
121        assert_eq!(throttle_remaining_secs(0, 0), None);
122        assert_eq!(throttle_remaining_secs(100, 0), None);
123        assert_eq!(throttle_remaining_secs(60, 900), Some(840));
124        assert_eq!(throttle_remaining_secs(900, 900), None);
125        assert_eq!(throttle_remaining_secs(901, 900), None);
126    }
127
128    #[test]
129    fn same_json_same_hash() {
130        assert_eq!(
131            config_content_hash(r#"{"version":1}"#),
132            config_content_hash(r#"{"version":1}"#)
133        );
134        assert_ne!(
135            config_content_hash(r#"{"version":1}"#),
136            config_content_hash(r#"{"version":2}"#)
137        );
138    }
139}