Refactor app updater a bit

- fixes things discovered in manual testing
- avoid $effect loops
This commit is contained in:
Mattias Granlund 2024-08-17 08:51:34 +01:00
parent 87b5590161
commit c9b5aa7c26
2 changed files with 80 additions and 52 deletions

View File

@ -14,13 +14,21 @@ import posthog from 'posthog-js';
import { derived, writable, type Readable } from 'svelte/store';
// TODO: Investigate why 'DOWNLOADED' is not in the type provided by Tauri.
export type Update =
| { version?: string; status?: UpdateStatus | 'DOWNLOADED'; body?: string }
| undefined;
export type Update = {
version?: string;
status?: UpdateStatus | 'DOWNLOADED';
body?: string;
manual: boolean;
};
export class UpdaterService {
private status = writable<UpdateStatus>(undefined);
private result = writable<UpdateManifest | undefined>(undefined, () => {
// True if manually initiated check.
private manual = writable(false);
// An object rather than string to prevent unique deduplication.
private status = writable<{ status: UpdateStatus } | undefined>(undefined);
private manifest = writable<UpdateManifest | undefined>(undefined, () => {
this.start();
return () => {
this.stop();
@ -28,41 +36,42 @@ export class UpdaterService {
});
update: Readable<Update | undefined> = derived(
[this.status, this.result],
([status, update]) => {
return { ...update, status };
[this.status, this.manifest, this.manual],
([status, result, manual]) => {
// Do not emit when up-to-date unless manually initiated.
if (status?.status === 'UPTODATE' && result && !manual) {
return;
}
return { ...result, ...status, manual };
},
undefined
);
// Needed to reset dismissed modal when version changes.
currentVersion = writable<string | undefined>(undefined);
readonly version = derived(this.update, (update) => update?.version);
prev: Update | undefined;
intervalId: any;
unlistenStatusFn: any;
unlistenManualCheckFn: any;
intervalId: any;
constructor() {}
private async start() {
const currentVersion = await getVersion();
this.currentVersion.set(currentVersion);
this.currentVersion.set(await getVersion());
this.unlistenManualCheckFn = listen<string>('menu://global/update/clicked', () => {
this.checkForUpdate(true);
});
this.unlistenStatusFn = await onUpdaterEvent((status) => {
const err = status.error;
if (err) {
showErrorToast(err);
posthog.capture('App Update Status Error', { error: err });
this.unlistenStatusFn = await onUpdaterEvent((event) => {
const { error, status } = event;
if (error) {
showErrorToast(error);
posthog.capture('App Update Status Error', { error });
}
this.status.set(status.status);
this.status.set({ status });
});
await this.checkForUpdate();
setInterval(async () => await this.checkForUpdate(), 3600000); // hourly
this.checkForUpdate();
}
private async stop() {
@ -74,17 +83,18 @@ export class UpdaterService {
}
}
async checkForUpdate(isManual = false) {
async checkForUpdate(manual = false) {
const update = await Promise.race([
checkUpdate(), // In DEV mode this never returns.
new Promise<UpdateResult>((resolve) =>
setTimeout(() => resolve({ shouldUpdate: false }), 30000)
)
]);
if (!update.shouldUpdate && isManual) {
this.status.set('UPTODATE');
this.manual.set(manual);
if (!update.shouldUpdate && manual) {
this.status.set({ status: 'UPTODATE' });
} else if (update.manifest) {
this.result.set(update.manifest);
this.manifest.set(update.manifest);
}
}

View File

@ -2,50 +2,58 @@
import Link from '../shared/Link.svelte';
import newVersionSvg from '$lib/assets/empty-state/app-new-version.svg?raw';
import upToDateSvg from '$lib/assets/empty-state/app-up-to-date.svg?raw';
import { UpdaterService } from '$lib/backend/updater';
import { UpdaterService, type Update } from '$lib/backend/updater';
import { getContext } from '$lib/utils/context';
import Button from '@gitbutler/ui/inputs/Button.svelte';
import Modal from '@gitbutler/ui/modal/Modal.svelte';
const updaterService = getContext(UpdaterService);
const update = updaterService.update;
const currentVersion = updaterService.currentVersion;
const status = $derived($update?.status);
const version = $derived($update?.version);
const update = updaterService.update;
let dismissed = $state(false);
let open = $state(false);
let modalRef: Modal;
let status = $state<Update['status']>();
let version = $state<Update['version']>();
let lastVersion: string | undefined;
let dismissed = false;
let modal: Modal;
$effect(() => {
if (version || (status && status !== 'ERROR' && !dismissed && !open)) {
open = true;
if ($update) {
console.log($update);
handleUpdate($update);
}
});
$effect(() => {
if (status === 'ERROR') {
open = false;
}
});
function handleUpdate(update: Update) {
version = update?.version;
$effect(() => {
if (open) {
modalRef.show();
} else {
modalRef.close();
if (version !== lastVersion) {
dismissed = false;
}
});
status = update?.status;
const manual = update?.manual;
if (manual) {
modal.show();
} else if (status === 'ERROR') {
modal.close();
} else if (status && status !== 'UPTODATE' && !dismissed) {
modal.show();
} else if (version && !dismissed) {
modal.show();
}
lastVersion = version;
}
function handleDismiss() {
dismissed = true;
open = false;
modal.close();
}
</script>
<Modal width="xsmall" bind:this={modalRef}>
<Modal width="xsmall" bind:this={modal}>
<div class="modal-illustration">
{#if status === 'UPTODATE' || status === 'DONE'}
{@html upToDateSvg}
@ -88,22 +96,32 @@
{#if status === 'UPTODATE'}
<Button style="pop" kind="solid" wide outline onclick={handleDismiss}>Got it!</Button>
{:else if status === 'DONE'}
<Button
style="pop"
kind="solid"
wide
outline
onclick={() => {
updaterService.relaunchApp();
}}
>
Restart
</Button>
{:else}
<Button
style="pop"
kind="solid"
wide
loading={status === 'PENDING' || status === 'DOWNLOADED'}
onclick={async () => {
await updaterService.installUpdate();
onclick={() => {
updaterService.installUpdate();
}}
>
{#if status === 'PENDING'}
Downloading update...
{:else if status === 'DOWNLOADED'}
Installing update...
{:else if status === 'DONE'}
Restart
{:else}
Download {version}
{/if}