Skip to content

Commit bb03d8c

Browse files
Cleanup core
1 parent 1b716cc commit bb03d8c

13 files changed

Lines changed: 108 additions & 178 deletions

src/core/AppStateManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ export class AppStateManager {
7070
return appStore.select((state) => state.app.status) === "service-ready";
7171
}
7272

73-
destroy(): void {
73+
cleanup(): void {
74+
console.log("Cleaning up AppStateManager...");
7475
if (this.unsubscribe) {
7576
this.unsubscribe();
7677
this.unsubscribe = null;

src/core/CleanupManager.ts

Lines changed: 35 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,183 +1,67 @@
11
import { globalShortcut } from "electron";
22
import { TranscriptionPluginManager } from "../plugins";
3-
import { DictationWindowService } from "../services/DictationWindowService";
4-
import { SettingsService } from "../services/SettingsService";
5-
import { TrayService } from "../services/TrayService";
6-
import { WindowManager } from "./WindowManager";
73
import { promiseManager } from "./PromiseManager";
8-
import { PushToTalkManager } from "./PushToTalkManager";
4+
5+
export interface Cleanable {
6+
cleanup(): void | Promise<void>;
7+
}
98

109
export class CleanupManager {
11-
private transcriptionPluginManager: TranscriptionPluginManager;
12-
private dictationWindowService: DictationWindowService;
13-
private settingsService: SettingsService;
14-
private trayService: TrayService | null;
15-
private windowManager: WindowManager;
1610
private finishingTimeout: NodeJS.Timeout | null = null;
17-
private pushToTalkManager: PushToTalkManager | null = null;
1811

19-
constructor(
20-
transcriptionPluginManager: TranscriptionPluginManager,
21-
dictationWindowService: DictationWindowService,
22-
settingsService: SettingsService,
23-
trayService: TrayService | null,
24-
windowManager: WindowManager,
25-
) {
26-
this.transcriptionPluginManager = transcriptionPluginManager;
27-
this.dictationWindowService = dictationWindowService;
28-
this.settingsService = settingsService;
29-
this.trayService = trayService;
30-
this.windowManager = windowManager;
31-
}
32-
33-
setPushToTalkManager(manager: PushToTalkManager | null): void {
34-
this.pushToTalkManager = manager;
35-
}
12+
constructor(private readonly cleanables: (Cleanable | null)[] = []) {}
3613

3714
setFinishingTimeout(timeout: NodeJS.Timeout | null): void {
3815
this.finishingTimeout = timeout;
3916
}
4017

4118
async cleanup(): Promise<void> {
42-
console.log("=== Starting app cleanup ===");
19+
console.log("=== Starting comprehensive app cleanup ===");
4320

4421
const cleanupId = `app:cleanup:${Date.now()}`;
4522
promiseManager.start(cleanupId);
4623

24+
// Safety timeout to ensure app exits even if cleanup hangs
4725
const cleanupTimeout = setTimeout(() => {
48-
console.log("Cleanup timeout reached, forcing app quit...");
49-
promiseManager.reject(cleanupId, new Error("Cleanup timeout"));
26+
console.log("Cleanup timeout reached, forcing exit...");
5027
process.exit(0);
51-
}, 10000); // Increased timeout to 10 seconds
28+
}, 10000);
5229

5330
try {
54-
// Coordinate cleanup steps
55-
await promiseManager.sequence([
56-
async () => {
57-
console.log("Step 1: Stopping transcription...");
58-
await this.stopTranscription();
59-
return { step: "transcription", success: true };
60-
},
61-
async () => {
62-
console.log("Step 2: Disabling push-to-talk hotkey...");
63-
this.disposePushToTalkHotkey();
64-
return { step: "push-to-talk", success: true };
65-
},
66-
async () => {
67-
console.log("Step 3: Unregistering shortcuts...");
68-
this.unregisterShortcuts();
69-
return { step: "shortcuts", success: true };
70-
},
71-
async () => {
72-
console.log("Step 4: Clearing timeouts...");
73-
this.clearTimeouts();
74-
return { step: "timeouts", success: true };
75-
},
76-
async () => {
77-
console.log("Step 5: Cleaning up services...");
78-
this.cleanupServices();
79-
return { step: "services", success: true };
80-
},
81-
async () => {
82-
console.log("Step 6: Closing windows...");
83-
this.closeWindows();
84-
return { step: "windows", success: true };
85-
},
86-
async () => {
87-
console.log("Step 7: Waiting for graceful shutdown...");
88-
await new Promise((resolve) => setTimeout(resolve, 500));
89-
return { step: "graceful-wait", success: true };
90-
},
91-
async () => {
92-
console.log("Step 8: Force closing remaining windows...");
93-
this.forceCloseRemainingWindows();
94-
return { step: "force-close", success: true };
95-
},
96-
async () => {
97-
console.log("Step 9: Final cleanup...");
98-
await this.finalCleanup();
99-
return { step: "final", success: true };
100-
},
101-
]);
102-
103-
console.log("=== App cleanup completed successfully ===");
104-
promiseManager.resolve(cleanupId);
105-
} catch (error) {
106-
console.error("Error during cleanup:", error);
107-
promiseManager.reject(cleanupId, error);
108-
// Continue with cleanup even if there are errors
109-
} finally {
110-
clearTimeout(cleanupTimeout);
111-
}
112-
}
113-
114-
private unregisterShortcuts(): void {
115-
globalShortcut.unregisterAll();
116-
console.log("Global shortcuts unregistered");
117-
}
118-
119-
private disposePushToTalkHotkey(): void {
120-
if (this.pushToTalkManager) {
121-
this.pushToTalkManager.dispose();
122-
this.pushToTalkManager = null;
123-
console.log("Push-to-talk hotkey unregistered");
124-
}
125-
}
126-
127-
private async stopTranscription(): Promise<void> {
128-
await this.transcriptionPluginManager.stopTranscription();
129-
console.log("Transcription stopped");
130-
}
131-
132-
private closeWindows(): void {
133-
this.dictationWindowService.cleanup();
134-
this.settingsService.cleanup();
135-
this.windowManager.closeModelManagerWindow();
136-
console.log("Windows closed");
137-
}
138-
139-
private cleanupServices(): void {
140-
// Best-effort async cleanup; don't await here to keep shutdown fast
141-
void this.transcriptionPluginManager.cleanup();
142-
this.trayService?.destroy();
143-
console.log("Services cleaned up");
144-
}
145-
146-
private clearTimeouts(): void {
147-
if (this.finishingTimeout) {
148-
clearTimeout(this.finishingTimeout);
149-
this.finishingTimeout = null;
150-
}
151-
console.log("Timeouts cleared");
152-
}
153-
154-
private forceCloseRemainingWindows(): void {
155-
this.windowManager.forceCloseAllWindows();
156-
}
157-
158-
private async finalCleanup(): Promise<void> {
159-
try {
160-
// Remove event listeners from services that extend EventEmitter
161-
if (this.transcriptionPluginManager?.removeAllListeners) {
162-
this.transcriptionPluginManager.removeAllListeners();
31+
// Step 1: Clear application timeouts
32+
if (this.finishingTimeout) {
33+
clearTimeout(this.finishingTimeout);
34+
this.finishingTimeout = null;
16335
}
16436

165-
if (this.dictationWindowService?.removeAllListeners) {
166-
this.dictationWindowService.removeAllListeners();
37+
// Step 2: Cleanup all other registered components and services
38+
console.log("Step 1: Cleaning up registered components and services...");
39+
for (const component of this.cleanables) {
40+
try {
41+
if (component) {
42+
await component.cleanup();
43+
}
44+
} catch (err) {
45+
console.error("Error cleaning up component:", err);
46+
}
16747
}
16848

169-
// SettingsService doesn't extend EventEmitter, so it doesn't have removeAllListeners
170-
// Its cleanup is handled by its own cleanup() method
49+
// Step 3: Final environment cleanup
50+
console.log("Step 2: Final environment cleanup...");
51+
globalShortcut.unregisterAll();
17152

172-
// Force garbage collection if available
173-
if (typeof global !== "undefined" && global.gc) {
174-
global.gc();
53+
// Best-effort GC
54+
if (typeof global !== "undefined" && (global as any).gc) {
55+
(global as any).gc();
17556
}
17657

177-
console.log("All event listeners cleared and final cleanup completed");
58+
console.log("=== App cleanup completed successfully ===");
59+
promiseManager.resolve(cleanupId);
17860
} catch (error) {
179-
console.error("Error in final cleanup:", error);
180-
// Don't throw - we want to continue with app shutdown
61+
console.error("Cleanup failed:", error);
62+
promiseManager.reject(cleanupId, error);
63+
} finally {
64+
clearTimeout(cleanupTimeout);
18165
}
18266
}
18367
}

src/core/InitializationManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,8 @@ export class InitializationManager {
154154
});
155155
});
156156
}
157+
158+
cleanup(): void {
159+
console.log("Cleaning up InitializationManager...");
160+
}
157161
}

src/core/IpcHandlerManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class IpcHandlerManager {
5050
this.setupOnboardingSetupHandlers();
5151
}
5252

53-
cleanupIpcHandlers(): void {
53+
cleanup(): void {
5454
console.log("=== Cleaning up IPC handlers ===");
5555

5656
// Remove all dictation handlers

src/core/IpcStateBridge.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ export class IpcStateBridge {
188188
}
189189
}
190190

191-
destroy(): void {
191+
cleanup(): void {
192+
console.log("Cleaning up IpcStateBridge...");
192193
for (const [id] of this.subscriptions) {
193194
this.removeSubscription(id);
194195
}

src/core/PushToTalkManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export class PushToTalkManager {
7979
);
8080
}
8181

82-
dispose(): void {
82+
cleanup(): void {
83+
console.log("Cleaning up PushToTalkManager...");
8384
this.disposed = true;
8485

8586
if (this.settingsListener) {

src/core/ShortcutManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,9 @@ export class ShortcutManager {
310310
getRegisteredShortcuts(): string[] {
311311
return [...this.registeredShortcuts];
312312
}
313+
314+
cleanup(): void {
315+
console.log("Cleaning up ShortcutManager...");
316+
this.unregisterAll();
317+
}
313318
}

src/core/TrayInteractionManager.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ export class TrayInteractionManager {
1919
this.setupStatusChangeHandler();
2020
}
2121

22+
private unsubscribeStatusChange: (() => void) | null = null;
23+
2224
private setupStatusChangeHandler(): void {
23-
this.appStateManager.onSetupStatusChange((status) => {
24-
if (status === "idle" && this.pendingToggle) {
25-
this.pendingToggle = false;
26-
this.onToggleRecording();
27-
}
28-
});
25+
this.unsubscribeStatusChange = this.appStateManager.onSetupStatusChange(
26+
(status) => {
27+
if (status === "idle" && this.pendingToggle) {
28+
this.pendingToggle = false;
29+
this.onToggleRecording();
30+
}
31+
},
32+
);
2933
}
3034

3135
public handleTrayClick(): void {
@@ -119,4 +123,12 @@ export class TrayInteractionManager {
119123
console.error("Error showing dock after onboarding:", error);
120124
}
121125
}
126+
127+
cleanup(): void {
128+
console.log("Cleaning up TrayInteractionManager...");
129+
if (this.unsubscribeStatusChange) {
130+
this.unsubscribeStatusChange();
131+
this.unsubscribeStatusChange = null;
132+
}
133+
}
122134
}

src/core/WindowManager.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,11 @@ export class WindowManager {
113113

114114
console.log("=== All windows force closed ===");
115115
}
116+
117+
cleanup(): void {
118+
console.log("Cleaning up WindowManager...");
119+
this.closeOnboardingWindow();
120+
this.closeModelManagerWindow();
121+
this.forceCloseAllWindows();
122+
}
116123
}

src/main.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,25 @@ class WhisperMacApp {
368368
this.appStateManager.setSetupStatus("preparing-app");
369369

370370
await this.initializationManager.initialize();
371+
372+
this.cleanupManager = new CleanupManager([
373+
this.transcriptionPluginManager,
374+
this.dictationWindowService,
375+
this.settingsService,
376+
this.trayService,
377+
this.windowManager,
378+
this.appStateManager,
379+
this.shortcutManager,
380+
this.dictationFlowManager,
381+
this.ipcHandlerManager,
382+
this.initializationManager,
383+
this.trayInteractionManager,
384+
this.errorManager,
385+
this.modelManager,
386+
this.historyService,
387+
this.pushToTalkManager,
388+
ipcStateBridge,
389+
]);
371390
}
372391

373392
// ... (rest of methods remain the same)
@@ -385,16 +404,8 @@ class WhisperMacApp {
385404
);
386405
this.trayService.createTray();
387406

388-
this.cleanupManager = new CleanupManager(
389-
this.transcriptionPluginManager,
390-
this.dictationWindowService,
391-
this.settingsService,
392-
this.trayService,
393-
this.windowManager,
394-
);
395407
this.dictationFlowManager.setTrayService(this.trayService);
396408
this.shortcutManager.setDictationFlowManager(this.dictationFlowManager);
397-
this.cleanupManager.setPushToTalkManager(this.pushToTalkManager);
398409
}
399410

400411
private handleTrayClick(): void {
@@ -552,22 +563,15 @@ class WhisperMacApp {
552563
}
553564

554565
async cleanup(): Promise<void> {
555-
// Cleanup history service
556-
await this.historyService.cleanup();
557-
558566
console.log("=== WhisperMacApp cleanup starting ===");
559567

560568
try {
561569
const finishingTimeout =
562570
this.dictationFlowManager.setFinishingTimeout(null);
563571
this.cleanupManager.setFinishingTimeout(finishingTimeout);
564572

565-
console.log("Cleaning up IPC handlers...");
566-
this.ipcHandlerManager.cleanupIpcHandlers();
567-
568-
console.log("Starting comprehensive cleanup...");
573+
console.log("Starting comprehensive cleanup via CleanupManager...");
569574
await this.cleanupManager.cleanup();
570-
await this.transcriptionPluginManager.cleanup();
571575

572576
console.log("=== WhisperMacApp cleanup completed ===");
573577
} catch (error) {

0 commit comments

Comments
 (0)