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:
parent
1b4f180e81
commit
5bf41c6ed8
2 changed files with 117 additions and 167 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue