From 8b286f15e80bbde47028aeb10983893675d956d5 Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 10 Feb 2026 00:06:10 -0800 Subject: [PATCH] refactor: extract app shutdown orchestration service --- package.json | 2 +- .../app-shutdown-runtime-service.test.ts | 32 +++++++++ .../services/app-shutdown-runtime-service.ts | 27 +++++++ src/main.ts | 71 ++++++++++++------- 4 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 src/core/services/app-shutdown-runtime-service.test.ts create mode 100644 src/core/services/app-shutdown-runtime-service.ts diff --git a/package.json b/package.json index 8138fcb..9acee4f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "test:config": "pnpm run build && node --test dist/config/config.test.js", - "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js", + "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js", "test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js", "generate:config-example": "pnpm run build && node dist/generate-config-example.js", "start": "pnpm run build && electron . --start", diff --git a/src/core/services/app-shutdown-runtime-service.test.ts b/src/core/services/app-shutdown-runtime-service.test.ts new file mode 100644 index 0000000..aa45bea --- /dev/null +++ b/src/core/services/app-shutdown-runtime-service.test.ts @@ -0,0 +1,32 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { runAppShutdownRuntimeService } from "./app-shutdown-runtime-service"; + +test("runAppShutdownRuntimeService runs teardown steps in order", () => { + const calls: string[] = []; + runAppShutdownRuntimeService({ + unregisterAllGlobalShortcuts: () => calls.push("unregisterAllGlobalShortcuts"), + stopSubtitleWebsocket: () => calls.push("stopSubtitleWebsocket"), + stopTexthookerService: () => calls.push("stopTexthookerService"), + destroyYomitanParserWindow: () => calls.push("destroyYomitanParserWindow"), + clearYomitanParserPromises: () => calls.push("clearYomitanParserPromises"), + stopWindowTracker: () => calls.push("stopWindowTracker"), + destroyMpvSocket: () => calls.push("destroyMpvSocket"), + clearReconnectTimer: () => calls.push("clearReconnectTimer"), + destroySubtitleTimingTracker: () => calls.push("destroySubtitleTimingTracker"), + destroyAnkiIntegration: () => calls.push("destroyAnkiIntegration"), + }); + + assert.deepEqual(calls, [ + "unregisterAllGlobalShortcuts", + "stopSubtitleWebsocket", + "stopTexthookerService", + "destroyYomitanParserWindow", + "clearYomitanParserPromises", + "stopWindowTracker", + "destroyMpvSocket", + "clearReconnectTimer", + "destroySubtitleTimingTracker", + "destroyAnkiIntegration", + ]); +}); diff --git a/src/core/services/app-shutdown-runtime-service.ts b/src/core/services/app-shutdown-runtime-service.ts new file mode 100644 index 0000000..7680387 --- /dev/null +++ b/src/core/services/app-shutdown-runtime-service.ts @@ -0,0 +1,27 @@ +export interface AppShutdownRuntimeDeps { + unregisterAllGlobalShortcuts: () => void; + stopSubtitleWebsocket: () => void; + stopTexthookerService: () => void; + destroyYomitanParserWindow: () => void; + clearYomitanParserPromises: () => void; + stopWindowTracker: () => void; + destroyMpvSocket: () => void; + clearReconnectTimer: () => void; + destroySubtitleTimingTracker: () => void; + destroyAnkiIntegration: () => void; +} + +export function runAppShutdownRuntimeService( + deps: AppShutdownRuntimeDeps, +): void { + deps.unregisterAllGlobalShortcuts(); + deps.stopSubtitleWebsocket(); + deps.stopTexthookerService(); + deps.destroyYomitanParserWindow(); + deps.clearYomitanParserPromises(); + deps.stopWindowTracker(); + deps.destroyMpvSocket(); + deps.clearReconnectTimer(); + deps.destroySubtitleTimingTracker(); + deps.destroyAnkiIntegration(); +} diff --git a/src/main.ts b/src/main.ts index 008a013..2083c8c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -202,6 +202,7 @@ import { setOverlayDebugVisualizationEnabledRuntimeService, } from "./core/services/overlay-broadcast-runtime-service"; import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-service"; +import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runtime-service"; import { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService, @@ -564,30 +565,52 @@ if (initialArgs.generateConfig && !shouldStartApp(initialArgs)) { }); }, onWillQuitCleanup: () => { - globalShortcut.unregisterAll(); - subtitleWsService.stop(); - texthookerService.stop(); - if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) { - yomitanParserWindow.destroy(); - } - yomitanParserWindow = null; - yomitanParserReadyPromise = null; - yomitanParserInitPromise = null; - if (windowTracker) { - windowTracker.stop(); - } - if (mpvClient && mpvClient.socket) { - mpvClient.socket.destroy(); - } - if (reconnectTimer) { - clearTimeout(reconnectTimer); - } - if (subtitleTimingTracker) { - subtitleTimingTracker.destroy(); - } - if (ankiIntegration) { - ankiIntegration.destroy(); - } + runAppShutdownRuntimeService({ + unregisterAllGlobalShortcuts: () => { + globalShortcut.unregisterAll(); + }, + stopSubtitleWebsocket: () => { + subtitleWsService.stop(); + }, + stopTexthookerService: () => { + texthookerService.stop(); + }, + destroyYomitanParserWindow: () => { + if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) { + yomitanParserWindow.destroy(); + } + yomitanParserWindow = null; + }, + clearYomitanParserPromises: () => { + yomitanParserReadyPromise = null; + yomitanParserInitPromise = null; + }, + stopWindowTracker: () => { + if (windowTracker) { + windowTracker.stop(); + } + }, + destroyMpvSocket: () => { + if (mpvClient && mpvClient.socket) { + mpvClient.socket.destroy(); + } + }, + clearReconnectTimer: () => { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + }, + destroySubtitleTimingTracker: () => { + if (subtitleTimingTracker) { + subtitleTimingTracker.destroy(); + } + }, + destroyAnkiIntegration: () => { + if (ankiIntegration) { + ankiIntegration.destroy(); + } + }, + }); }, shouldRestoreWindowsOnActivate: () => overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,