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 },
{ 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: () => () => {},

View File

@@ -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<void>;
}
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<void>;
};
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,