1414// publish block in electron-builder.yml.
1515
1616import { app , ipcMain , type BrowserWindow } from 'electron'
17- import { readFileSync , writeFileSync } from 'node:fs'
17+ import { readFileSync , statSync , writeFileSync } from 'node:fs'
1818import { join } from 'node:path'
1919import { autoUpdater , type UpdateInfo } from 'electron-updater'
2020import { isMicrosoftStore } from './distribution'
@@ -62,6 +62,10 @@ function send(channel: string, payload: unknown): void {
6262 * `beta.yml`, `alpha.yml`) and the updater fetches exactly the one named. */
6363function applyChannel ( channel : Channel ) : void {
6464 autoUpdater . channel = channel === 'stable' ? 'latest' : channel
65+ // electron-updater's channel setter quietly turns downgrades on. Left that
66+ // way, any feed older than this build is offered as an "update": 1.2.0
67+ // offered 1.1.4 while its own feed was not yet published. Updates only go up.
68+ autoUpdater . allowDowngrade = false
6569}
6670
6771/** True while a pre-release check is in flight that a stable check will follow.
@@ -97,6 +101,58 @@ async function check(): Promise<{ ok: boolean; reason?: string }> {
97101 }
98102}
99103
104+ /** The update on offer, and the size of the app files its web installer
105+ * fetches after itself (latest.yml's `packages`). */
106+ let offered : { version : string ; packageSize : number } | null = null
107+ let packageTimer : ReturnType < typeof setInterval > | null = null
108+ let packageStarted = false
109+
110+ function packageSize ( info : UpdateInfo ) : number {
111+ const packages = ( info as UpdateInfo & { packages ?: Record < string , { size ?: number } > } ) . packages
112+ const first = packages ? Object . values ( packages ) [ 0 ] : undefined
113+ return typeof first ?. size === 'number' ? first . size : 0
114+ }
115+
116+ /** Report the app files download as it happens.
117+ *
118+ * A web-installer update is two downloads. electron-updater reports progress
119+ * for the first, the ~1 MB setup, then writes the multi-gigabyte app package
120+ * straight to its pending folder without reporting anything, which left the
121+ * bar at "100% · 1 MB of 1 MB" for as long as that took and read as frozen.
122+ * The file is written in place, so its size on disk is the real figure. */
123+ function watchPackageDownload ( ) : void {
124+ stopPackageWatch ( )
125+ packageStarted = false
126+ const target = offered
127+ if ( ! target ) return
128+ packageTimer = setInterval ( ( ) => {
129+ const helper = ( autoUpdater as unknown as {
130+ downloadedUpdateHelper ?: { cacheDirForPendingUpdate : string }
131+ } ) . downloadedUpdateHelper
132+ if ( ! helper ) return
133+ let transferred = 0
134+ try {
135+ transferred = statSync ( join ( helper . cacheDirForPendingUpdate , `package-${ target . version } .7z` ) ) . size
136+ } catch {
137+ return // the app files download has not started yet
138+ }
139+ packageStarted = true
140+ const total = target . packageSize
141+ send ( 'update:state' , {
142+ state : 'downloading' ,
143+ phase : 'package' ,
144+ transferred,
145+ total : total || undefined ,
146+ percent : total ? Math . min ( 99 , Math . round ( ( transferred / total ) * 100 ) ) : undefined
147+ } )
148+ } , 1000 )
149+ }
150+
151+ function stopPackageWatch ( ) : void {
152+ if ( packageTimer ) clearInterval ( packageTimer )
153+ packageTimer = null
154+ }
155+
100156export function setupUpdater ( win : BrowserWindow ) : void {
101157 mainWindow = win
102158
@@ -136,6 +192,7 @@ export function setupUpdater(win: BrowserWindow): void {
136192 send ( 'update:state' , { state : 'none' } )
137193 return
138194 }
195+ offered = { version : info . version , packageSize : packageSize ( info ) }
139196 send ( 'update:state' , {
140197 state : 'available' ,
141198 version : info . version ,
@@ -146,21 +203,27 @@ export function setupUpdater(win: BrowserWindow): void {
146203
147204 autoUpdater . on ( 'update-not-available' , ( ) => send ( 'update:state' , { state : 'none' } ) )
148205
149- autoUpdater . on ( 'download-progress' , ( p ) =>
206+ autoUpdater . on ( 'download-progress' , ( p ) => {
207+ // Once the app files are downloading, the watcher reports them; this
208+ // event would otherwise overwrite that with the finished setup's 100%.
209+ if ( packageStarted ) return
150210 send ( 'update:state' , {
151211 state : 'downloading' ,
212+ phase : 'installer' ,
152213 percent : Math . round ( p . percent ) ,
153214 transferred : p . transferred ,
154215 total : p . total ,
155216 bytesPerSecond : p . bytesPerSecond
156217 } )
157- )
218+ } )
158219
159- autoUpdater . on ( 'update-downloaded' , ( info : UpdateInfo ) =>
220+ autoUpdater . on ( 'update-downloaded' , ( info : UpdateInfo ) => {
221+ stopPackageWatch ( )
160222 send ( 'update:state' , { state : 'ready' , version : info . version } )
161- )
223+ } )
162224
163225 autoUpdater . on ( 'error' , ( err ) => {
226+ stopPackageWatch ( )
164227 // An empty pre-release feed is about to be retried against stable, and
165228 // the retry reports the real outcome.
166229 if ( willRetryOnStable ) return
@@ -179,12 +242,15 @@ export function setupUpdater(win: BrowserWindow): void {
179242 } )
180243
181244 ipcMain . handle ( 'update:download' , async ( ) => {
245+ watchPackageDownload ( )
182246 try {
183247 await autoUpdater . downloadUpdate ( )
184248 return { ok : true }
185249 } catch ( e ) {
186250 send ( 'update:state' , { state : 'error' , message : String ( e ) } )
187251 return { ok : false }
252+ } finally {
253+ stopPackageWatch ( )
188254 }
189255 } )
190256
0 commit comments