|
@@ -0,0 +1,156 @@
|
|
|
+import { check, type DownloadEvent } from "@tauri-apps/plugin-updater";
|
|
|
+import { relaunch } from "@tauri-apps/plugin-process";
|
|
|
+import { type LazyStore } from "@tauri-apps/plugin-store";
|
|
|
+import { UpdateStatus, CheckResult, UpdateState } from "~/types";
|
|
|
+
|
|
|
+export class UpdaterService {
|
|
|
+ constructor(private store: LazyStore) {}
|
|
|
+
|
|
|
+ async initialize(): Promise<void> {
|
|
|
+ await this.saveUpdateState({
|
|
|
+ status: UpdateStatus.IDLE
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async checkForUpdates(timeout = 2000): Promise<CheckResult> {
|
|
|
+ try {
|
|
|
+ await this.saveUpdateState({
|
|
|
+ status: UpdateStatus.CHECKING
|
|
|
+ });
|
|
|
+
|
|
|
+ // Create a timeout promise, this is just to make sure we don't keep checking for updates indefinitely
|
|
|
+ // NOTE: Also `checkUpdate` tends to hang indefinitely in dev mode, but works in build
|
|
|
+ const timeoutPromise = new Promise<null>((resolve) => {
|
|
|
+ setTimeout(() => {
|
|
|
+ console.log("Update check timeout reached, proceeding with app load");
|
|
|
+ resolve(null);
|
|
|
+ }, timeout);
|
|
|
+ });
|
|
|
+
|
|
|
+ const updateResult = await Promise.race([
|
|
|
+ check({ timeout }),
|
|
|
+ timeoutPromise
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // If we got a timeout (null), we treat it as no update available
|
|
|
+ // NOTE: We could maybe show more info but for now this works fine
|
|
|
+ if (!updateResult) {
|
|
|
+ console.log("Update check timed out or no update available");
|
|
|
+ await this.saveUpdateState({
|
|
|
+ status: UpdateStatus.NOT_AVAILABLE
|
|
|
+ });
|
|
|
+ return CheckResult.TIMEOUT;
|
|
|
+ }
|
|
|
+
|
|
|
+ const hasUpdates = updateResult.available;
|
|
|
+
|
|
|
+ await this.saveUpdateState(
|
|
|
+ hasUpdates
|
|
|
+ ? {
|
|
|
+ status: UpdateStatus.AVAILABLE,
|
|
|
+ version: updateResult.version,
|
|
|
+ message: updateResult.body
|
|
|
+ }
|
|
|
+ : {
|
|
|
+ status: UpdateStatus.NOT_AVAILABLE
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log("Update check result:", {
|
|
|
+ available: updateResult.available,
|
|
|
+ currentVersion: updateResult.currentVersion,
|
|
|
+ version: updateResult.version
|
|
|
+ });
|
|
|
+
|
|
|
+ return hasUpdates ? CheckResult.AVAILABLE : CheckResult.NOT_AVAILABLE;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Error checking for updates:", error);
|
|
|
+ await this.saveUpdateState({
|
|
|
+ status: UpdateStatus.ERROR,
|
|
|
+ message: String(error)
|
|
|
+ });
|
|
|
+ return CheckResult.ERROR;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async downloadAndInstall(): Promise<void> {
|
|
|
+ try {
|
|
|
+ const updateResult = await check();
|
|
|
+
|
|
|
+ if (!updateResult) {
|
|
|
+ throw new Error("No update available to install");
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.saveUpdateState({
|
|
|
+ status: UpdateStatus.DOWNLOADING,
|
|
|
+ progress: {
|
|
|
+ downloaded: 0,
|
|
|
+ total: undefined
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ let totalBytes: number | undefined;
|
|
|
+ let downloadedBytes = 0;
|
|
|
+
|
|
|
+ await updateResult.downloadAndInstall(
|
|
|
+ (event: DownloadEvent) => {
|
|
|
+ if (event.event === 'Started') {
|
|
|
+ totalBytes = event.data.contentLength;
|
|
|
+ this.saveUpdateState({
|
|
|
+ status: UpdateStatus.DOWNLOADING,
|
|
|
+ progress: {
|
|
|
+ downloaded: 0,
|
|
|
+ total: totalBytes
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else if (event.event === 'Progress') {
|
|
|
+ downloadedBytes += event.data.chunkLength;
|
|
|
+ this.saveUpdateState({
|
|
|
+ status: UpdateStatus.DOWNLOADING,
|
|
|
+ progress: {
|
|
|
+ downloaded: downloadedBytes,
|
|
|
+ total: totalBytes
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else if (event.event === 'Finished') {
|
|
|
+ this.saveUpdateState({
|
|
|
+ status: UpdateStatus.INSTALLING
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // If we reach here, it means the app hasn't restarted automatically
|
|
|
+ // Mark as ready to restart
|
|
|
+ await this.saveUpdateState({
|
|
|
+ status: UpdateStatus.READY_TO_RESTART
|
|
|
+ });
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Error installing updates:", error);
|
|
|
+ await this.saveUpdateState({
|
|
|
+ status: UpdateStatus.ERROR,
|
|
|
+ message: String(error)
|
|
|
+ });
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async restartApp(): Promise<void> {
|
|
|
+ try {
|
|
|
+ await relaunch();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Failed to restart app:", error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async saveUpdateState(state: UpdateState): Promise<void> {
|
|
|
+ try {
|
|
|
+ await this.store.set("updateState", state);
|
|
|
+ await this.store.save();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Failed to save update state:", error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|