197 lines · 5.8 KB · TypeScript
RawDownload
/** * ServiceContext - Bundle of session-data services for a single workspace context. * * Responsibilities: * - Encapsulate all session-data services (ProjectScanner, SessionParser, etc.) * - Manage service lifecycle (creation, start, stop, dispose) * - Provide isolation between local and SSH contexts * * Each ServiceContext represents a complete service stack for one workspace: * - Local context: ~/.claude/projects/ on local filesystem * - SSH context: remote ~/.claude/projects/ over SFTP */ import { ChunkBuilder } from '@main/services/analysis/ChunkBuilder';import { ProjectScanner } from '@main/services/discovery/ProjectScanner';import { SubagentResolver } from '@main/services/discovery/SubagentResolver';import { SessionParser } from '@main/services/parsing/SessionParser';import {  CACHE_CLEANUP_INTERVAL_MINUTES,  CACHE_TTL_MINUTES,  MAX_CACHE_SESSIONS,} from '@shared/constants';import { createLogger } from '@shared/utils/logger'; import { DataCache } from './DataCache';import { FileWatcher } from './FileWatcher'; import type { FileSystemProvider } from './FileSystemProvider'; const logger = createLogger('Infrastructure:ServiceContext'); /** * Configuration for creating a ServiceContext. */export interface ServiceContextConfig {  /** Unique identifier (e.g., 'local', 'ssh-myserver') */  id: string;  /** Context type */  type: 'local' | 'ssh';  /** Filesystem provider for this context */  fsProvider: FileSystemProvider;  /** Projects directory path (defaults to ~/.claude/projects) */  projectsDir?: string;  /** Todos directory path (defaults to ~/.claude/todos) */  todosDir?: string;} /** * ServiceContext - Isolated service bundle for one workspace context. * * Contains all session-data services configured for a specific workspace * (local or SSH). Services share the same FileSystemProvider and are * properly wired with dependencies. * * Lifecycle: * - Create: new ServiceContext(config) * - Start: context.start() — activates file watching and cache cleanup * - Pause: context.stopFileWatcher() — on context switch * - Resume: context.startFileWatcher() — on context switch back * - Destroy: context.dispose() — cleans up all resources */export class ServiceContext {  /** Context identifier */  readonly id: string;  /** Context type */  readonly type: 'local' | 'ssh';  /** Filesystem provider */  readonly fsProvider: FileSystemProvider;   // Service instances  readonly projectScanner: ProjectScanner;  readonly sessionParser: SessionParser;  readonly subagentResolver: SubagentResolver;  readonly chunkBuilder: ChunkBuilder;  readonly dataCache: DataCache;  readonly fileWatcher: FileWatcher;   private cleanupInterval: NodeJS.Timeout | null = null;  private disposed = false;   constructor(config: ServiceContextConfig) {    this.id = config.id;    this.type = config.type;    this.fsProvider = config.fsProvider;     logger.info(`Creating ServiceContext: ${config.id} (${config.type})`);     // Create services in dependency order    const disableCache = process.env.CLAUDE_CONTEXT_DISABLE_CACHE === '1';     // 1. ProjectScanner - no dependencies (uses fsProvider directly)    this.projectScanner = new ProjectScanner(      config.projectsDir,      config.todosDir,      config.fsProvider    );     // 2. SessionParser - depends on ProjectScanner    this.sessionParser = new SessionParser(this.projectScanner);     // 3. SubagentResolver - depends on ProjectScanner    this.subagentResolver = new SubagentResolver(this.projectScanner);     // 4. ChunkBuilder - no dependencies    this.chunkBuilder = new ChunkBuilder();     // 5. DataCache - standalone service    this.dataCache = new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache);     // 6. FileWatcher - uses fsProvider and dataCache    this.fileWatcher = new FileWatcher(      this.dataCache,      config.projectsDir,      config.todosDir,      config.fsProvider    );     logger.info(`ServiceContext created: ${config.id}`);  }   /**   * Starts the file watcher and cache cleanup.   * Call this after creating the context to activate monitoring.   */  start(): void {    if (this.disposed) {      logger.error(`Cannot start disposed context: ${this.id}`);      return;    }     logger.info(`Starting ServiceContext: ${this.id}`);     // Start file watcher    this.fileWatcher.start();     // Start cache auto-cleanup    this.cleanupInterval = this.dataCache.startAutoCleanup(CACHE_CLEANUP_INTERVAL_MINUTES);  }   /**   * Stops the file watcher (for pausing on context switch).   * Does not dispose resources - can be resumed with startFileWatcher().   */  stopFileWatcher(): void {    logger.info(`Stopping FileWatcher for context: ${this.id}`);    this.fileWatcher.stop();  }   /**   * Starts the file watcher (for resuming after context switch).   */  startFileWatcher(): void {    if (this.disposed) {      logger.error(`Cannot start FileWatcher on disposed context: ${this.id}`);      return;    }     logger.info(`Starting FileWatcher for context: ${this.id}`);    this.fileWatcher.start();  }   /**   * Disposes all resources.   * After calling dispose(), this context cannot be reused.   */  dispose(): void {    if (this.disposed) {      logger.warn(`ServiceContext already disposed: ${this.id}`);      return;    }     logger.info(`Disposing ServiceContext: ${this.id}`);     // Stop and dispose FileWatcher    this.fileWatcher.dispose();     // Dispose DataCache    this.dataCache.dispose();     // Clear cleanup interval    if (this.cleanupInterval) {      clearInterval(this.cleanupInterval);      this.cleanupInterval = null;    }     this.disposed = true;     logger.info(`ServiceContext disposed: ${this.id}`);  }   /**   * Returns whether this context has been disposed.   */  isDisposed(): boolean {    return this.disposed;  }}