From 1a448cf7d93a7fcea5b0bdb173b368e4100c007c Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 28 Mar 2026 11:10:51 -0700 Subject: [PATCH] fix: tighten type safety in boot services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AppLifecycleShape and OverlayModalInputStateShape constraints so TAppLifecycleApp and TOverlayModalInputState generics are bounded - Remove unsafe `as { handleModalInputStateChange? }` cast — now directly callable via the constraint - Use `satisfies AppLifecycleShape` for structural validation on the appLifecycleApp object literal - Document Electron App.on incompatibility with simple signatures --- src/main/boot/services.test.ts | 6 +++--- src/main/boot/services.ts | 26 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/main/boot/services.test.ts b/src/main/boot/services.test.ts index 6930f9d..c09e69b 100644 --- a/src/main/boot/services.test.ts +++ b/src/main/boot/services.test.ts @@ -24,7 +24,7 @@ test('createMainBootServices builds boot-phase service bundle', () => { { scope: string; warn: () => void; info: () => void; error: () => void }, { registry: boolean }, { getModalWindow: () => null }, - { inputState: boolean }, + { inputState: boolean; getModalInputExclusive: () => boolean; handleModalInputStateChange: (isActive: boolean) => void }, { measurementStore: boolean }, { modalRuntime: boolean }, { mpvSocketPath: string; texthookerPort: number }, @@ -50,7 +50,7 @@ test('createMainBootServices builds boot-phase service bundle', () => { setPathValue = value; }, quit: () => {}, - on: (event) => { + on: (event: string) => { appOnCalls.push(event); return {}; }, @@ -80,7 +80,7 @@ test('createMainBootServices builds boot-phase service bundle', () => { createOverlayManager: () => ({ getModalWindow: () => null, }), - createOverlayModalInputState: () => ({ inputState: true }), + createOverlayModalInputState: () => ({ inputState: true, getModalInputExclusive: () => false, handleModalInputStateChange: () => {} }), createOverlayContentMeasurementStore: () => ({ measurementStore: true }), getSyncOverlayShortcutsForModal: () => () => {}, getSyncOverlayVisibilityForModal: () => () => {}, diff --git a/src/main/boot/services.ts b/src/main/boot/services.ts index 5733c95..862c1fa 100644 --- a/src/main/boot/services.ts +++ b/src/main/boot/services.ts @@ -1,6 +1,18 @@ -import { BrowserWindow } from 'electron'; +import type { BrowserWindow } from 'electron'; import { ConfigStartupParseError } from '../../config'; +export interface AppLifecycleShape { + requestSingleInstanceLock: () => boolean; + quit: () => void; + on: (event: string, listener: (...args: unknown[]) => void) => unknown; + whenReady: () => Promise; +} + +export interface OverlayModalInputStateShape { + getModalInputExclusive: () => boolean; + handleModalInputStateChange: (isActive: boolean) => void; +} + export interface MainBootServicesParams< TConfigService, TAnilistTokenStore, @@ -38,7 +50,8 @@ export interface MainBootServicesParams< app: { setPath: (name: string, value: string) => void; quit: () => void; - on: (event: any, listener: (...args: unknown[]) => void) => any; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- Electron App.on has 50+ overloaded signatures + on: Function; whenReady: () => Promise; }; shouldBypassSingleInstanceLock: () => boolean; @@ -124,11 +137,11 @@ export function createMainBootServices< TLogger, TRuntimeRegistry, TOverlayManager extends { getModalWindow: () => BrowserWindow | null }, - TOverlayModalInputState, + TOverlayModalInputState extends OverlayModalInputStateShape, TOverlayContentMeasurementStore, TOverlayModalRuntime, TAppState, - TAppLifecycleApp, + TAppLifecycleApp extends AppLifecycleShape, >( params: MainBootServicesParams< TConfigService, @@ -212,8 +225,7 @@ export function createMainBootServices< overlayManager, overlayModalInputState, onModalStateChange: (isActive: boolean) => - (overlayModalInputState as { handleModalInputStateChange?: (isActive: boolean) => void }) - .handleModalInputStateChange?.(isActive), + overlayModalInputState.handleModalInputStateChange(isActive), }); const appState = params.createAppState({ mpvSocketPath: params.getDefaultSocketPath(), @@ -242,7 +254,7 @@ export function createMainBootServices< return appLifecycleApp; }, whenReady: () => params.app.whenReady(), - } as TAppLifecycleApp; + } satisfies AppLifecycleShape as TAppLifecycleApp; return { configDir,