Skip to content

Commit ba99519

Browse files
committed
Download updates automatically on startup
Startup still checks after 5s, but now also downloads and stages the install. Relaunch stays a user action so unsaved work is not interrupted. Manual "check for updates" still only probes.
1 parent 16d89f6 commit ba99519

4 files changed

Lines changed: 83 additions & 7 deletions

File tree

docs/updater.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ tauri-action 会自动:
4545
2. 汇总各平台的版本号、下载地址和签名,生成 `latest.json` 并上传到该 Release。
4646

4747
之后已安装的客户端访问
48-
`releases/latest/download/latest.json` 即可发现并下载新版本。
48+
`releases/latest/download/latest.json` 即可发现新版本。应用启动约 5 秒后会自动检查,
49+
发现更新后自动下载并安装到待应用状态;为了避免中断未保存工作,应用不会自动退出,
50+
用户可在设置 → 概览 →「应用更新」中点击「重启并安装」完成切换。手动点击「检查更新」
51+
仍只检查版本,不会自动下载。
4952

5053
## 3. 发布流程
5154

src/lib/updater.svelte.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { relaunch } from '@tauri-apps/plugin-process';
55

66
export type UpdatePhase = 'idle' | 'checking' | 'latest' | 'available' | 'downloading' | 'ready' | 'error';
77

8-
class UpdaterState {
8+
export class UpdaterState {
99
phase = $state<UpdatePhase>('idle');
1010
/** 可用的新版本号(phase 为 available/downloading/ready 时有效)。 */
1111
version = $state('');
@@ -19,16 +19,20 @@ class UpdaterState {
1919
return this.phase === 'available' || this.phase === 'downloading' || this.phase === 'ready';
2020
}
2121

22-
/** 检查更新。`silent` 用于启动时的后台检查:失败/无更新都保持安静。 */
23-
async check(silent = false) {
22+
/**
23+
* Check for updates. Startup checks can pass `autoInstall` to download and
24+
* install immediately. Relaunch stays user-controlled to avoid lost work.
25+
*/
26+
async check(silent = false, autoInstall = false) {
2427
if (this.phase === 'checking' || this.phase === 'downloading' || this.phase === 'ready') return;
25-
if (!silent) this.phase = 'checking';
28+
this.phase = 'checking';
2629
try {
2730
const u = await check();
2831
if (u) {
2932
this.#update = u;
3033
this.version = u.version;
3134
this.phase = 'available';
35+
if (autoInstall) await this.download();
3236
} else {
3337
this.phase = silent ? 'idle' : 'latest';
3438
}

src/lib/updater.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { UpdaterState } from './updater.svelte';
3+
4+
const { check, relaunch } = vi.hoisted(() => ({ check: vi.fn(), relaunch: vi.fn() }));
5+
6+
vi.mock('@tauri-apps/plugin-updater', () => ({ check }));
7+
vi.mock('@tauri-apps/plugin-process', () => ({ relaunch }));
8+
9+
describe('UpdaterState', () => {
10+
beforeEach(() => {
11+
check.mockReset();
12+
relaunch.mockReset();
13+
});
14+
15+
const update = () => ({
16+
version: '0.3.2',
17+
downloadAndInstall: vi.fn().mockResolvedValue(undefined)
18+
});
19+
20+
it('automatically downloads and installs a silent startup update', async () => {
21+
const u = update();
22+
check.mockResolvedValue(u);
23+
const state = new UpdaterState();
24+
25+
await state.check(true, true);
26+
27+
expect(u.downloadAndInstall).toHaveBeenCalledOnce();
28+
expect(state.phase).toBe('ready');
29+
expect(state.version).toBe('0.3.2');
30+
});
31+
32+
it('keeps manual checks download-free until the user starts the download', async () => {
33+
const u = update();
34+
check.mockResolvedValue(u);
35+
const state = new UpdaterState();
36+
37+
await state.check();
38+
39+
expect(u.downloadAndInstall).not.toHaveBeenCalled();
40+
expect(state.phase).toBe('available');
41+
});
42+
43+
it('does not start a second check while an update is downloading', async () => {
44+
const u = update();
45+
let resolveDownload!: () => void;
46+
u.downloadAndInstall.mockReturnValue(new Promise<void>((resolve) => (resolveDownload = resolve)));
47+
check.mockResolvedValue(u);
48+
const state = new UpdaterState();
49+
50+
const first = state.check(true, true);
51+
await vi.waitFor(() => expect(state.phase).toBe('downloading'));
52+
await state.check(true, true);
53+
expect(check).toHaveBeenCalledOnce();
54+
resolveDownload();
55+
await first;
56+
});
57+
58+
it('surfaces an automatic install failure without pretending it is ready', async () => {
59+
const u = update();
60+
u.downloadAndInstall.mockRejectedValue(new Error('signature mismatch'));
61+
check.mockResolvedValue(u);
62+
const state = new UpdaterState();
63+
64+
await state.check(true, true);
65+
66+
expect(state.phase).toBe('error');
67+
expect(state.error).toContain('signature mismatch');
68+
});
69+
});

src/routes/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -811,8 +811,8 @@
811811
for (const u of urls) handleDeepLink(u);
812812
});
813813
cleanups.push(undeep);
814-
// 启动约 5 秒后静默检查一次更新;有新版本时设置入口显示小圆点。
815-
const updateTimer = setTimeout(() => updater.check(true), 5000);
814+
// Check GitHub after startup and install updates; relaunch stays user-controlled.
815+
const updateTimer = setTimeout(() => updater.check(true, true), 5000);
816816
cleanups.push(() => clearTimeout(updateTimer));
817817
loadProviders();
818818
readAuthProviders()

0 commit comments

Comments
 (0)