Skip to main content

tauri_plugin_nostr_sync/
builder.rs

1use tauri::{plugin::TauriPlugin, Manager, Runtime};
2
3pub struct PluginBuilder {
4    pub(crate) relays: Vec<String>,
5    pub(crate) namespace: String,
6    pub(crate) device_id: String,
7    pub(crate) max_payload_size: usize,
8}
9
10impl PluginBuilder {
11    pub fn new() -> Self {
12        Self {
13            relays: Vec::new(),
14            namespace: "default".to_string(),
15            device_id: uuid::Uuid::new_v4().to_string(),
16            max_payload_size: crate::state::DEFAULT_PAYLOAD_LIMIT,
17        }
18    }
19
20    pub fn relays(mut self, urls: impl IntoIterator<Item = impl Into<String>>) -> Self {
21        self.relays = urls.into_iter().map(|u| u.into()).collect();
22        self
23    }
24
25    pub fn app_namespace(mut self, ns: impl Into<String>) -> Self {
26        self.namespace = ns.into();
27        self
28    }
29
30    /// Override the device identifier stamped on published events.
31    ///
32    /// Defaults to a random UUID generated at process start (ephemeral). Supply a
33    /// stable, per-device value — e.g. a UUID persisted in the app's data directory,
34    /// a hardware serial, or a hash of the machine name — so that consumers can
35    /// reliably attribute events to a specific device across restarts.
36    pub fn device_id(mut self, id: impl Into<String>) -> Self {
37        self.device_id = id.into();
38        self
39    }
40
41    /// Override the maximum payload size in bytes. Defaults to 64KB. Must not exceed 400KB.
42    ///
43    /// Payloads exceeding this limit return `Error::PayloadTooLarge` from `publish`.
44    /// Values above 400KB surface as `Error::InvalidPayloadLimit` at plugin startup.
45    pub fn max_payload_size(mut self, bytes: usize) -> Self {
46        self.max_payload_size = bytes;
47        self
48    }
49
50    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
51        // Panic on invalid namespace — consistent with Tauri builder conventions.
52        crate::state::validate_namespace(&self.namespace)
53            .unwrap_or_else(|e| panic!("invalid app_namespace: {e}"));
54
55        let relays = self.relays;
56        let namespace = self.namespace;
57        let device_id = self.device_id;
58        let max_payload_size = self.max_payload_size;
59
60        tauri::plugin::Builder::<R>::new("nostr-sync")
61            .invoke_handler(tauri::generate_handler![
62                crate::commands::publish,
63                crate::commands::fetch,
64                crate::commands::sync_all,
65                crate::commands::add_relay,
66                crate::commands::remove_relay,
67                crate::commands::get_relays,
68                crate::commands::get_pubkey,
69                crate::commands::get_status,
70                crate::commands::poll,
71            ])
72            .setup(move |app, api| {
73                #[cfg(mobile)]
74                {
75                    let plugin = crate::mobile::init(
76                        app,
77                        api,
78                        relays,
79                        &namespace,
80                        &device_id,
81                        max_payload_size,
82                    )?;
83                    app.manage(plugin);
84                }
85                #[cfg(desktop)]
86                {
87                    let _ = &api;
88                    let plugin = crate::desktop::init(
89                        app,
90                        relays,
91                        &namespace,
92                        &device_id,
93                        max_payload_size,
94                    )?;
95                    app.manage(plugin);
96                }
97                Ok(())
98            })
99            .build()
100    }
101}
102
103impl Default for PluginBuilder {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn defaults_to_default_namespace() {
115        let b = PluginBuilder::new();
116        assert_eq!(b.namespace, "default");
117    }
118
119    #[test]
120    fn defaults_to_empty_relays() {
121        let b = PluginBuilder::new();
122        assert!(b.relays.is_empty());
123    }
124
125    #[test]
126    fn defaults_to_ephemeral_device_id() {
127        let b = PluginBuilder::new();
128        assert!(!b.device_id.is_empty());
129        // Two builders get different ephemeral IDs.
130        assert_ne!(b.device_id, PluginBuilder::new().device_id);
131    }
132
133    #[test]
134    fn device_id_overrides_ephemeral() {
135        let b = PluginBuilder::new().device_id("my-stable-device");
136        assert_eq!(b.device_id, "my-stable-device");
137    }
138
139    #[test]
140    fn app_namespace_overrides_default() {
141        let b = PluginBuilder::new().app_namespace("sage");
142        assert_eq!(b.namespace, "sage");
143    }
144
145    #[test]
146    fn relays_stores_provided_urls() {
147        let b = PluginBuilder::new().relays(vec!["wss://relay.damus.io", "wss://nos.lol"]);
148        assert_eq!(b.relays, vec!["wss://relay.damus.io", "wss://nos.lol"]);
149    }
150
151    #[test]
152    fn max_payload_size_defaults_to_64kb() {
153        let b = PluginBuilder::new();
154        assert_eq!(b.max_payload_size, crate::state::DEFAULT_PAYLOAD_LIMIT);
155    }
156
157    #[test]
158    fn max_payload_size_setter_stores_value() {
159        let b = PluginBuilder::new().max_payload_size(128 * 1024);
160        assert_eq!(b.max_payload_size, 128 * 1024);
161    }
162}