refactor(02-02): IPC handlers route via ServiceContextRegistry
- projects.ts: Accept registry, resolve projectScanner via getActive() - sessions.ts: Accept registry, destructure all services from getActive() - search.ts: Accept registry, resolve projectScanner via getActive() - subagents.ts: Accept registry, resolve all services from getActive() - ssh.ts: Accept registry, create/destroy ServiceContext on connect/disconnect - SSH_CONNECT: Create new ServiceContext with SSH provider, register, start, switch - SSH_DISCONNECT: Switch to local, destroy SSH context - Import onContextSwitched from index.ts to rewire file watcher events - All handlers now call registry.getActive() at invocation time - No module-level service variables remain - fully dynamic routing
This commit is contained in:
parent
5bf41c6ed8
commit
24051acac8
5 changed files with 100 additions and 70 deletions
|
|
@ -14,18 +14,18 @@ import { type Project, type RepositoryGroup, type Session } from '../types';
|
||||||
|
|
||||||
import { validateProjectId } from './guards';
|
import { validateProjectId } from './guards';
|
||||||
|
|
||||||
import type { ProjectScanner } from '../services';
|
import type { ServiceContextRegistry } from '../services';
|
||||||
|
|
||||||
const logger = createLogger('IPC:projects');
|
const logger = createLogger('IPC:projects');
|
||||||
|
|
||||||
// Service instance - set via initialize
|
// Service registry - set via initialize
|
||||||
let projectScanner: ProjectScanner;
|
let registry: ServiceContextRegistry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes project handlers with service instance.
|
* Initializes project handlers with service registry.
|
||||||
*/
|
*/
|
||||||
export function initializeProjectHandlers(scanner: ProjectScanner): void {
|
export function initializeProjectHandlers(contextRegistry: ServiceContextRegistry): void {
|
||||||
projectScanner = scanner;
|
registry = contextRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,6 +60,7 @@ export function removeProjectHandlers(ipcMain: IpcMain): void {
|
||||||
*/
|
*/
|
||||||
async function handleGetProjects(_event: IpcMainInvokeEvent): Promise<Project[]> {
|
async function handleGetProjects(_event: IpcMainInvokeEvent): Promise<Project[]> {
|
||||||
try {
|
try {
|
||||||
|
const { projectScanner } = registry.getActive();
|
||||||
const projects = await projectScanner.scan();
|
const projects = await projectScanner.scan();
|
||||||
return projects;
|
return projects;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -75,6 +76,7 @@ async function handleGetProjects(_event: IpcMainInvokeEvent): Promise<Project[]>
|
||||||
*/
|
*/
|
||||||
async function handleGetRepositoryGroups(_event: IpcMainInvokeEvent): Promise<RepositoryGroup[]> {
|
async function handleGetRepositoryGroups(_event: IpcMainInvokeEvent): Promise<RepositoryGroup[]> {
|
||||||
try {
|
try {
|
||||||
|
const { projectScanner } = registry.getActive();
|
||||||
const groups = await projectScanner.scanWithWorktreeGrouping();
|
const groups = await projectScanner.scanWithWorktreeGrouping();
|
||||||
return groups;
|
return groups;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -100,6 +102,7 @@ async function handleGetWorktreeSessions(
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { projectScanner } = registry.getActive();
|
||||||
const sessions = await projectScanner.listWorktreeSessions(validatedProject.value!);
|
const sessions = await projectScanner.listWorktreeSessions(validatedProject.value!);
|
||||||
return sessions;
|
return sessions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,16 @@ import { coerceSearchMaxResults, validateProjectId, validateSearchQuery } from '
|
||||||
|
|
||||||
const logger = createLogger('IPC:search');
|
const logger = createLogger('IPC:search');
|
||||||
|
|
||||||
import type { ProjectScanner } from '../services';
|
import type { ServiceContextRegistry } from '../services';
|
||||||
|
|
||||||
// Service instance - set via initialize
|
// Service registry - set via initialize
|
||||||
let projectScanner: ProjectScanner;
|
let registry: ServiceContextRegistry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes search handlers with service instance.
|
* Initializes search handlers with service registry.
|
||||||
*/
|
*/
|
||||||
export function initializeSearchHandlers(scanner: ProjectScanner): void {
|
export function initializeSearchHandlers(contextRegistry: ServiceContextRegistry): void {
|
||||||
projectScanner = scanner;
|
registry = contextRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -68,6 +68,7 @@ async function handleSearchSessions(
|
||||||
return { results: [], totalMatches: 0, sessionsSearched: 0, query };
|
return { results: [], totalMatches: 0, sessionsSearched: 0, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { projectScanner } = registry.getActive();
|
||||||
const safeMaxResults = coerceSearchMaxResults(maxResults, 50);
|
const safeMaxResults = coerceSearchMaxResults(maxResults, 50);
|
||||||
const result = await projectScanner.searchSessions(
|
const result = await projectScanner.searchSessions(
|
||||||
validatedProject.value!,
|
validatedProject.value!,
|
||||||
|
|
|
||||||
|
|
@ -24,33 +24,19 @@ import {
|
||||||
|
|
||||||
import { coercePageLimit, validateProjectId, validateSessionId } from './guards';
|
import { coercePageLimit, validateProjectId, validateSessionId } from './guards';
|
||||||
|
|
||||||
import type { ChunkBuilder, ProjectScanner, SessionParser, SubagentResolver } from '../services';
|
import type { ServiceContextRegistry } from '../services';
|
||||||
import type { WaterfallData } from '@shared/types';
|
import type { WaterfallData } from '@shared/types';
|
||||||
|
|
||||||
const logger = createLogger('IPC:sessions');
|
const logger = createLogger('IPC:sessions');
|
||||||
|
|
||||||
// Service instances - set via initialize
|
// Service registry - set via initialize
|
||||||
let projectScanner: ProjectScanner;
|
let registry: ServiceContextRegistry;
|
||||||
let sessionParser: SessionParser;
|
|
||||||
let subagentResolver: SubagentResolver;
|
|
||||||
let chunkBuilder: ChunkBuilder;
|
|
||||||
let dataCache: DataCache;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes session handlers with service instances.
|
* Initializes session handlers with service registry.
|
||||||
*/
|
*/
|
||||||
export function initializeSessionHandlers(
|
export function initializeSessionHandlers(contextRegistry: ServiceContextRegistry): void {
|
||||||
scanner: ProjectScanner,
|
registry = contextRegistry;
|
||||||
parser: SessionParser,
|
|
||||||
resolver: SubagentResolver,
|
|
||||||
builder: ChunkBuilder,
|
|
||||||
cache: DataCache
|
|
||||||
): void {
|
|
||||||
projectScanner = scanner;
|
|
||||||
sessionParser = parser;
|
|
||||||
subagentResolver = resolver;
|
|
||||||
chunkBuilder = builder;
|
|
||||||
dataCache = cache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -100,6 +86,7 @@ async function handleGetSessions(
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { projectScanner } = registry.getActive();
|
||||||
const sessions = await projectScanner.listSessions(validatedProject.value!);
|
const sessions = await projectScanner.listSessions(validatedProject.value!);
|
||||||
return sessions;
|
return sessions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -128,6 +115,7 @@ async function handleGetSessionsPaginated(
|
||||||
return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 };
|
return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { projectScanner } = registry.getActive();
|
||||||
const safeLimit = coercePageLimit(limit, 20);
|
const safeLimit = coercePageLimit(limit, 20);
|
||||||
const result = await projectScanner.listSessionsPaginated(
|
const result = await projectScanner.listSessionsPaginated(
|
||||||
validatedProject.value!,
|
validatedProject.value!,
|
||||||
|
|
@ -162,6 +150,9 @@ async function handleGetSessionDetail(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache } =
|
||||||
|
registry.getActive();
|
||||||
|
|
||||||
const safeProjectId = validatedProject.value!;
|
const safeProjectId = validatedProject.value!;
|
||||||
const safeSessionId = validatedSession.value!;
|
const safeSessionId = validatedSession.value!;
|
||||||
const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);
|
const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);
|
||||||
|
|
@ -223,6 +214,7 @@ async function handleGetSessionGroups(
|
||||||
);
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const { sessionParser, subagentResolver, chunkBuilder } = registry.getActive();
|
||||||
const safeProjectId = validatedProject.value!;
|
const safeProjectId = validatedProject.value!;
|
||||||
const safeSessionId = validatedSession.value!;
|
const safeSessionId = validatedSession.value!;
|
||||||
|
|
||||||
|
|
@ -262,6 +254,7 @@ async function handleGetSessionMetrics(
|
||||||
if (!validatedProject.valid || !validatedSession.valid) {
|
if (!validatedProject.valid || !validatedSession.valid) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const { sessionParser, dataCache } = registry.getActive();
|
||||||
const safeProjectId = validatedProject.value!;
|
const safeProjectId = validatedProject.value!;
|
||||||
const safeSessionId = validatedSession.value!;
|
const safeSessionId = validatedSession.value!;
|
||||||
|
|
||||||
|
|
@ -297,6 +290,7 @@ async function handleGetWaterfallData(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { chunkBuilder } = registry.getActive();
|
||||||
return chunkBuilder.buildWaterfallData(detail.chunks, detail.processes);
|
return chunkBuilder.buildWaterfallData(detail.chunks, detail.processes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error in get-waterfall-data for ${projectId}/${sessionId}:`, error);
|
logger.error(`Error in get-waterfall-data for ${projectId}/${sessionId}:`, error);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
* SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests.
|
* SSH IPC Handlers - Manages SSH connection lifecycle from renderer requests.
|
||||||
*
|
*
|
||||||
* Channels:
|
* Channels:
|
||||||
* - ssh:connect - Connect to SSH host, switch to remote mode
|
* - ssh:connect - Connect to SSH host, create new context
|
||||||
* - ssh:disconnect - Disconnect and switch back to local mode
|
* - ssh:disconnect - Disconnect and switch back to local context
|
||||||
* - ssh:getState - Get current connection state
|
* - ssh:getState - Get current connection state
|
||||||
* - ssh:test - Test connection without switching
|
* - ssh:test - Test connection without switching
|
||||||
*/
|
*/
|
||||||
|
|
@ -20,13 +20,14 @@ import {
|
||||||
} from '@preload/constants/ipcChannels';
|
} from '@preload/constants/ipcChannels';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
|
||||||
import { configManager } from '../services';
|
import { configManager, ServiceContext } from '../services';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ServiceContextRegistry,
|
||||||
SshConnectionConfig,
|
SshConnectionConfig,
|
||||||
SshConnectionManager,
|
SshConnectionManager,
|
||||||
SshConnectionStatus,
|
SshConnectionStatus,
|
||||||
} from '../services/infrastructure/SshConnectionManager';
|
} from '../services';
|
||||||
import type { SshLastConnection } from '@shared/types';
|
import type { SshLastConnection } from '@shared/types';
|
||||||
import type { IpcMain } from 'electron';
|
import type { IpcMain } from 'electron';
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ const logger = createLogger('IPC:ssh');
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
let connectionManager: SshConnectionManager;
|
let connectionManager: SshConnectionManager;
|
||||||
let onModeSwitch: ((mode: 'local' | 'ssh') => Promise<void>) | null = null;
|
let registry: ServiceContextRegistry;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Initialization
|
// Initialization
|
||||||
|
|
@ -46,14 +47,14 @@ let onModeSwitch: ((mode: 'local' | 'ssh') => Promise<void>) | null = null;
|
||||||
/**
|
/**
|
||||||
* Initialize SSH handlers with required services.
|
* Initialize SSH handlers with required services.
|
||||||
* @param manager - The SSH connection manager instance
|
* @param manager - The SSH connection manager instance
|
||||||
* @param modeSwitchCallback - Called when switching between local/SSH mode
|
* @param contextRegistry - The service context registry
|
||||||
*/
|
*/
|
||||||
export function initializeSshHandlers(
|
export function initializeSshHandlers(
|
||||||
manager: SshConnectionManager,
|
manager: SshConnectionManager,
|
||||||
modeSwitchCallback: (mode: 'local' | 'ssh') => Promise<void>
|
contextRegistry: ServiceContextRegistry
|
||||||
): void {
|
): void {
|
||||||
connectionManager = manager;
|
connectionManager = manager;
|
||||||
onModeSwitch = modeSwitchCallback;
|
registry = contextRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -63,10 +64,41 @@ export function initializeSshHandlers(
|
||||||
export function registerSshHandlers(ipcMain: IpcMain): void {
|
export function registerSshHandlers(ipcMain: IpcMain): void {
|
||||||
ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => {
|
ipcMain.handle(SSH_CONNECT, async (_event, config: SshConnectionConfig) => {
|
||||||
try {
|
try {
|
||||||
|
// Connect to SSH host
|
||||||
await connectionManager.connect(config);
|
await connectionManager.connect(config);
|
||||||
if (onModeSwitch) {
|
|
||||||
await onModeSwitch('ssh');
|
// Get provider and remote path
|
||||||
|
const provider = connectionManager.getProvider();
|
||||||
|
const remoteProjectsPath = connectionManager.getRemoteProjectsPath() ?? undefined;
|
||||||
|
|
||||||
|
// Generate context ID
|
||||||
|
const contextId = `ssh-${config.host}`;
|
||||||
|
|
||||||
|
// Destroy existing SSH context if any (reconnection case)
|
||||||
|
if (registry.has(contextId)) {
|
||||||
|
logger.info(`Destroying existing SSH context: ${contextId}`);
|
||||||
|
registry.destroy(contextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new SSH context
|
||||||
|
const sshContext = new ServiceContext({
|
||||||
|
id: contextId,
|
||||||
|
type: 'ssh',
|
||||||
|
fsProvider: provider,
|
||||||
|
projectsDir: remoteProjectsPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register and start SSH context
|
||||||
|
registry.registerContext(sshContext);
|
||||||
|
sshContext.start();
|
||||||
|
|
||||||
|
// Switch to SSH context
|
||||||
|
registry.switch(contextId);
|
||||||
|
|
||||||
|
// Import and call context switch callback from index.ts
|
||||||
|
const { onContextSwitched } = await import('../index');
|
||||||
|
onContextSwitched(sshContext);
|
||||||
|
|
||||||
return { success: true, data: connectionManager.getStatus() };
|
return { success: true, data: connectionManager.getStatus() };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
|
@ -77,10 +109,27 @@ export function registerSshHandlers(ipcMain: IpcMain): void {
|
||||||
|
|
||||||
ipcMain.handle(SSH_DISCONNECT, async () => {
|
ipcMain.handle(SSH_DISCONNECT, async () => {
|
||||||
try {
|
try {
|
||||||
|
// Get current SSH context ID before disconnecting
|
||||||
|
const currentContextId = registry.getActiveContextId();
|
||||||
|
const isSshContext = currentContextId.startsWith('ssh-');
|
||||||
|
|
||||||
|
// Disconnect from SSH
|
||||||
connectionManager.disconnect();
|
connectionManager.disconnect();
|
||||||
if (onModeSwitch) {
|
|
||||||
await onModeSwitch('local');
|
// If we were on an SSH context, destroy it
|
||||||
|
if (isSshContext) {
|
||||||
|
// Switch back to local first (this also starts local file watcher)
|
||||||
|
registry.switch('local');
|
||||||
|
|
||||||
|
// Destroy the SSH context
|
||||||
|
registry.destroy(currentContextId);
|
||||||
|
|
||||||
|
// Call context switch callback to rewire events
|
||||||
|
const localContext = registry.getActive();
|
||||||
|
const { onContextSwitched } = await import('../index');
|
||||||
|
onContextSwitched(localContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, data: connectionManager.getStatus() };
|
return { success: true, data: connectionManager.getStatus() };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
|
|
||||||
|
|
@ -12,38 +12,18 @@ import { type SubagentDetail } from '../types';
|
||||||
|
|
||||||
import { validateProjectId, validateSessionId, validateSubagentId } from './guards';
|
import { validateProjectId, validateSessionId, validateSubagentId } from './guards';
|
||||||
|
|
||||||
import type {
|
import type { ServiceContextRegistry } from '../services';
|
||||||
ChunkBuilder,
|
|
||||||
DataCache,
|
|
||||||
ProjectScanner,
|
|
||||||
SessionParser,
|
|
||||||
SubagentResolver,
|
|
||||||
} from '../services';
|
|
||||||
|
|
||||||
const logger = createLogger('IPC:subagents');
|
const logger = createLogger('IPC:subagents');
|
||||||
|
|
||||||
// Service instances - set via initialize
|
// Service registry - set via initialize
|
||||||
let chunkBuilder: ChunkBuilder;
|
let registry: ServiceContextRegistry;
|
||||||
let dataCache: DataCache;
|
|
||||||
let sessionParser: SessionParser;
|
|
||||||
let subagentResolver: SubagentResolver;
|
|
||||||
let projectScanner: ProjectScanner;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes subagent handlers with service instances.
|
* Initializes subagent handlers with service registry.
|
||||||
*/
|
*/
|
||||||
export function initializeSubagentHandlers(
|
export function initializeSubagentHandlers(contextRegistry: ServiceContextRegistry): void {
|
||||||
builder: ChunkBuilder,
|
registry = contextRegistry;
|
||||||
cache: DataCache,
|
|
||||||
parser: SessionParser,
|
|
||||||
resolver: SubagentResolver,
|
|
||||||
scanner: ProjectScanner
|
|
||||||
): void {
|
|
||||||
chunkBuilder = builder;
|
|
||||||
dataCache = cache;
|
|
||||||
sessionParser = parser;
|
|
||||||
subagentResolver = resolver;
|
|
||||||
projectScanner = scanner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,6 +77,9 @@ async function handleGetSubagentDetail(
|
||||||
const safeSessionId = validatedSession.value!;
|
const safeSessionId = validatedSession.value!;
|
||||||
const safeSubagentId = validatedSubagent.value!;
|
const safeSubagentId = validatedSubagent.value!;
|
||||||
|
|
||||||
|
const { chunkBuilder, sessionParser, subagentResolver, projectScanner, dataCache } =
|
||||||
|
registry.getActive();
|
||||||
|
|
||||||
const cacheKey = `subagent-${safeProjectId}-${safeSessionId}-${safeSubagentId}`;
|
const cacheKey = `subagent-${safeProjectId}-${safeSessionId}-${safeSubagentId}`;
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue