1use serde::Serialize;
18
19use crate::error::AppError;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum EventKind {
28 BucketsUpdated,
29 ObjectsUpdated,
30 TransferProgress,
31 TransferState,
32 LockAcquired,
33 LockReleased,
34 NotificationNew,
35 SearchPage,
36 MediaRevoked,
37 UpdaterStatus,
38 KeychainFallbackRequired,
42}
43
44impl EventKind {
45 pub fn as_str(&self) -> &'static str {
50 match self {
51 Self::BucketsUpdated => "buckets:updated",
52 Self::ObjectsUpdated => "objects:updated",
53 Self::TransferProgress => "transfer:progress",
54 Self::TransferState => "transfer:state",
55 Self::LockAcquired => "lock:acquired",
56 Self::LockReleased => "lock:released",
57 Self::NotificationNew => "notification:new",
58 Self::SearchPage => "search:page",
59 Self::MediaRevoked => "media:revoked",
60 Self::UpdaterStatus => "updater:status",
61 Self::KeychainFallbackRequired => "keychain:fallback-required",
62 }
63 }
64}
65
66pub trait EventEmitter {
75 fn emit<P: Serialize + Clone>(&self, kind: EventKind, payload: P) -> Result<(), AppError>;
76}
77
78impl EventEmitter for tauri::AppHandle {
83 fn emit<P: Serialize + Clone>(&self, kind: EventKind, payload: P) -> Result<(), AppError> {
84 tauri::Emitter::emit(self, kind.as_str(), payload).map_err(|e| AppError::Network {
85 source: format!("tauri emit error: {e}"),
86 })
87 }
88}
89
90pub fn emit<P, E>(channel: &E, kind: EventKind, payload: P) -> Result<(), AppError>
99where
100 P: Serialize + Clone,
101 E: EventEmitter,
102{
103 channel.emit(kind, payload)
104}
105
106#[cfg(test)]
116#[derive(Default)]
117pub struct MockChannel {
118 recorded: std::sync::Mutex<Vec<(EventKind, serde_json::Value)>>,
119}
120
121#[cfg(test)]
122impl MockChannel {
123 pub fn emitted(&self) -> Vec<(EventKind, serde_json::Value)> {
125 self.recorded.lock().expect("lock poisoned").clone()
126 }
127}
128
129#[cfg(test)]
130impl EventEmitter for MockChannel {
131 fn emit<P: Serialize>(&self, kind: EventKind, payload: P) -> Result<(), AppError> {
132 let value = serde_json::to_value(payload).expect("MockChannel: payload must serialize");
133 self.recorded
134 .lock()
135 .expect("lock poisoned")
136 .push((kind, value));
137 Ok(())
138 }
139}
140
141#[cfg(test)]
146mod tests {
147 use super::*;
148 use serde_json::json;
149
150 #[test]
151 fn all_event_kind_strings_are_unique() {
152 let kinds = [
153 EventKind::BucketsUpdated,
154 EventKind::ObjectsUpdated,
155 EventKind::TransferProgress,
156 EventKind::TransferState,
157 EventKind::LockAcquired,
158 EventKind::LockReleased,
159 EventKind::NotificationNew,
160 EventKind::SearchPage,
161 EventKind::MediaRevoked,
162 EventKind::UpdaterStatus,
163 EventKind::KeychainFallbackRequired,
164 ];
165 let mut seen = std::collections::HashSet::new();
166 for k in &kinds {
167 let s = k.as_str();
168 assert!(seen.insert(s), "duplicate event name: {s}");
169 }
170 }
171
172 #[test]
173 fn event_kind_as_str_values() {
174 assert_eq!(EventKind::BucketsUpdated.as_str(), "buckets:updated");
175 assert_eq!(EventKind::ObjectsUpdated.as_str(), "objects:updated");
176 assert_eq!(EventKind::TransferProgress.as_str(), "transfer:progress");
177 assert_eq!(EventKind::TransferState.as_str(), "transfer:state");
178 assert_eq!(EventKind::LockAcquired.as_str(), "lock:acquired");
179 assert_eq!(EventKind::LockReleased.as_str(), "lock:released");
180 assert_eq!(EventKind::NotificationNew.as_str(), "notification:new");
181 assert_eq!(EventKind::SearchPage.as_str(), "search:page");
182 assert_eq!(EventKind::MediaRevoked.as_str(), "media:revoked");
183 assert_eq!(EventKind::UpdaterStatus.as_str(), "updater:status");
184 assert_eq!(
185 EventKind::KeychainFallbackRequired.as_str(),
186 "keychain:fallback-required"
187 );
188 }
189
190 #[test]
191 fn mock_channel_records_emission() {
192 let channel = MockChannel::default();
193 let payload = json!({
194 "profileId": "my-profile",
195 "bucket": "my-bucket",
196 "prefix": "folder/"
197 });
198 emit(&channel, EventKind::ObjectsUpdated, &payload).expect("emit should succeed");
199
200 let emitted = channel.emitted();
201 assert_eq!(emitted.len(), 1);
202 assert_eq!(emitted[0].0, EventKind::ObjectsUpdated);
203 assert_eq!(emitted[0].1["profileId"], "my-profile");
204 assert_eq!(emitted[0].1["bucket"], "my-bucket");
205 assert_eq!(emitted[0].1["prefix"], "folder/");
206 }
207
208 #[test]
209 fn mock_channel_records_multiple_emissions() {
210 let channel = MockChannel::default();
211 emit(
212 &channel,
213 EventKind::BucketsUpdated,
214 json!({"profileId": "p1"}),
215 )
216 .expect("first emit");
217 emit(
218 &channel,
219 EventKind::TransferState,
220 json!({"requestId": "r1", "state": "done"}),
221 )
222 .expect("second emit");
223
224 let emitted = channel.emitted();
225 assert_eq!(emitted.len(), 2);
226 assert_eq!(emitted[0].0, EventKind::BucketsUpdated);
227 assert_eq!(emitted[1].0, EventKind::TransferState);
228 }
229}