Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions apps/desktop/main/updater/update-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ export class UpdateManager {
onChecking: () => {
const diagnostic = this.getDiagnostic();
this.logCheck("update check event: checking for update", diagnostic);
this.send("update:checking", diagnostic);
if (this.userInitiatedCheck) {
this.send("update:checking", diagnostic);
}
},
onAvailable: (info) => {
const diagnostic = this.getDiagnostic({
Expand Down Expand Up @@ -400,8 +402,9 @@ export class UpdateManager {
"update check: background download already complete, surfacing",
this.getDiagnostic(),
);
// Brief "checking" flash so the settings button shows a transition
this.send("update:checking", this.getDiagnostic());
if (this.userInitiatedCheck) {
this.send("update:checking", this.getDiagnostic());
}
this.send("update:downloaded", { version: this.pendingVersion });
return { updateAvailable: true };
}
Expand All @@ -416,8 +419,9 @@ export class UpdateManager {
this.getDiagnostic(),
);

// Brief "checking" flash so the settings button shows a transition
this.send("update:checking", this.getDiagnostic());
if (this.userInitiatedCheck) {
this.send("update:checking", this.getDiagnostic());
}

const diagnostic = this.getDiagnostic({
remoteVersion: this.pendingVersion,
Expand Down
11 changes: 7 additions & 4 deletions apps/desktop/shared/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,10 +453,7 @@ export type HostInvokeResultMap = {
"update:download": { ok: boolean };
"update:install": undefined;
"update:get-current-version": { version: string };
"update:get-status": {
phase: "idle" | "downloading" | "ready";
version: string | null;
};
"update:get-status": DesktopUpdateStatus;
"update:set-channel": { ok: boolean };
"update:set-source": { ok: boolean };
"component:check": {
Expand Down Expand Up @@ -748,6 +745,12 @@ export interface UpdateCheckDiagnostic {
remoteReleaseDate?: string;
}

export interface DesktopUpdateStatus {
phase: "idle" | "downloading" | "ready";
version: string | null;
percent: number;
}

export type UpdaterEventMap = {
"update:checking": UpdateCheckDiagnostic;
"update:available": {
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/hooks/use-auto-update.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useCallback, useEffect, useState } from "react";
import type { DesktopUpdateCapability } from "../../shared/host";
import type { DesktopUpdateStatus } from "../../shared/host";
import type { DesktopUpdateExperience } from "../../shared/update-policy";
import {
checkForUpdate,
downloadUpdate,
getUpdateCapability,
getUpdateStatus,
installUpdate,
} from "../lib/host-api";
import { resolveLocale } from "../lib/i18n";
Expand Down Expand Up @@ -58,6 +60,30 @@ export function restorePhaseAfterInstall(
: state;
}

export function applyUpdateStatus(
state: UpdateState,
status: DesktopUpdateStatus,
): UpdateState {
if (state.phase === "installing") {
return state;
}

if (
(status.phase === "downloading" || status.phase === "ready") &&
status.version
) {
return {
...state,
phase: status.phase,
version: status.version,
percent: status.percent,
dismissed: false,
Comment thread
anthhub marked this conversation as resolved.
};
}

return state;
}

export function useAutoUpdate(options?: {
experience?: DesktopUpdateExperience;
}) {
Expand Down Expand Up @@ -97,6 +123,31 @@ export function useAutoUpdate(options?: {
};
}, []);

useEffect(() => {
let cancelled = false;

const pollStatus = () => {
void getUpdateStatus()
.then((status) => {
if (cancelled) {
return;
}
setState((prev) => applyUpdateStatus(prev, status));
})
.catch(() => {
// Ignore transient bridge errors and rely on event-driven updates.
});
};

pollStatus();
const pollTimer = window.setInterval(pollStatus, 1000);

return () => {
cancelled = true;
window.clearInterval(pollTimer);
};
}, []);

useEffect(() => {
const updater = window.nexuUpdater;
if (!updater) return;
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/lib/host-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
AppInfo,
DesktopRuntimeConfig,
DesktopUpdateCapability,
DesktopUpdateStatus,
DiagnosticsExportResult,
DiagnosticsInfo,
HostDesktopCommand,
Expand Down Expand Up @@ -202,6 +203,10 @@ export async function getUpdateCapability(): Promise<DesktopUpdateCapability> {
return getHostBridge().invoke("update:get-capability", undefined);
}

export async function getUpdateStatus(): Promise<DesktopUpdateStatus> {
return getHostBridge().invoke("update:get-status", undefined);
}

export async function downloadUpdate(): Promise<boolean> {
const result = await getHostBridge().invoke("update:download", undefined);
return result.ok;
Expand Down
61 changes: 60 additions & 1 deletion tests/auto-update-install-phase.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,67 @@
import { describe, expect, it } from "vitest";
import { restorePhaseAfterInstall as restoreDesktopPhase } from "../apps/desktop/src/hooks/use-auto-update";
import type { DesktopUpdateStatus } from "../apps/desktop/shared/host";
import {
applyUpdateStatus,
restorePhaseAfterInstall as restoreDesktopPhase,
} from "../apps/desktop/src/hooks/use-auto-update";
import { restorePhaseAfterInstall as restoreWebPhase } from "../apps/web/src/hooks/use-auto-update";

describe("desktop useAutoUpdate", () => {
it("hydrates downloading state from polled main-process status", () => {
const status: DesktopUpdateStatus = {
phase: "downloading",
version: "1.2.3",
percent: 42,
};

expect(
applyUpdateStatus(
{
capability: null,
phase: "idle",
version: null,
releaseNotes: null,
actionUrl: null,
percent: 0,
errorMessage: null,
dismissed: true,
userInitiated: false,
},
status,
),
).toMatchObject({
phase: "downloading",
version: "1.2.3",
percent: 42,
dismissed: false,
});
});

it("does not override the installing phase with polled status", () => {
const status: DesktopUpdateStatus = {
phase: "ready",
version: "1.2.3",
percent: 100,
};

expect(
applyUpdateStatus(
{
capability: null,
phase: "installing",
version: "1.0.0",
releaseNotes: null,
actionUrl: null,
percent: 0,
errorMessage: null,
dismissed: false,
userInitiated: false,
},
status,
).phase,
).toBe("installing");
});

it("restores the prior actionable phase after install returns without quitting", () => {
expect(
restoreDesktopPhase(
Expand Down
17 changes: 15 additions & 2 deletions tests/desktop/update-manager-full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,9 @@ describe("bindEvents", () => {
// checking-for-update
// -------------------------------------------------------------------------

it("checking-for-update: calls logCheck and sends update:checking", async () => {
const { win } = await createManager();
it("checking-for-update: user-initiated checks send update:checking", async () => {
const { mgr, win } = await createManager();
(mgr as { userInitiatedCheck: boolean }).userInitiatedCheck = true;
const handlers = extractHandlers();

handlers["checking-for-update"]();
Expand All @@ -263,6 +264,18 @@ describe("bindEvents", () => {
);
});

it("checking-for-update: background checks do not send update:checking", async () => {
const { win } = await createManager();
const handlers = extractHandlers();

handlers["checking-for-update"]();

expect(win.webContents.send).not.toHaveBeenCalledWith(
"update:checking",
expect.anything(),
);
});

// -------------------------------------------------------------------------
// update-available
// -------------------------------------------------------------------------
Expand Down
Loading