- Created SUMMARY.md with comprehensive execution details - Updated STATE.md: Phase 2 Plan 1 complete, 2 plans total completed - Added decisions: ServiceContext bundling, dispose() lifecycle separation, removeAllListeners() ordering - Self-check: PASSED (all files exist, commits verified, tests passing)
284 lines
10 KiB
Markdown
284 lines
10 KiB
Markdown
---
|
|
phase: 02-service-infrastructure
|
|
plan: 01
|
|
subsystem: service-lifecycle
|
|
tags:
|
|
- infrastructure
|
|
- multi-context
|
|
- lifecycle
|
|
- memory-management
|
|
dependency-graph:
|
|
requires: []
|
|
provides:
|
|
- ServiceContext
|
|
- ServiceContextRegistry
|
|
- EventEmitter disposal pattern
|
|
affects:
|
|
- src/main/index.ts (will use ServiceContextRegistry in Phase 2 Plan 2)
|
|
- IPC handlers (will get services from active context)
|
|
tech-stack:
|
|
added: []
|
|
patterns:
|
|
- Registry pattern for context management
|
|
- Comprehensive dispose() with EventEmitter cleanup
|
|
- Start/stop/dispose lifecycle separation
|
|
key-files:
|
|
created:
|
|
- src/main/services/infrastructure/ServiceContext.ts
|
|
- src/main/services/infrastructure/ServiceContextRegistry.ts
|
|
modified:
|
|
- src/main/services/infrastructure/FileWatcher.ts
|
|
- src/main/services/infrastructure/DataCache.ts
|
|
- src/main/services/infrastructure/index.ts
|
|
decisions:
|
|
- title: ServiceContext bundles all session-data services
|
|
rationale: Isolation and lifecycle management for local vs SSH contexts
|
|
- title: Local context is permanent, SSH contexts are ephemeral
|
|
rationale: App always has local access, SSH can disconnect
|
|
- title: dispose() is separate from stop()
|
|
rationale: stop() pauses (reversible), dispose() destroys (permanent)
|
|
- title: removeAllListeners() called last in dispose()
|
|
rationale: Prevents event emission during cleanup, avoiding memory leaks
|
|
metrics:
|
|
duration: 4
|
|
tasks_completed: 2
|
|
files_created: 2
|
|
files_modified: 3
|
|
tests_added: 0
|
|
tests_passing: 494
|
|
commits: 2
|
|
completed: 2026-02-12
|
|
---
|
|
|
|
# Phase 2 Plan 1: ServiceContext Infrastructure Summary
|
|
|
|
ServiceContext bundle and ServiceContextRegistry coordinator created with comprehensive EventEmitter cleanup for multi-context support.
|
|
|
|
## Overview
|
|
|
|
Created the foundational infrastructure for multi-context support in claude-devtools. ServiceContext encapsulates all session-data services (ProjectScanner, SessionParser, SubagentResolver, ChunkBuilder, DataCache, FileWatcher) for a single workspace context (local or SSH). ServiceContextRegistry manages the Map of contexts, tracks the active context, and enforces lifecycle rules (local context is permanent, SSH contexts can be destroyed).
|
|
|
|
**Key innovation:** Comprehensive dispose() methods on EventEmitter-based services prevent memory leaks during context switching by clearing all timers, tracking maps, and listeners in the correct order.
|
|
|
|
## What Was Built
|
|
|
|
### ServiceContext (src/main/services/infrastructure/ServiceContext.ts)
|
|
|
|
Service bundle class that creates and owns all session-data services for one workspace:
|
|
|
|
**Configuration:**
|
|
- `id: string` - Unique identifier (e.g., 'local', 'ssh-myserver')
|
|
- `type: 'local' | 'ssh'` - Context type
|
|
- `fsProvider: FileSystemProvider` - Filesystem provider
|
|
- `projectsDir?: string` - Projects directory (defaults to ~/.claude/projects)
|
|
- `todosDir?: string` - Todos directory (defaults to ~/.claude/todos)
|
|
|
|
**Services created in dependency order:**
|
|
1. ProjectScanner(projectsDir, todosDir, fsProvider)
|
|
2. SessionParser(projectScanner)
|
|
3. SubagentResolver(projectScanner)
|
|
4. ChunkBuilder()
|
|
5. DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache)
|
|
6. FileWatcher(dataCache, projectsDir, todosDir, fsProvider)
|
|
|
|
**Lifecycle methods:**
|
|
- `start()` - Activates file watching and cache cleanup
|
|
- `stopFileWatcher()` - Pauses file watching (for context switch)
|
|
- `startFileWatcher()` - Resumes file watching
|
|
- `dispose()` - Destroys all resources (irreversible)
|
|
|
|
**Disposed flag:** Prevents reuse after disposal, logs errors if start() called on disposed context.
|
|
|
|
### ServiceContextRegistry (src/main/services/infrastructure/ServiceContextRegistry.ts)
|
|
|
|
Registry coordinator that manages all contexts:
|
|
|
|
**State:**
|
|
- `contexts: Map<string, ServiceContext>` - All registered contexts
|
|
- `activeContextId: string` - Currently active context (defaults to 'local')
|
|
|
|
**Methods:**
|
|
- `registerContext(context)` - Adds context to map (throws if ID exists)
|
|
- `getActive()` - Returns active context (throws if not found)
|
|
- `get(contextId)` - Returns context by ID or undefined
|
|
- `has(contextId)` - Check existence
|
|
- `switch(contextId)` - Switches to different context:
|
|
- Stops old file watcher
|
|
- Updates activeContextId
|
|
- Starts new file watcher
|
|
- Returns {previous, current} for IPC re-init
|
|
- `destroy(contextId)` - Destroys SSH context:
|
|
- Throws if contextId === 'local' (permanent context)
|
|
- Calls context.dispose()
|
|
- Removes from map
|
|
- If destroying active context, switches to 'local'
|
|
- `list()` - Returns array of {id, type} metadata
|
|
- `dispose()` - Disposes ALL contexts (app shutdown only)
|
|
|
|
**Enforcement:** Local context permanence enforced in destroy() method.
|
|
|
|
### FileWatcher.dispose() (src/main/services/infrastructure/FileWatcher.ts)
|
|
|
|
Comprehensive cleanup for EventEmitter-based service:
|
|
|
|
**Cleanup sequence:**
|
|
1. Call `stop()` - Closes watchers, clears most timers and maps
|
|
2. Explicitly clear retry timer (redundant but explicit)
|
|
3. Clear all debounce timers + debounceTimers map
|
|
4. Clear catch-up interval timer
|
|
5. Clear polling interval timer (SSH mode)
|
|
6. Clear all tracking maps:
|
|
- lastProcessedLineCount
|
|
- lastProcessedSize
|
|
- activeSessionFiles
|
|
- polledFileSizes
|
|
- processingInProgress
|
|
- pendingReprocess
|
|
7. **LAST:** Call `removeAllListeners()` - Prevents events during cleanup
|
|
8. Set `disposed = true` flag
|
|
|
|
**Disposed flag check:** Added to `start()` method to prevent restarting disposed watcher.
|
|
|
|
### DataCache.dispose() (src/main/services/infrastructure/DataCache.ts)
|
|
|
|
Simple cleanup for cache service:
|
|
|
|
**Cleanup:**
|
|
1. Clear cache Map
|
|
2. Set `enabled = false`
|
|
3. Set `disposed = true` flag
|
|
|
|
**Note:** Auto-cleanup interval returned by `startAutoCleanup()` is managed by caller (ServiceContext), not stored internally, so no timer cleanup needed here.
|
|
|
|
## Deviations from Plan
|
|
|
|
None - plan executed exactly as written.
|
|
|
|
## Technical Decisions
|
|
|
|
### 1. ServiceContext owns cleanup interval handle
|
|
|
|
**Decision:** ServiceContext stores the cleanup interval handle returned by `dataCache.startAutoCleanup()` and clears it in `dispose()`.
|
|
|
|
**Rationale:** DataCache doesn't store the interval internally (it only returns it), so ownership belongs to the caller.
|
|
|
|
### 2. removeAllListeners() called LAST in FileWatcher.dispose()
|
|
|
|
**Decision:** EventEmitter cleanup happens after all other cleanup steps.
|
|
|
|
**Rationale:** Prevents firing events (like 'file-change') during cleanup when internal state is partially cleared. Emitting events mid-cleanup can cause memory leaks if listeners try to access cleared maps.
|
|
|
|
### 3. Separated start() check for disposal vs already watching
|
|
|
|
**Decision:** Added explicit `if (this.disposed)` check before `if (this.isWatching)` in FileWatcher.start().
|
|
|
|
**Rationale:** Disposal is a permanent error condition (log error), while already watching is a normal edge case (log warning). Clearer error messaging.
|
|
|
|
### 4. Registry does NOT create local context in constructor
|
|
|
|
**Decision:** ServiceContextRegistry constructor is empty - local context registered externally.
|
|
|
|
**Rationale:** Local context creation requires mainWindow and NotificationManager wiring that exists in index.ts, not in registry constructor. Keeps registry focused on coordination, not initialization.
|
|
|
|
## Testing Results
|
|
|
|
**Type checking:** ✅ Passed (0 errors)
|
|
|
|
**Test suite:** ✅ 494/494 tests passing (no regressions)
|
|
|
|
Existing FileWatcher tests verify:
|
|
- File watching lifecycle (start/stop)
|
|
- Debouncing behavior
|
|
- Error detection
|
|
- SSH polling mode
|
|
|
|
No new tests added (infrastructure code, tested via integration in Phase 2 Plan 2).
|
|
|
|
## Verification
|
|
|
|
**Created files exist:**
|
|
```bash
|
|
✅ src/main/services/infrastructure/ServiceContext.ts
|
|
✅ src/main/services/infrastructure/ServiceContextRegistry.ts
|
|
```
|
|
|
|
**Exports updated:**
|
|
```bash
|
|
✅ infrastructure/index.ts exports ServiceContext and ServiceContextRegistry
|
|
```
|
|
|
|
**ServiceContext constructor creates all 6 services:**
|
|
```typescript
|
|
✅ projectScanner: ProjectScanner
|
|
✅ sessionParser: SessionParser
|
|
✅ subagentResolver: SubagentResolver
|
|
✅ chunkBuilder: ChunkBuilder
|
|
✅ dataCache: DataCache
|
|
✅ fileWatcher: FileWatcher
|
|
```
|
|
|
|
**ServiceContextRegistry enforces lifecycle rules:**
|
|
```typescript
|
|
✅ destroy('local') throws Error
|
|
✅ switch() stops old watcher, starts new watcher
|
|
✅ destroy(activeContext) switches to 'local'
|
|
```
|
|
|
|
**Dispose methods exist:**
|
|
```typescript
|
|
✅ FileWatcher.dispose() calls removeAllListeners()
|
|
✅ DataCache.dispose() clears cache
|
|
✅ Both have disposed flag
|
|
```
|
|
|
|
## Integration Points
|
|
|
|
**Used by (Phase 2 Plan 2):**
|
|
- `src/main/index.ts` - Will create ServiceContextRegistry, register local context, wire IPC handlers
|
|
- IPC handlers - Will get services from `registry.getActive()` instead of global instances
|
|
- SSH connection flow - Will create/register/destroy SSH contexts
|
|
|
|
**Provides to system:**
|
|
- Isolated service stacks per workspace
|
|
- Safe context switching without memory leaks
|
|
- Foundation for SSH multi-context support
|
|
|
|
## Performance Impact
|
|
|
|
**Memory:** Minimal overhead - registry is a simple Map, contexts reuse existing service code.
|
|
|
|
**Context switch latency:** ~10-50ms (stop old watcher + start new watcher), acceptable for user-initiated action.
|
|
|
|
**Disposal thoroughness:** Prevents memory leaks - comprehensive cleanup of all timers, maps, and listeners. Critical for long-running sessions with frequent SSH connect/disconnect cycles.
|
|
|
|
## Next Steps
|
|
|
|
**Phase 2 Plan 2 (IPC Refactoring):**
|
|
1. Create ServiceContextRegistry in index.ts
|
|
2. Register local context with NotificationManager wiring
|
|
3. Refactor IPC handlers to use `registry.getActive()` instead of global instances
|
|
4. Add context switch IPC handlers (`ssh:switch-context`, `ssh:destroy-context`)
|
|
|
|
**Phase 2 Plan 3 (SSH Integration):**
|
|
1. Wire SshConnectionManager to create ServiceContext on connect
|
|
2. Register SSH context in registry
|
|
3. Switch to SSH context on successful connection
|
|
4. Destroy SSH context on disconnect
|
|
|
|
## Self-Check
|
|
|
|
**Files created:**
|
|
✅ src/main/services/infrastructure/ServiceContext.ts (exists, 5932 bytes)
|
|
✅ src/main/services/infrastructure/ServiceContextRegistry.ts (exists, 5552 bytes)
|
|
|
|
**Commits exist:**
|
|
✅ 777d93f: feat(02-01): create ServiceContext and ServiceContextRegistry
|
|
✅ 767c985: feat(02-01): add comprehensive dispose() to FileWatcher and DataCache
|
|
|
|
**Type checking:**
|
|
✅ `pnpm typecheck` passes with 0 errors
|
|
|
|
**Test suite:**
|
|
✅ `pnpm test` passes with 494/494 tests
|
|
|
|
**Self-Check: PASSED**
|