From d87f1752126ecc0ebac006520b70334abe4c1cab Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Mon, 8 Jun 2026 06:27:13 +0530 Subject: [PATCH] feat(renderer): add updater store mirroring main process state --- src/renderer/lib/updater-store.ts | 42 ++++++++++++++++++++++ tests/unit/lib/updater-store.test.ts | 52 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/renderer/lib/updater-store.ts create mode 100644 tests/unit/lib/updater-store.test.ts diff --git a/src/renderer/lib/updater-store.ts b/src/renderer/lib/updater-store.ts new file mode 100644 index 0000000..4b3e60b --- /dev/null +++ b/src/renderer/lib/updater-store.ts @@ -0,0 +1,42 @@ +import { create } from 'zustand'; +import { ipc } from '@/lib/ipc'; + +type State = 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'error'; + +interface UpdaterState { + state: State; + version: string | null; + percent: number; + lastCheckAt: number; + applyStatus: (s: { state: State; version?: string; percent?: number; code?: string }) => void; + check: () => Promise; + install: () => void; +} + +const DEBOUNCE_MS = 60_000; + +export const useUpdaterStore = create((set, get) => ({ + state: 'idle', + version: null, + percent: 0, + lastCheckAt: 0, + applyStatus: (s) => + set({ + state: s.state, + version: s.version ?? get().version, + percent: s.percent ?? get().percent, + }), + check: async () => { + if (Date.now() - get().lastCheckAt < DEBOUNCE_MS) return; + set({ lastCheckAt: Date.now() }); + await ipc.updater.check(); + }, + install: () => ipc.updater.install(), +})); + +// Subscribe to live updates from main +if (typeof window !== 'undefined' && (window as any).electronAPI?.updater?.onStatus) { + (window as any).electronAPI.updater.onStatus((payload: any) => { + useUpdaterStore.getState().applyStatus(payload); + }); +} diff --git a/tests/unit/lib/updater-store.test.ts b/tests/unit/lib/updater-store.test.ts new file mode 100644 index 0000000..87d14f4 --- /dev/null +++ b/tests/unit/lib/updater-store.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test, beforeEach, vi } from 'vitest'; + +vi.mock('@/lib/ipc', () => ({ + ipc: { + updater: { + check: vi.fn(), + install: vi.fn(), + getState: vi.fn().mockResolvedValue({ state: 'idle' }), + }, + }, +})); + +import { useUpdaterStore } from '@/lib/updater-store'; +import { ipc } from '@/lib/ipc'; + +beforeEach(() => { + useUpdaterStore.setState({ + state: 'idle', + version: null, + percent: 0, + lastCheckAt: 0, + }); + vi.clearAllMocks(); +}); + +describe('useUpdaterStore', () => { + test('initial state is idle', () => { + expect(useUpdaterStore.getState().state).toBe('idle'); + }); + + test('applyStatus updates state from incoming event', () => { + useUpdaterStore.getState().applyStatus({ state: 'available', version: '5.0.2' }); + expect(useUpdaterStore.getState().state).toBe('available'); + expect(useUpdaterStore.getState().version).toBe('5.0.2'); + }); + + test('check() debounces within 60s', async () => { + useUpdaterStore.setState({ lastCheckAt: Date.now() }); + await useUpdaterStore.getState().check(); + expect(ipc.updater.check).not.toHaveBeenCalled(); + }); + + test('check() invokes ipc.updater.check when not debounced', async () => { + await useUpdaterStore.getState().check(); + expect(ipc.updater.check).toHaveBeenCalled(); + }); + + test('install() invokes ipc.updater.install', () => { + useUpdaterStore.getState().install(); + expect(ipc.updater.install).toHaveBeenCalled(); + }); +});