fix: tighten type safety in boot services

- 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
This commit is contained in:
2026-03-28 11:10:51 -07:00
parent 86b50dcb70
commit 1a448cf7d9
2 changed files with 22 additions and 10 deletions

View File

@@ -24,7 +24,7 @@ test('createMainBootServices builds boot-phase service bundle', () => {
{ scope: string; warn: () => void; info: () => void; error: () => void }, { scope: string; warn: () => void; info: () => void; error: () => void },
{ registry: boolean }, { registry: boolean },
{ getModalWindow: () => null }, { getModalWindow: () => null },
{ inputState: boolean }, { inputState: boolean; getModalInputExclusive: () => boolean; handleModalInputStateChange: (isActive: boolean) => void },
{ measurementStore: boolean }, { measurementStore: boolean },
{ modalRuntime: boolean }, { modalRuntime: boolean },
{ mpvSocketPath: string; texthookerPort: number }, { mpvSocketPath: string; texthookerPort: number },
@@ -50,7 +50,7 @@ test('createMainBootServices builds boot-phase service bundle', () => {
setPathValue = value; setPathValue = value;
}, },
quit: () => {}, quit: () => {},
on: (event) => { on: (event: string) => {
appOnCalls.push(event); appOnCalls.push(event);
return {}; return {};
}, },
@@ -80,7 +80,7 @@ test('createMainBootServices builds boot-phase service bundle', () => {
createOverlayManager: () => ({ createOverlayManager: () => ({
getModalWindow: () => null, getModalWindow: () => null,
}), }),
createOverlayModalInputState: () => ({ inputState: true }), createOverlayModalInputState: () => ({ inputState: true, getModalInputExclusive: () => false, handleModalInputStateChange: () => {} }),
createOverlayContentMeasurementStore: () => ({ measurementStore: true }), createOverlayContentMeasurementStore: () => ({ measurementStore: true }),
getSyncOverlayShortcutsForModal: () => () => {}, getSyncOverlayShortcutsForModal: () => () => {},
getSyncOverlayVisibilityForModal: () => () => {}, getSyncOverlayVisibilityForModal: () => () => {},

View File

@@ -1,6 +1,18 @@
import { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import { ConfigStartupParseError } from '../../config'; import { ConfigStartupParseError } from '../../config';
export interface AppLifecycleShape {
requestSingleInstanceLock: () => boolean;
quit: () => void;
on: (event: string, listener: (...args: unknown[]) => void) => unknown;
whenReady: () => Promise<void>;
}
export interface OverlayModalInputStateShape {
getModalInputExclusive: () => boolean;
handleModalInputStateChange: (isActive: boolean) => void;
}
export interface MainBootServicesParams< export interface MainBootServicesParams<
TConfigService, TConfigService,
TAnilistTokenStore, TAnilistTokenStore,
@@ -38,7 +50,8 @@ export interface MainBootServicesParams<
app: { app: {
setPath: (name: string, value: string) => void; setPath: (name: string, value: string) => void;
quit: () => 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<void>; whenReady: () => Promise<void>;
}; };
shouldBypassSingleInstanceLock: () => boolean; shouldBypassSingleInstanceLock: () => boolean;
@@ -124,11 +137,11 @@ export function createMainBootServices<
TLogger, TLogger,
TRuntimeRegistry, TRuntimeRegistry,
TOverlayManager extends { getModalWindow: () => BrowserWindow | null }, TOverlayManager extends { getModalWindow: () => BrowserWindow | null },
TOverlayModalInputState, TOverlayModalInputState extends OverlayModalInputStateShape,
TOverlayContentMeasurementStore, TOverlayContentMeasurementStore,
TOverlayModalRuntime, TOverlayModalRuntime,
TAppState, TAppState,
TAppLifecycleApp, TAppLifecycleApp extends AppLifecycleShape,
>( >(
params: MainBootServicesParams< params: MainBootServicesParams<
TConfigService, TConfigService,
@@ -212,8 +225,7 @@ export function createMainBootServices<
overlayManager, overlayManager,
overlayModalInputState, overlayModalInputState,
onModalStateChange: (isActive: boolean) => onModalStateChange: (isActive: boolean) =>
(overlayModalInputState as { handleModalInputStateChange?: (isActive: boolean) => void }) overlayModalInputState.handleModalInputStateChange(isActive),
.handleModalInputStateChange?.(isActive),
}); });
const appState = params.createAppState({ const appState = params.createAppState({
mpvSocketPath: params.getDefaultSocketPath(), mpvSocketPath: params.getDefaultSocketPath(),
@@ -242,7 +254,7 @@ export function createMainBootServices<
return appLifecycleApp; return appLifecycleApp;
}, },
whenReady: () => params.app.whenReady(), whenReady: () => params.app.whenReady(),
} as TAppLifecycleApp; } satisfies AppLifecycleShape as TAppLifecycleApp;
return { return {
configDir, configDir,