Skip to content

Commit c39021d

Browse files
committed
fix(desktop): clear a cancelled update replacement so refreshes resume
1 parent 670c84c commit c39021d

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

apps/desktop/src/main/updater.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,38 @@ describe('initUpdater state machine', () => {
640640
expect(autoUpdaterMock.downloadUpdate).not.toHaveBeenCalled()
641641
})
642642

643+
it('resumes refreshing after a replacement download is cancelled without an error event', async () => {
644+
const { handle } = await createUpdater()
645+
await stageUpdate(handle, '2.0.0')
646+
await vi.advanceTimersByTimeAsync(10_000)
647+
autoUpdaterMock.downloadUpdate.mockImplementationOnce(() =>
648+
Promise.reject(new Error('cancelled'))
649+
)
650+
emit('update-available', { version: '2.1.0' })
651+
await vi.advanceTimersByTimeAsync(0)
652+
653+
await vi.advanceTimersByTimeAsync(30 * 60 * 1000 - 10_000)
654+
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
655+
emit('update-available', { version: '2.1.0' })
656+
expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalledTimes(3)
657+
expect(handle.getState()).toEqual({ status: 'ready', version: '2.0.0' })
658+
})
659+
660+
it('follows the feed when a re-check rolls an offered release back', async () => {
661+
const { handle } = await createUpdater({ autoDownload: false })
662+
handle.check()
663+
await vi.advanceTimersByTimeAsync(0)
664+
emit('update-available', { version: '2.2.0' })
665+
666+
await vi.advanceTimersByTimeAsync(10_000)
667+
emit('update-available', { version: '2.1.0' })
668+
expect(handle.getState()).toEqual({ status: 'available', version: '2.1.0' })
669+
670+
handle.check()
671+
emit('update-downloaded', { version: '2.1.0' })
672+
expect(handle.getState()).toEqual({ status: 'ready', version: '2.1.0' })
673+
})
674+
643675
it('withdraws an offered update once a re-check stores a blocked candidate', async () => {
644676
const { handle } = await createUpdater({ autoDownload: false })
645677
handle.check()

apps/desktop/src/main/updater.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,13 +554,21 @@ export function initUpdater(deps: UpdaterDeps): UpdaterHandle {
554554
}
555555
acceptedUpdateVersion = info.version
556556
deps.events.record('update_check', { available: info.version, replacing: stagedVersion })
557+
const replacementVersion = info.version
558+
// Cancellation rejects without an `error` event, so the promise owns
559+
// clearing its replacement for every failure mode.
557560
void autoUpdater.downloadUpdate().catch((error) => {
561+
if (acceptedUpdateVersion === replacementVersion) acceptedUpdateVersion = null
562+
if (pendingReplacementVersion === replacementVersion) pendingReplacementVersion = null
558563
logger.warn('Replacement update download failed; keeping the staged update', {
559564
message: getErrorMessage(error, 'unknown'),
560565
})
561566
})
562567
return
563568
}
569+
// An offer mirrors the feed's latest release, even after a rollback: the
570+
// library only keeps this check's update info, so Update would download
571+
// this version regardless of which one the offer displayed.
564572
if (state.status === 'available' && validCandidate && state.version === info.version) {
565573
return
566574
}

0 commit comments

Comments
 (0)