tauri_plugin_widgets/
store.rs1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12pub const CONFIG_KEY_PREFIX: &str = "config:";
14pub const PENDING_ACTIONS_KEY: &str = "pending_actions";
16pub const META_NONCE_KEY: &str = "__meta_nonce__";
18pub const META_UPDATED_AT_KEY: &str = "__meta_updated_at__";
20
21pub type DataMap = HashMap<String, String>;
22
23pub fn config_key(widget_id: &str) -> String {
25 format!("{CONFIG_KEY_PREFIX}{widget_id}")
26}
27
28pub fn now_ms() -> u64 {
30 SystemTime::now()
31 .duration_since(UNIX_EPOCH)
32 .map(|d| d.as_millis() as u64)
33 .unwrap_or(0)
34}
35
36pub fn map_nonce(map: &DataMap) -> u64 {
38 map.get(META_NONCE_KEY)
39 .and_then(|s| s.parse().ok())
40 .unwrap_or(0)
41}
42
43pub fn map_updated_at(map: &DataMap) -> u64 {
45 map.get(META_UPDATED_AT_KEY)
46 .and_then(|s| s.parse().ok())
47 .unwrap_or(0)
48}
49
50pub fn touch_meta(map: &mut DataMap) {
52 let next = map_nonce(map).saturating_add(1);
53 map.insert(META_NONCE_KEY.into(), next.to_string());
54 map.insert(META_UPDATED_AT_KEY.into(), now_ms().to_string());
55}
56
57pub fn touch_meta_above(map: &mut DataMap, floor: u64) {
63 let next = map_nonce(map)
64 .saturating_add(1)
65 .max(floor.saturating_add(1));
66 map.insert(META_NONCE_KEY.into(), next.to_string());
67 map.insert(META_UPDATED_AT_KEY.into(), now_ms().to_string());
68}
69
70pub fn pick_freshest(maps: impl IntoIterator<Item = DataMap>) -> DataMap {
72 let mut best: Option<DataMap> = None;
73 let mut best_nonce = 0u64;
74 let mut best_ts = 0u64;
75 for map in maps {
76 let n = map_nonce(&map);
77 let t = map_updated_at(&map);
78 let better = best.is_none() || n > best_nonce || (n == best_nonce && t > best_ts);
79 if better {
80 best_nonce = n;
81 best_ts = t;
82 best = Some(map);
83 }
84 }
85 best.unwrap_or_default()
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct WidgetActionEnvelope {
92 pub action: String,
93 #[serde(default)]
94 pub payload: Option<String>,
95 pub ts: u64,
97 pub widget_id: String,
98 pub group: String,
99}
100
101impl WidgetActionEnvelope {
102 pub fn new(
103 action: impl Into<String>,
104 payload: Option<String>,
105 widget_id: impl Into<String>,
106 group: impl Into<String>,
107 ) -> Self {
108 Self {
109 action: action.into(),
110 payload,
111 ts: now_ms(),
112 widget_id: widget_id.into(),
113 group: group.into(),
114 }
115 }
116}
117
118pub fn parse_pending_actions(raw: Option<&str>) -> Vec<WidgetActionEnvelope> {
120 let Some(s) = raw.filter(|s| !s.is_empty()) else {
121 return Vec::new();
122 };
123 serde_json::from_str(s).unwrap_or_default()
124}
125
126pub fn encode_pending_actions(actions: &[WidgetActionEnvelope]) -> crate::Result<String> {
128 Ok(serde_json::to_string(actions)?)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn touch_meta_above_beats_stale_sibling_nonce() {
137 let mut map = DataMap::new();
138 map.insert("config:example".into(), "{}".into());
139 map.insert(META_NONCE_KEY.into(), "9".into());
141 touch_meta_above(&mut map, 65);
142 assert_eq!(map_nonce(&map), 66);
143 }
144}