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:
2 files changed
+93
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
|
import { check } from '@tauri-apps/plugin-updater';
|
||||||
|
import { relaunch } from '@tauri-apps/plugin-process';
|
||||||
import {
|
import {
|
||||||
acknowledgeRelease,
|
acknowledgeRelease,
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
@@ -316,3 +318,77 @@ export async function disableUpdateChecks(): Promise<void> {
|
|||||||
console.debug('[Updates] Failed to disable update checks:', error);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,8 +31,10 @@ let preferences: UpdatePreferences = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let currentVersion = '';
|
let currentVersion = '';
|
||||||
|
let autoUpdateEligible = false;
|
||||||
let versionLoaded = false;
|
let versionLoaded = false;
|
||||||
let prefsLoaded = false;
|
let prefsLoaded = false;
|
||||||
|
let eligibilityLoaded = false;
|
||||||
|
|
||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
@@ -63,6 +65,10 @@ export function getCurrentVersion(): string {
|
|||||||
return currentVersion;
|
return currentVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAutoUpdateEligible(): boolean {
|
||||||
|
return autoUpdateEligible;
|
||||||
|
}
|
||||||
|
|
||||||
export async function initUpdatesStore(): Promise<void> {
|
export async function initUpdatesStore(): Promise<void> {
|
||||||
if (skipIfRemote()) return;
|
if (skipIfRemote()) return;
|
||||||
// get_current_version never needs a session (compile-time value).
|
// 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
|
// get_update_preferences needs the SQLite store which is only
|
||||||
// available after login. Retry on each call until it succeeds,
|
// available after login. Retry on each call until it succeeds,
|
||||||
// so a pre-login failure doesn't lock in stale defaults.
|
// so a pre-login failure doesn't lock in stale defaults.
|
||||||
|
|||||||
Reference in new issue
Block a user