Files
deepseek-harness/apps/desktop/src/update-coordinator.ts
T

96 lines
3.6 KiB
TypeScript

/** One Electron release stream for the version-bound shell and dsh seed. */
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { app } from 'electron'
import electronUpdater, { type AppUpdater } from 'electron-updater'
import type { DesktopUpdateState } from './ipc.ts'
const { autoUpdater } = electronUpdater
/** Checks, downloads, and installs one complete Desktop release. */
export class DesktopUpdateCoordinator {
private availableVersion: string | undefined
private checkOperation: Promise<DesktopUpdateState> | undefined
private installOperation: Promise<DesktopUpdateState> | undefined
/**
* @param publish - state sink for every desktop window.
* @param beforeRestart - stop application-owned processes before replacement.
* @param updater - Electron artifact updater; replaceable for tests.
* @param enabled - whether this packaged process carries updater configuration.
*/
constructor(
private readonly publish: (state: DesktopUpdateState) => DesktopUpdateState,
private readonly beforeRestart: () => Promise<void> = async () => {},
private readonly updater: AppUpdater = autoUpdater,
private readonly enabled: () => boolean = () => (
app.isPackaged && existsSync(join(process.resourcesPath, 'app-update.yml'))
),
) {
this.updater.autoDownload = false
this.updater.autoInstallOnAppQuit = false
}
/** Check the configured Desktop release stream and retain an available version. */
async check(): Promise<DesktopUpdateState> {
if (this.installOperation !== undefined) return this.installOperation
if (this.checkOperation !== undefined) return this.checkOperation
this.checkOperation = this.doCheck().finally(() => { this.checkOperation = undefined })
return this.checkOperation
}
/** Wait for an in-flight check, then download and install its retained release. */
async install(): Promise<DesktopUpdateState> {
if (this.installOperation !== undefined) return this.installOperation
this.installOperation = (async () => {
await this.checkOperation
return this.doInstall()
})().finally(() => { this.installOperation = undefined })
return this.installOperation
}
private async doCheck(): Promise<DesktopUpdateState> {
this.publish({ phase: 'checking' })
try {
if (!this.enabled()) {
this.availableVersion = undefined
return this.publish({ phase: 'idle' })
}
const result = await this.updater.checkForUpdates()
const version = result?.isUpdateAvailable === true ? result.updateInfo.version : undefined
this.availableVersion = version
return version === undefined
? this.publish({ phase: 'idle' })
: this.publish({ phase: 'available', version })
} catch (error) {
this.availableVersion = undefined
return this.publish({
phase: 'error',
message: error instanceof Error ? error.message : String(error),
})
}
}
private async doInstall(): Promise<DesktopUpdateState> {
const version = this.availableVersion
if (version === undefined) {
throw new Error('desktop update: no verified update is available')
}
this.publish({ phase: 'installing', version })
try {
await this.updater.downloadUpdate()
this.availableVersion = undefined
const ready = this.publish({ phase: 'ready', version })
await this.beforeRestart()
this.updater.quitAndInstall(false, true)
return ready
} catch (error) {
return this.publish({
phase: 'error',
version,
message: error instanceof Error ? error.message : String(error),
})
}
}
}