feat(telemetry): handle application shutdown process

This update introduces a structured shutdown process for the application. It ensures that background tasks are properly terminated and telemetry is notified when the frontend is shutting down, preventing any ongoing operations from continuing during this state.

- Added event listeners to manage app shutdown events
- Implemented a shutdown handler to clear timers and notify telemetry
- Updated background task checks to respect the shutdown state
This commit is contained in:
Christoph Brandau
2026-08-10 17:29:03 +02:00
parent a6c86daf62
commit ac7cacd687
2 changed files with 42 additions and 9 deletions
+34 -9
View File
@@ -1,9 +1,11 @@
<script lang="ts">
import { onDestroy, onMount, tick } from "svelte";
import { getVersion } from "@tauri-apps/api/app";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
import { beginFrontendShutdown } from "./lib/telemetry";
import TitleBar from "./lib/TitleBar.svelte";
import RepoToolbar from "./lib/RepoToolbar.svelte";
@@ -376,6 +378,8 @@
let backgroundFetchInFlight = false;
let backgroundRepoStatusInFlight = false;
let backgroundRepoStatusIndex = 0;
let appShuttingDown = false;
let unlistenCloseRequested: (() => void) | undefined;
let lastRepoSwitchAt = 0;
let updateToastOpen = false;
let updateToastState: UpdateToastState = "available";
@@ -476,21 +480,40 @@
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
void runStartupSequence();
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
window.addEventListener("beforeunload", handleAppShutdown);
window.addEventListener("pagehide", handleAppShutdown);
void getCurrentWindow().onCloseRequested(() => handleAppShutdown()).then((unlisten) => {
if (appShuttingDown) unlisten();
else unlistenCloseRequested = unlisten;
}).catch(() => {
// Browser preview has no Tauri window; DOM lifecycle events still cover it.
});
});
onDestroy(() => {
handleAppShutdown();
themeMediaQuery?.removeEventListener("change", handleSystemThemeChange);
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
if (backgroundRepoStatusTimer) clearInterval(backgroundRepoStatusTimer);
if (backgroundFetchTimer) clearInterval(backgroundFetchTimer);
if (commitAiPollTimer) clearInterval(commitAiPollTimer);
window.removeEventListener("beforeunload", handleAppShutdown);
window.removeEventListener("pagehide", handleAppShutdown);
unlistenCloseRequested?.();
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
Object.values(errorAutoHideStates).forEach((state) => {
if (state?.timer) clearTimeout(state.timer);
});
if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId);
// Do not invoke backend cancellation here: Windows may already be shutting
// down, and starting another IPC/Git operation is precisely what we avoid.
});
function handleAppShutdown() {
if (appShuttingDown) return;
appShuttingDown = true;
beginFrontendShutdown();
if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = undefined; }
if (backgroundRepoStatusTimer) { clearInterval(backgroundRepoStatusTimer); backgroundRepoStatusTimer = undefined; }
if (backgroundFetchTimer) { clearInterval(backgroundFetchTimer); backgroundFetchTimer = undefined; }
if (commitAiPollTimer) { clearInterval(commitAiPollTimer); commitAiPollTimer = undefined; }
}
function wait(ms: number): Promise<void> {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
@@ -528,6 +551,7 @@
}
function startBackgroundTimers() {
if (appShuttingDown) return;
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
backgroundFetchTimer = setInterval(() => { void backgroundFetchTick(); }, BACKGROUND_FETCH_INTERVAL);
@@ -630,7 +654,7 @@
// Errors are swallowed here — auth/network failures surface via the manual Fetch/Pull/
// Push buttons instead, not as a background popup.
async function backgroundFetchTick() {
if (!autoRefreshEnabled) return;
if (appShuttingDown || !autoRefreshEnabled) return;
if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight
&& !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) {
@@ -650,7 +674,7 @@
}
async function backgroundFetchRepo(path: string) {
if (!autoRefreshEnabled || !path || backgroundFetchInFlight) return;
if (appShuttingDown || !autoRefreshEnabled || !path || backgroundFetchInFlight) return;
backgroundFetchInFlight = true;
try {
@@ -700,7 +724,7 @@
}
async function backgroundRepoStatusTick(fetchFirst: boolean) {
if (!autoRefreshEnabled || backgroundRepoStatusInFlight) return;
if (appShuttingDown || !autoRefreshEnabled || backgroundRepoStatusInFlight) return;
const others = knownRepoPathsForBackground();
if (others.length === 0) return;
@@ -711,6 +735,7 @@
// still never running more than one `git fetch` subprocess at a time.
const batchSize = fetchFirst ? BACKGROUND_FETCH_OTHER_BATCH_SIZE : BACKGROUND_REPO_STATUS_BATCH_SIZE;
for (let step = 0; step < Math.min(batchSize, others.length); step++) {
if (appShuttingDown) break;
if (backgroundRepoStatusIndex >= others.length) backgroundRepoStatusIndex = 0;
const path = others[backgroundRepoStatusIndex];
backgroundRepoStatusIndex += 1;
@@ -729,7 +754,7 @@
}
async function autoRefreshTick() {
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
const path = activeRepoPath;
autoRefreshInFlight = true;
try {