tauri_plugin_hot_update/
commands.rs1use std::time::{Duration, Instant};
15
16use serde::{Deserialize, Serialize};
17use tauri::{AppHandle, Emitter, Runtime, State};
18
19use crate::machine::AckOutcome;
20use crate::runtime::{BundleSource, CurrentBundle, HotUpdate};
21use crate::update::{UpdateConfig, UpdateOutcome};
22use crate::{Error, Result};
23
24pub const PROGRESS_EVENT: &str = "hot-update://progress";
29
30const PROGRESS_MIN_INTERVAL: Duration = Duration::from_millis(100);
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct DownloadProgress {
37 pub downloaded: u64,
38 pub total: u64,
39}
40
41pub(crate) struct CommandConfig {
44 pub(crate) update: Option<UpdateConfig>,
45}
46
47impl CommandConfig {
48 fn update_config(&self) -> Result<&UpdateConfig> {
49 self.update.as_ref().ok_or(Error::Disabled)
50 }
51
52 fn is_disabled(&self) -> bool {
53 self.update.is_none()
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
62#[serde(
63 tag = "status",
64 rename_all = "camelCase",
65 rename_all_fields = "camelCase"
66)]
67pub(crate) enum AckResult {
68 Committed { seq: u64 },
69 AlreadyCommitted { seq: u64 },
70 EmbeddedNoop,
71 Stale { seq: u64 },
72}
73
74impl From<AckOutcome> for AckResult {
75 fn from(outcome: AckOutcome) -> Self {
76 match outcome {
77 AckOutcome::Committed(seq) => Self::Committed { seq },
78 AckOutcome::AlreadyCommitted(seq) => Self::AlreadyCommitted { seq },
79 AckOutcome::EmbeddedNoop => Self::EmbeddedNoop,
80 AckOutcome::Stale(seq) => Self::Stale { seq },
81 }
82 }
83}
84
85pub(crate) struct ProgressThrottle {
89 min_interval: Duration,
90 last_emit: Option<Instant>,
91}
92
93impl ProgressThrottle {
94 pub(crate) fn new(min_interval: Duration) -> Self {
95 Self {
96 min_interval,
97 last_emit: None,
98 }
99 }
100
101 pub(crate) fn should_emit(&mut self, downloaded: u64, total: u64) -> bool {
102 let is_final = downloaded >= total;
103 let due = self
104 .last_emit
105 .map_or(true, |at| at.elapsed() >= self.min_interval);
106 if is_final || due {
107 self.last_emit = Some(Instant::now());
108 return true;
109 }
110 false
111 }
112}
113
114#[tauri::command]
119pub(crate) async fn check(
120 hot_update: State<'_, HotUpdate>,
121 config: State<'_, CommandConfig>,
122) -> Result<UpdateOutcome> {
123 hot_update.check(config.update_config()?).await
124}
125
126#[tauri::command]
131pub(crate) async fn download<R: Runtime>(
132 app: AppHandle<R>,
133 hot_update: State<'_, HotUpdate>,
134 config: State<'_, CommandConfig>,
135) -> Result<UpdateOutcome> {
136 let mut throttle = ProgressThrottle::new(PROGRESS_MIN_INTERVAL);
137 hot_update
138 .check_and_download(config.update_config()?, |downloaded, total| {
139 if throttle.should_emit(downloaded, total) {
140 if let Err(e) = app.emit(PROGRESS_EVENT, DownloadProgress { downloaded, total }) {
141 log::warn!("hot-update: failed to emit {PROGRESS_EVENT}: {e}");
142 }
143 }
144 })
145 .await
146}
147
148#[tauri::command]
152pub(crate) fn notify_app_ready(
153 hot_update: State<'_, HotUpdate>,
154 config: State<'_, CommandConfig>,
155) -> Result<AckResult> {
156 if config.is_disabled() {
157 return Ok(AckResult::EmbeddedNoop);
158 }
159 Ok(hot_update.notify_app_ready()?.into())
160}
161
162#[tauri::command]
165pub(crate) fn current_bundle<R: Runtime>(
166 app: AppHandle<R>,
167 hot_update: State<'_, HotUpdate>,
168 config: State<'_, CommandConfig>,
169) -> Result<CurrentBundle> {
170 if config.is_disabled() {
171 return Ok(CurrentBundle {
172 source: BundleSource::Embedded,
173 seq: None,
174 version: app.package_info().version.clone(),
175 });
176 }
177 hot_update.current_bundle()
178}
179
180#[tauri::command]
185pub(crate) fn reset(
186 hot_update: State<'_, HotUpdate>,
187 config: State<'_, CommandConfig>,
188) -> Result<()> {
189 if config.is_disabled() {
190 return Ok(());
191 }
192 hot_update.reset()
193}
194
195#[cfg(test)]
196mod tests;