feat(updater): add performAutoUpdate and eligibility to store/service

Additive changes only: adds isAutoUpdateEligible() getter and
eligibility fetch in initUpdatesStore (keeps existing skipIfRemote
guards); adds performAutoUpdate() in updatesService for the
download/install/relaunch flow with cancellation support.

cc
This commit is contained in:
vicrodh committed 2026-04-13 14:57:57 -06:00
1 parent 46f8693f16
commit 10b5d95852
2 files changed
+93

No files matched your search

+76
View File
@@ -1,5 +1,7 @@
import { invoke } from '@tauri-apps/api/core';
import { openUrl } from '@tauri-apps/plugin-opener';
import { check } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
import {
acknowledgeRelease,
checkForUpdates,
@@ -316,3 +318,77 @@ export async function disableUpdateChecks(): Promise<void> {
console.debug('[Updates] Failed to disable update checks:', error);
}
}
// ==================== Auto-update ====================
export type AutoUpdateProgress =
| { state: 'checking' }
| { state: 'downloading'; downloadedBytes: number; totalBytes?: number }
| { state: 'installing' }
| { state: 'restarting' }
| { state: 'error'; error: string };
let autoUpdateInProgress = false;
export function isAutoUpdateInProgress(): boolean {
return autoUpdateInProgress;
}
/**
* Download, install and relaunch the pending update.
*
* `isCancelled` is polled before kicking off the download and again before the
* final relaunch so the caller can abort mid-flight.
* Returns true when the relaunch was triggered, false otherwise.
*/
export async function performAutoUpdate(
onProgress: (progress: AutoUpdateProgress) => void,
isCancelled?: () => boolean,
): Promise<boolean> {
if (autoUpdateInProgress) {
console.debug('[Updates] Auto-update already in progress');
return false;
}
autoUpdateInProgress = true;
try {
onProgress({ state: 'checking' });
const update = await check();
if (!update) return false;
if (isCancelled?.()) return false;
let totalBytes: number | undefined;
let downloadedBytes = 0;
onProgress({ state: 'downloading', downloadedBytes: 0 });
await update.downloadAndInstall((event) => {
switch (event.event) {
case 'Started':
totalBytes = event.data.contentLength ?? undefined;
onProgress({ state: 'downloading', downloadedBytes: 0, totalBytes });
break;
case 'Progress':
downloadedBytes += event.data.chunkLength;
onProgress({ state: 'downloading', downloadedBytes, totalBytes });
break;
case 'Finished':
onProgress({ state: 'installing' });
break;
}
});
if (isCancelled?.()) return false;
onProgress({ state: 'restarting' });
await relaunch();
return true;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error('[Updates] Auto-update failed:', message);
onProgress({ state: 'error', error: message });
return false;
} finally {
autoUpdateInProgress = false;
}
}
+17
View File
@@ -31,8 +31,10 @@ let preferences: UpdatePreferences = {
};
let currentVersion = '';
let autoUpdateEligible = false;
let versionLoaded = false;
let prefsLoaded = false;
let eligibilityLoaded = false;
const listeners = new Set<() => void>();
@@ -63,6 +65,10 @@ export function getCurrentVersion(): string {
return currentVersion;
}
export function isAutoUpdateEligible(): boolean {
return autoUpdateEligible;
}
export async function initUpdatesStore(): Promise<void> {
if (skipIfRemote()) return;
// get_current_version never needs a session (compile-time value).
@@ -76,6 +82,17 @@ export async function initUpdatesStore(): Promise<void> {
}
}
// Auto-update eligibility is compile-time + runtime (APPIMAGE env, flatpak/snap
// detection). No session required. Only fetch once.
if (!eligibilityLoaded) {
try {
autoUpdateEligible = await invoke<boolean>('v2_is_auto_update_eligible');
eligibilityLoaded = true;
} catch (error) {
console.debug('[Updates] Failed to check auto-update eligibility:', error);
}
}
// get_update_preferences needs the SQLite store which is only
// available after login. Retry on each call until it succeeds,
// so a pre-login failure doesn't lock in stale defaults.