refactor(02-02): main/index.ts to use ServiceContextRegistry

- Replace global service variables with ServiceContextRegistry
- Create local context at startup with LocalFileSystemProvider
- Register local context in registry and start it
- Wire file watcher events via wireFileWatcherEvents helper
- Export onContextSwitched callback for SSH handler
- Remove handleModeSwitch callback (replaced by registry pattern)
- Update initializeIpcHandlers to accept registry instead of individual services
- Remove reinitializeServiceHandlers entirely from handlers.ts
- ServiceContextRegistry pattern eliminates need for service recreation on mode switch
This commit is contained in:
matt 2026-02-12 01:04:20 +00:00
parent 1b4f180e81
commit 5bf41c6ed8
2 changed files with 117 additions and 167 deletions

View file

@ -4,30 +4,23 @@
* Responsibilities: * Responsibilities:
* - Initialize Electron app and main window * - Initialize Electron app and main window
* - Set up IPC handlers for data access * - Set up IPC handlers for data access
* - Initialize services (ProjectScanner, SessionParser, etc.) * - Initialize ServiceContextRegistry with local context
* - Start file watcher for live updates * - Start file watcher for live updates
* - Manage application lifecycle * - Manage application lifecycle
*/ */
import { import {
CACHE_CLEANUP_INTERVAL_MINUTES,
CACHE_TTL_MINUTES,
DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_HEIGHT,
DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_WIDTH,
DEV_SERVER_PORT, DEV_SERVER_PORT,
getTrafficLightPositionForZoom, getTrafficLightPositionForZoom,
MAX_CACHE_SESSIONS,
WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL, WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL,
} from '@shared/constants'; } from '@shared/constants';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { app, BrowserWindow } from 'electron'; import { app, BrowserWindow } from 'electron';
import { join } from 'path'; import { join } from 'path';
import { import { initializeIpcHandlers, removeIpcHandlers } from './ipc/handlers';
initializeIpcHandlers,
reinitializeServiceHandlers,
removeIpcHandlers,
} from './ipc/handlers';
// Icon path - works for both dev and production // Icon path - works for both dev and production
const getIconPath = (): string => { const getIconPath = (): string => {
@ -42,15 +35,12 @@ const logger = createLogger('App');
import { SSH_STATUS } from '@preload/constants/ipcChannels'; import { SSH_STATUS } from '@preload/constants/ipcChannels';
import { import {
ChunkBuilder,
configManager, configManager,
DataCache, LocalFileSystemProvider,
FileWatcher,
NotificationManager, NotificationManager,
ProjectScanner, ServiceContext,
SessionParser, ServiceContextRegistry,
SshConnectionManager, SshConnectionManager,
SubagentResolver,
UpdaterService, UpdaterService,
} from './services'; } from './services';
@ -60,17 +50,71 @@ import {
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
// Service instances // Service registry and global services
let projectScanner: ProjectScanner; let contextRegistry: ServiceContextRegistry;
let sessionParser: SessionParser;
let subagentResolver: SubagentResolver;
let chunkBuilder: ChunkBuilder;
let dataCache: DataCache;
let fileWatcher: FileWatcher;
let notificationManager: NotificationManager; let notificationManager: NotificationManager;
let updaterService: UpdaterService; let updaterService: UpdaterService;
let sshConnectionManager: SshConnectionManager; let sshConnectionManager: SshConnectionManager;
let cleanupInterval: NodeJS.Timeout | null = null;
// File watcher event cleanup functions
let fileChangeCleanup: (() => void) | null = null;
let todoChangeCleanup: (() => void) | null = null;
/**
* Wires file watcher events from a ServiceContext to the renderer.
* Cleans up previous listeners before adding new ones.
*/
function wireFileWatcherEvents(context: ServiceContext): void {
logger.info(`Wiring FileWatcher events for context: ${context.id}`);
// Clean up previous listeners
if (fileChangeCleanup) {
fileChangeCleanup();
fileChangeCleanup = null;
}
if (todoChangeCleanup) {
todoChangeCleanup();
todoChangeCleanup = null;
}
// Wire file-change events
const fileChangeHandler = (event: unknown) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('file-change', event);
}
};
context.fileWatcher.on('file-change', fileChangeHandler);
fileChangeCleanup = () => context.fileWatcher.off('file-change', fileChangeHandler);
// Wire todo-change events
const todoChangeHandler = (event: unknown) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('todo-change', event);
}
};
context.fileWatcher.on('todo-change', todoChangeHandler);
todoChangeCleanup = () => context.fileWatcher.off('todo-change', todoChangeHandler);
logger.info(`FileWatcher events wired for context: ${context.id}`);
}
/**
* Callback invoked when context switches (called by SSH IPC handler).
* Re-wires file watcher events and notifies renderer.
*/
export function onContextSwitched(context: ServiceContext): void {
// Re-wire file watcher events to new context
wireFileWatcherEvents(context);
// Notify renderer of context change
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(SSH_STATUS, sshConnectionManager.getStatus());
mainWindow.webContents.send('context-changed', {
contextId: context.id,
type: context.type,
});
}
}
/** /**
* Initializes all services. * Initializes all services.
@ -81,71 +125,36 @@ function initializeServices(): void {
// Initialize SSH connection manager // Initialize SSH connection manager
sshConnectionManager = new SshConnectionManager(); sshConnectionManager = new SshConnectionManager();
// Initialize services (paths are set automatically from environment) // Create ServiceContextRegistry
projectScanner = new ProjectScanner(); contextRegistry = new ServiceContextRegistry();
sessionParser = new SessionParser(projectScanner);
subagentResolver = new SubagentResolver(projectScanner); // Create local context
chunkBuilder = new ChunkBuilder(); const localContext = new ServiceContext({
const disableCache = process.env.CLAUDE_CONTEXT_DISABLE_CACHE === '1'; id: 'local',
dataCache = new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache); type: 'local',
fsProvider: new LocalFileSystemProvider(),
});
// Register and start local context
contextRegistry.registerContext(localContext);
localContext.start();
logger.info(`Projects directory: ${localContext.projectScanner.getProjectsDir()}`);
// Initialize notification manager (singleton, not context-scoped)
notificationManager = NotificationManager.getInstance();
// Set notification manager on local context's file watcher
localContext.fileWatcher.setNotificationManager(notificationManager);
// Wire file watcher events for local context
wireFileWatcherEvents(localContext);
// Initialize updater service
updaterService = new UpdaterService(); updaterService = new UpdaterService();
logger.info(`Projects directory: ${projectScanner.getProjectsDir()}`); // Initialize IPC handlers with registry
initializeIpcHandlers(contextRegistry, updaterService, sshConnectionManager);
// Mode switch callback: recreates services with new provider when switching local↔SSH
const handleModeSwitch = async (mode: 'local' | 'ssh'): Promise<void> => {
logger.info(`Switching to ${mode} mode`);
// Stop file watcher
fileWatcher.stop();
// Clear data cache
dataCache.clear();
// Get provider and projects path from connection manager
const provider = sshConnectionManager.getProvider();
const projectsDir =
mode === 'ssh' ? (sshConnectionManager.getRemoteProjectsPath() ?? undefined) : undefined;
// Recreate services with new provider
projectScanner = new ProjectScanner(projectsDir, undefined, provider);
sessionParser = new SessionParser(projectScanner);
subagentResolver = new SubagentResolver(projectScanner);
// Re-initialize IPC handler service references so subsequent calls use new instances
reinitializeServiceHandlers(
projectScanner,
sessionParser,
subagentResolver,
chunkBuilder,
dataCache
);
// Update file watcher provider
fileWatcher.setFileSystemProvider(provider);
// Restart file watcher
fileWatcher.start();
// Notify renderer to re-fetch all data
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(SSH_STATUS, sshConnectionManager.getStatus());
}
logger.info(`Mode switch to ${mode} complete`);
};
// Initialize IPC handlers (including SSH)
initializeIpcHandlers(
projectScanner,
sessionParser,
subagentResolver,
chunkBuilder,
dataCache,
updaterService,
sshConnectionManager,
handleModeSwitch
);
// Forward SSH state changes to renderer // Forward SSH state changes to renderer
sshConnectionManager.on('state-change', (status: unknown) => { sshConnectionManager.on('state-change', (status: unknown) => {
@ -154,33 +163,6 @@ function initializeServices(): void {
} }
}); });
// Initialize notification manager using singleton pattern
// This ensures IPC handlers and FileWatcher use the same instance
// Note: mainWindow will be set later via setMainWindow() when window is created
notificationManager = NotificationManager.getInstance();
// Start file watcher with notification manager for error detection
fileWatcher = new FileWatcher(dataCache);
fileWatcher.setNotificationManager(notificationManager);
fileWatcher.start();
// Forward file change events to renderer
// Note: Error detection is handled internally by FileWatcher via NotificationManager
fileWatcher.on('file-change', (event) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('file-change', event);
}
});
fileWatcher.on('todo-change', (event) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('todo-change', event);
}
});
// Start automatic cache cleanup
cleanupInterval = dataCache.startAutoCleanup(CACHE_CLEANUP_INTERVAL_MINUTES);
logger.info('Services initialized successfully'); logger.info('Services initialized successfully');
} }
@ -190,15 +172,19 @@ function initializeServices(): void {
function shutdownServices(): void { function shutdownServices(): void {
logger.info('Shutting down services...'); logger.info('Shutting down services...');
// Stop file watcher // Clean up file watcher event listeners
if (fileWatcher) { if (fileChangeCleanup) {
fileWatcher.stop(); fileChangeCleanup();
fileChangeCleanup = null;
}
if (todoChangeCleanup) {
todoChangeCleanup();
todoChangeCleanup = null;
} }
// Stop cache cleanup // Dispose all contexts (including local)
if (cleanupInterval) { if (contextRegistry) {
clearInterval(cleanupInterval); contextRegistry.dispose();
cleanupInterval = null;
} }
// Dispose SSH connection manager // Dispose SSH connection manager

View file

@ -45,38 +45,23 @@ import {
import { registerUtilityHandlers, removeUtilityHandlers } from './utility'; import { registerUtilityHandlers, removeUtilityHandlers } from './utility';
import { registerValidationHandlers, removeValidationHandlers } from './validation'; import { registerValidationHandlers, removeValidationHandlers } from './validation';
import type { import type { ServiceContextRegistry, SshConnectionManager, UpdaterService } from '../services';
ChunkBuilder,
DataCache,
ProjectScanner,
SessionParser,
SshConnectionManager,
SubagentResolver,
UpdaterService,
} from '../services';
/** /**
* Initializes IPC handlers with service instances. * Initializes IPC handlers with service registry.
*/ */
export function initializeIpcHandlers( export function initializeIpcHandlers(
scanner: ProjectScanner, registry: ServiceContextRegistry,
parser: SessionParser,
resolver: SubagentResolver,
builder: ChunkBuilder,
cache: DataCache,
updater: UpdaterService, updater: UpdaterService,
sshManager?: SshConnectionManager, sshManager: SshConnectionManager
sshModeSwitchCallback?: (mode: 'local' | 'ssh') => Promise<void>
): void { ): void {
// Initialize domain handlers with their required services // Initialize domain handlers with registry
initializeProjectHandlers(scanner); initializeProjectHandlers(registry);
initializeSessionHandlers(scanner, parser, resolver, builder, cache); initializeSessionHandlers(registry);
initializeSearchHandlers(scanner); initializeSearchHandlers(registry);
initializeSubagentHandlers(builder, cache, parser, resolver, scanner); initializeSubagentHandlers(registry);
initializeUpdaterHandlers(updater); initializeUpdaterHandlers(updater);
if (sshManager && sshModeSwitchCallback) { initializeSshHandlers(sshManager, registry);
initializeSshHandlers(sshManager, sshModeSwitchCallback);
}
// Register all handlers // Register all handlers
registerProjectHandlers(ipcMain); registerProjectHandlers(ipcMain);
@ -88,32 +73,11 @@ export function initializeIpcHandlers(
registerNotificationHandlers(ipcMain); registerNotificationHandlers(ipcMain);
registerConfigHandlers(ipcMain); registerConfigHandlers(ipcMain);
registerUpdaterHandlers(ipcMain); registerUpdaterHandlers(ipcMain);
if (sshManager) { registerSshHandlers(ipcMain);
registerSshHandlers(ipcMain);
}
logger.info('All handlers registered'); logger.info('All handlers registered');
} }
/**
* Re-initializes service-dependent IPC handlers after a mode switch (local SSH).
* This updates the module-level service references held by each domain handler module,
* ensuring IPC calls after the switch use the new service instances.
*/
export function reinitializeServiceHandlers(
scanner: ProjectScanner,
parser: SessionParser,
resolver: SubagentResolver,
builder: ChunkBuilder,
cache: DataCache
): void {
initializeProjectHandlers(scanner);
initializeSessionHandlers(scanner, parser, resolver, builder, cache);
initializeSearchHandlers(scanner);
initializeSubagentHandlers(builder, cache, parser, resolver, scanner);
logger.info('Service handlers re-initialized after mode switch');
}
/** /**
* Removes all IPC handlers. * Removes all IPC handlers.
* Should be called when shutting down. * Should be called when shutting down.