fix: updater progress jumping and click-twice issues
- Guard installUpdate() with isDownloading flag to prevent concurrent downloads - Deduplicate checkForUpdates() with checkInProgress promise - Track monotonic progress (peakPercent never decreases) - Reduce startup check delay from 5s to 1s - Bump desktop to 0.2.4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "heicode-desktop",
|
||||
"private": true,
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "heicode-desktop"
|
||||
version = "0.2.2"
|
||||
version = "0.2.4"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json",
|
||||
"productName": "HeiCode",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.4",
|
||||
"identifier": "com.heicode.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeDevCommand": "bun run build:sidecars && bun run dev",
|
||||
"beforeBuildCommand": "bun run build && bun run build:sidecars"
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
|
||||
@@ -36,6 +36,8 @@ type UpdateStore = {
|
||||
let pendingUpdate: Update | null = null
|
||||
let startupCheckPromise: Promise<void> | null = null
|
||||
let periodicCheckTimer: ReturnType<typeof setInterval> | null = null
|
||||
let isDownloading = false
|
||||
let checkInProgress: Promise<Update | null> | null = null
|
||||
|
||||
// How often the long-running client re-polls the updater manifest while
|
||||
// idle. 2 hours strikes a balance between "user sees a new release the
|
||||
@@ -98,7 +100,7 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
if (!isTauriRuntime()) return
|
||||
if (!startupCheckPromise) {
|
||||
startupCheckPromise = (async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
await get().checkForUpdates({ silent: true })
|
||||
})().finally(() => {
|
||||
startupCheckPromise = null
|
||||
@@ -123,73 +125,85 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
checkForUpdates: async ({ silent = false } = {}) => {
|
||||
if (!isTauriRuntime()) return null
|
||||
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'checking',
|
||||
error: null,
|
||||
}))
|
||||
if (checkInProgress) return checkInProgress
|
||||
|
||||
try {
|
||||
const { check } = await import('@tauri-apps/plugin-updater')
|
||||
const update = await check()
|
||||
await setPendingUpdate(update)
|
||||
const doCheck = async (): Promise<Update | null> => {
|
||||
if (isDownloading) return pendingUpdate
|
||||
|
||||
const checkedAt = Date.now()
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'checking',
|
||||
error: null,
|
||||
}))
|
||||
|
||||
try {
|
||||
const { check } = await import('@tauri-apps/plugin-updater')
|
||||
const update = await check()
|
||||
await setPendingUpdate(update)
|
||||
|
||||
const checkedAt = Date.now()
|
||||
|
||||
if (!update) {
|
||||
writeDismissedUpdateVersion(null)
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'up-to-date',
|
||||
availableVersion: null,
|
||||
releaseNotes: null,
|
||||
progressPercent: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: null,
|
||||
checkedAt,
|
||||
error: null,
|
||||
shouldPrompt: false,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
|
||||
const dismissedVersion = readDismissedUpdateVersion()
|
||||
const shouldPrompt = dismissedVersion !== update.version
|
||||
|
||||
if (!update) {
|
||||
writeDismissedUpdateVersion(null)
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'up-to-date',
|
||||
availableVersion: null,
|
||||
releaseNotes: null,
|
||||
status: 'available',
|
||||
availableVersion: update.version,
|
||||
releaseNotes: update.body ?? null,
|
||||
progressPercent: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: null,
|
||||
checkedAt,
|
||||
error: null,
|
||||
shouldPrompt: false,
|
||||
shouldPrompt,
|
||||
}))
|
||||
return update
|
||||
} catch (error) {
|
||||
if (!silent) {
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'error',
|
||||
error: getErrorMessage(error),
|
||||
checkedAt: Date.now(),
|
||||
}))
|
||||
} else {
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: state.availableVersion ? 'available' : 'idle',
|
||||
checkedAt: Date.now(),
|
||||
}))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const dismissedVersion = readDismissedUpdateVersion()
|
||||
const shouldPrompt = dismissedVersion !== update.version
|
||||
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'available',
|
||||
availableVersion: update.version,
|
||||
releaseNotes: update.body ?? null,
|
||||
progressPercent: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: null,
|
||||
checkedAt,
|
||||
error: null,
|
||||
shouldPrompt,
|
||||
}))
|
||||
return update
|
||||
} catch (error) {
|
||||
if (!silent) {
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'error',
|
||||
error: getErrorMessage(error),
|
||||
checkedAt: Date.now(),
|
||||
}))
|
||||
} else {
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: state.availableVersion ? 'available' : 'idle',
|
||||
checkedAt: Date.now(),
|
||||
}))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
checkInProgress = doCheck().finally(() => {
|
||||
checkInProgress = null
|
||||
})
|
||||
return checkInProgress
|
||||
},
|
||||
|
||||
installUpdate: async () => {
|
||||
if (!isTauriRuntime()) return
|
||||
if (isDownloading) return
|
||||
|
||||
let update = pendingUpdate
|
||||
if (!update) {
|
||||
@@ -197,6 +211,8 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
if (!update) return
|
||||
}
|
||||
|
||||
isDownloading = true
|
||||
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'downloading',
|
||||
@@ -213,11 +229,13 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
const { relaunch } = await import('@tauri-apps/plugin-process')
|
||||
let totalBytes: number | null = null
|
||||
let downloadedBytes = 0
|
||||
let peakPercent = 0
|
||||
|
||||
await update.download((event) => {
|
||||
if (event.event === 'Started') {
|
||||
totalBytes = event.data.contentLength ?? null
|
||||
downloadedBytes = 0
|
||||
peakPercent = 0
|
||||
set((state) => ({
|
||||
...state,
|
||||
totalBytes,
|
||||
@@ -226,16 +244,17 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
}))
|
||||
} else if (event.event === 'Progress') {
|
||||
downloadedBytes += event.data.chunkLength
|
||||
const progressPercent =
|
||||
const rawPercent =
|
||||
totalBytes && totalBytes > 0
|
||||
? Math.min(Math.round((downloadedBytes / totalBytes) * 100), 100)
|
||||
: 0
|
||||
peakPercent = Math.max(peakPercent, rawPercent)
|
||||
|
||||
set((state) => ({
|
||||
...state,
|
||||
downloadedBytes,
|
||||
totalBytes,
|
||||
progressPercent,
|
||||
progressPercent: peakPercent,
|
||||
}))
|
||||
} else if (event.event === 'Finished') {
|
||||
set((state) => ({
|
||||
@@ -256,6 +275,7 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
|
||||
|
||||
await relaunch()
|
||||
} catch (error) {
|
||||
isDownloading = false
|
||||
set((state) => ({
|
||||
...state,
|
||||
status: 'available',
|
||||
|
||||
Reference in New Issue
Block a user