refactor: improve code quality with stricter TypeScript and memory leak prevention

- Add stricter TypeScript compiler flags (noUnusedLocals, noUnusedParameters,
  noImplicitReturns, noImplicitOverride, noFallthroughCasesInSwitch,
  allowUnreachableCode, allowUnusedLabels)
- Remove unused variables caught by stricter flags:
  - Remove unused `renameSession` destructuring in App.tsx
  - Remove unused `BG_GRAY` constant in DirectAttach.ts
  - Remove unused `INPUT_BATCH_INTERVAL` constant in useSessionManager.ts
- Add proper EventEmitter cleanup to RalphLoop:
  - Store bound event handlers for cleanup
  - Add cleanupEventHandlers() method
  - Add destroy() method for complete cleanup
  - Add destroyRalphLoop() singleton cleanup function
- Update ralph-loop tests to use destroy() instead of stop() to prevent
  MaxListenersExceededWarning

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 14:01:47 +01:00
co-authored by Claude Opus 4.5
parent 8b04734538
commit c33d6e0987
6 changed files with 60 additions and 14 deletions
+50 -9
View File
@@ -72,6 +72,13 @@ export class RalphLoop extends EventEmitter {
private tasksCompleted: number = 0;
private tasksGenerated: number = 0;
/** Bound event handlers for cleanup (prevents memory leaks) */
private sessionEventHandlers: {
completion: (sessionId: string, phrase: string) => void;
error: (sessionId: string, error: string) => void;
stopped: (sessionId: string) => void;
} | null = null;
constructor(options: RalphLoopOptions = {}) {
super();
this.sessionManager = getSessionManager();
@@ -95,17 +102,32 @@ export class RalphLoop extends EventEmitter {
}
private setupEventHandlers(): void {
this.sessionManager.on('sessionCompletion', (sessionId: string, phrase: string) => {
this.handleSessionCompletion(sessionId, phrase);
});
// Store bound handlers for later cleanup
this.sessionEventHandlers = {
completion: (sessionId: string, phrase: string) => {
this.handleSessionCompletion(sessionId, phrase);
},
error: (sessionId: string, error: string) => {
this.handleSessionError(sessionId, error);
},
stopped: (sessionId: string) => {
this.handleSessionStopped(sessionId);
},
};
this.sessionManager.on('sessionError', (sessionId: string, error: string) => {
this.handleSessionError(sessionId, error);
});
this.sessionManager.on('sessionCompletion', this.sessionEventHandlers.completion);
this.sessionManager.on('sessionError', this.sessionEventHandlers.error);
this.sessionManager.on('sessionStopped', this.sessionEventHandlers.stopped);
}
this.sessionManager.on('sessionStopped', (sessionId: string) => {
this.handleSessionStopped(sessionId);
});
/** Remove event listeners to prevent memory leaks */
private cleanupEventHandlers(): void {
if (this.sessionEventHandlers) {
this.sessionManager.off('sessionCompletion', this.sessionEventHandlers.completion);
this.sessionManager.off('sessionError', this.sessionEventHandlers.error);
this.sessionManager.off('sessionStopped', this.sessionEventHandlers.stopped);
this.sessionEventHandlers = null;
}
}
get status(): RalphLoopStatus {
@@ -427,6 +449,17 @@ export class RalphLoop extends EventEmitter {
this.minDurationMs = hours * 60 * 60 * 1000;
this.store.setRalphLoopState({ minDurationMs: this.minDurationMs });
}
/**
* Destroys the loop and cleans up all resources.
* Use this for complete cleanup (e.g., in tests or before creating a new instance).
* After calling destroy(), the instance should not be reused.
*/
destroy(): void {
this.stop();
this.cleanupEventHandlers();
this.removeAllListeners();
}
}
// Singleton instance
@@ -439,3 +472,11 @@ export function getRalphLoop(options?: RalphLoopOptions): RalphLoop {
}
return loopInstance;
}
/** Destroys the singleton instance. Use in tests or for cleanup. */
export function destroyRalphLoop(): void {
if (loopInstance) {
loopInstance.destroy();
loopInstance = null;
}
}
-1
View File
@@ -122,7 +122,6 @@ export function App(): React.ReactElement {
cases,
lastUsedCase,
toggleRespawn,
renameSession,
} = useSessionManager();
// Calculate terminal height based on stdout dimensions
-1
View File
@@ -17,7 +17,6 @@ const RESET = `${CSI}0m`;
const BOLD = `${CSI}1m`;
const DIM = `${CSI}2m`;
const BG_BLUE = `${CSI}44m`;
const BG_GRAY = `${CSI}48;5;238m`;
const FG_WHITE = `${CSI}37m`;
const FG_CYAN = `${CSI}36m`;
const FG_YELLOW = `${CSI}33m`;
-1
View File
@@ -37,7 +37,6 @@ const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
const INNER_STATE_FILE = join(homedir(), '.claudeman', 'state-inner.json');
const SETTINGS_FILE = join(homedir(), '.claudeman', 'settings.json');
const OUTPUT_POLL_INTERVAL = 300; // Poll terminal output every 300ms (faster refresh)
const INPUT_BATCH_INTERVAL = 16; // Batch input every 16ms (60fps)
/**
* Emoji to ASCII replacement map for screen hardcopy output.
+2 -1
View File
@@ -124,7 +124,8 @@ describe('RalphLoop', () => {
});
afterEach(() => {
loop.stop();
// Use destroy() instead of stop() to clean up event listeners
loop.destroy();
vi.useRealTimers();
});
+8 -1
View File
@@ -15,7 +15,14 @@
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx",
"jsxImportSource": "react"
"jsxImportSource": "react",
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]