refactor: extract startup lifecycle runtime deps

This commit is contained in:
2026-02-10 01:29:22 -08:00
parent 528cf0a57e
commit b177be0831
4 changed files with 284 additions and 147 deletions

View File

@@ -16,7 +16,7 @@
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "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: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/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-runtime-deps-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 dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-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/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-runtime-deps-service.test.js dist/core/services/startup-lifecycle-runtime-deps-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 dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js",
"test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.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", "generate:config-example": "pnpm run build && node dist/generate-config-example.js",
"start": "pnpm run build && electron . --start", "start": "pnpm run build && electron . --start",

View File

@@ -0,0 +1,74 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createStartupAppReadyDepsRuntimeService,
createStartupAppShutdownDepsRuntimeService,
} from "./startup-lifecycle-runtime-deps-service";
test("createStartupAppReadyDepsRuntimeService preserves runtime deps behavior", async () => {
const calls: string[] = [];
const deps = createStartupAppReadyDepsRuntimeService({
loadSubtitlePosition: () => calls.push("loadSubtitlePosition"),
resolveKeybindings: () => calls.push("resolveKeybindings"),
createMpvClient: () => calls.push("createMpvClient"),
reloadConfig: () => calls.push("reloadConfig"),
getResolvedConfig: () => ({
secondarySub: { defaultMode: "hover" },
websocket: { enabled: "auto", port: 1234 },
}),
getConfigWarnings: () => [],
logConfigWarning: () => {},
initRuntimeOptionsManager: () => calls.push("initRuntimeOptionsManager"),
setSecondarySubMode: () => calls.push("setSecondarySubMode"),
defaultSecondarySubMode: "hover",
defaultWebsocketPort: 8765,
hasMpvWebsocketPlugin: () => true,
startSubtitleWebsocket: () => calls.push("startSubtitleWebsocket"),
log: () => calls.push("log"),
createMecabTokenizerAndCheck: async () => {
calls.push("createMecab");
},
createSubtitleTimingTracker: () => calls.push("createSubtitleTimingTracker"),
loadYomitanExtension: async () => {
calls.push("loadYomitan");
},
texthookerOnlyMode: false,
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
initializeOverlayRuntime: () => calls.push("initOverlayRuntime"),
handleInitialArgs: () => calls.push("handleInitialArgs"),
});
deps.loadSubtitlePosition();
await deps.createMecabTokenizerAndCheck();
deps.handleInitialArgs();
assert.equal(deps.defaultWebsocketPort, 8765);
assert.equal(deps.defaultSecondarySubMode, "hover");
assert.deepEqual(calls, ["loadSubtitlePosition", "createMecab", "handleInitialArgs"]);
});
test("createStartupAppShutdownDepsRuntimeService preserves shutdown handlers", () => {
const calls: string[] = [];
const deps = createStartupAppShutdownDepsRuntimeService({
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"),
});
deps.stopSubtitleWebsocket();
deps.clearReconnectTimer();
deps.destroyAnkiIntegration();
assert.deepEqual(calls, [
"stopSubtitleWebsocket",
"clearReconnectTimer",
"destroyAnkiIntegration",
]);
});

View File

@@ -0,0 +1,55 @@
import {
AppReadyRuntimeDeps,
} from "./app-ready-runtime-service";
import {
AppShutdownRuntimeDeps,
} from "./app-shutdown-runtime-service";
export type StartupAppReadyDepsRuntimeOptions = AppReadyRuntimeDeps;
export type StartupAppShutdownDepsRuntimeOptions = AppShutdownRuntimeDeps;
export function createStartupAppReadyDepsRuntimeService(
options: StartupAppReadyDepsRuntimeOptions,
): AppReadyRuntimeDeps {
return {
loadSubtitlePosition: options.loadSubtitlePosition,
resolveKeybindings: options.resolveKeybindings,
createMpvClient: options.createMpvClient,
reloadConfig: options.reloadConfig,
getResolvedConfig: options.getResolvedConfig,
getConfigWarnings: options.getConfigWarnings,
logConfigWarning: options.logConfigWarning,
initRuntimeOptionsManager: options.initRuntimeOptionsManager,
setSecondarySubMode: options.setSecondarySubMode,
defaultSecondarySubMode: options.defaultSecondarySubMode,
defaultWebsocketPort: options.defaultWebsocketPort,
hasMpvWebsocketPlugin: options.hasMpvWebsocketPlugin,
startSubtitleWebsocket: options.startSubtitleWebsocket,
log: options.log,
createMecabTokenizerAndCheck: options.createMecabTokenizerAndCheck,
createSubtitleTimingTracker: options.createSubtitleTimingTracker,
loadYomitanExtension: options.loadYomitanExtension,
texthookerOnlyMode: options.texthookerOnlyMode,
shouldAutoInitializeOverlayRuntimeFromConfig:
options.shouldAutoInitializeOverlayRuntimeFromConfig,
initializeOverlayRuntime: options.initializeOverlayRuntime,
handleInitialArgs: options.handleInitialArgs,
};
}
export function createStartupAppShutdownDepsRuntimeService(
options: StartupAppShutdownDepsRuntimeOptions,
): AppShutdownRuntimeDeps {
return {
unregisterAllGlobalShortcuts: options.unregisterAllGlobalShortcuts,
stopSubtitleWebsocket: options.stopSubtitleWebsocket,
stopTexthookerService: options.stopTexthookerService,
destroyYomitanParserWindow: options.destroyYomitanParserWindow,
clearYomitanParserPromises: options.clearYomitanParserPromises,
stopWindowTracker: options.stopWindowTracker,
destroyMpvSocket: options.destroyMpvSocket,
clearReconnectTimer: options.clearReconnectTimer,
destroySubtitleTimingTracker: options.destroySubtitleTimingTracker,
destroyAnkiIntegration: options.destroyAnkiIntegration,
};
}

View File

@@ -207,6 +207,10 @@ import {
createOverlayWindowRuntimeDepsService, createOverlayWindowRuntimeDepsService,
createVisibleOverlayVisibilityDepsRuntimeService, createVisibleOverlayVisibilityDepsRuntimeService,
} from "./core/services/overlay-runtime-deps-service"; } from "./core/services/overlay-runtime-deps-service";
import {
createStartupAppReadyDepsRuntimeService,
createStartupAppShutdownDepsRuntimeService,
} from "./core/services/startup-lifecycle-runtime-deps-service";
import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service"; import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service";
import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service"; import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service";
import { import {
@@ -502,158 +506,162 @@ if (
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
logNoRunningInstance: () => appLogger.logNoRunningInstance(), logNoRunningInstance: () => appLogger.logNoRunningInstance(),
onReady: async () => { onReady: async () => {
await runAppReadyRuntimeService({ await runAppReadyRuntimeService(
loadSubtitlePosition: () => loadSubtitlePosition(), createStartupAppReadyDepsRuntimeService({
resolveKeybindings: () => { loadSubtitlePosition: () => loadSubtitlePosition(),
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS); resolveKeybindings: () => {
}, keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
createMpvClient: () => { },
mpvClient = new MpvIpcClient( createMpvClient: () => {
mpvSocketPath, mpvClient = new MpvIpcClient(
createMpvIpcClientDepsRuntimeService({ mpvSocketPath,
getResolvedConfig: () => getResolvedConfig(), createMpvIpcClientDepsRuntimeService({
autoStartOverlay, getResolvedConfig: () => getResolvedConfig(),
setOverlayVisible: (visible) => setOverlayVisible(visible), autoStartOverlay,
shouldBindVisibleOverlayToMpvSubVisibility: () => setOverlayVisible: (visible) => setOverlayVisible(visible),
shouldBindVisibleOverlayToMpvSubVisibility(), shouldBindVisibleOverlayToMpvSubVisibility: () =>
isVisibleOverlayVisible: () => visibleOverlayVisible, shouldBindVisibleOverlayToMpvSubVisibility(),
getReconnectTimer: () => reconnectTimer, isVisibleOverlayVisible: () => visibleOverlayVisible,
setReconnectTimer: (timer) => { getReconnectTimer: () => reconnectTimer,
reconnectTimer = timer; setReconnectTimer: (timer) => {
reconnectTimer = timer;
},
getCurrentSubText: () => currentSubText,
setCurrentSubText: (text) => {
currentSubText = text;
},
setCurrentSubAssText: (text) => {
currentSubAssText = text;
},
getSubtitleTimingTracker: () => subtitleTimingTracker,
subtitleWsBroadcast: (text) => {
subtitleWsService.broadcast(text);
},
getOverlayWindowsCount: () => getOverlayWindows().length,
tokenizeSubtitle: (text) => tokenizeSubtitle(text),
broadcastToOverlayWindows: (channel, ...args) => {
broadcastToOverlayWindows(channel, ...args);
},
updateCurrentMediaPath: (mediaPath) => {
updateCurrentMediaPath(mediaPath);
},
updateMpvSubtitleRenderMetrics: (patch) => {
updateMpvSubtitleRenderMetrics(patch);
},
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics,
setPreviousSecondarySubVisibility: (value) => {
previousSecondarySubVisibility = value;
},
showMpvOsd: (text) => {
showMpvOsd(text);
},
}),
);
},
reloadConfig: () => {
configService.reloadConfig();
},
getResolvedConfig: () => getResolvedConfig(),
getConfigWarnings: () => configService.getWarnings(),
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
initRuntimeOptionsManager: () => {
runtimeOptionsManager = createRuntimeOptionsManagerRuntimeService({
getAnkiConfig: () => configService.getConfig().ankiConnect,
applyAnkiPatch: (patch) => {
if (ankiIntegration) {
ankiIntegration.applyRuntimeConfigPatch(patch);
}
}, },
getCurrentSubText: () => currentSubText, onOptionsChanged: () => {
setCurrentSubText: (text) => { broadcastRuntimeOptionsChanged();
currentSubText = text; refreshOverlayShortcuts();
}, },
setCurrentSubAssText: (text) => { });
currentSubAssText = text; },
}, setSecondarySubMode: (mode) => {
getSubtitleTimingTracker: () => subtitleTimingTracker, secondarySubMode = mode;
subtitleWsBroadcast: (text) => { },
subtitleWsService.broadcast(text); defaultSecondarySubMode: "hover",
}, defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
getOverlayWindowsCount: () => getOverlayWindows().length, hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
tokenizeSubtitle: (text) => tokenizeSubtitle(text), startSubtitleWebsocket: (port) => {
broadcastToOverlayWindows: (channel, ...args) => { subtitleWsService.start(port, () => currentSubText);
broadcastToOverlayWindows(channel, ...args); },
}, log: (message) => appLogger.logInfo(message),
updateCurrentMediaPath: (mediaPath) => { createMecabTokenizerAndCheck: async () =>
updateCurrentMediaPath(mediaPath); createMecabTokenizerAndCheckRuntimeService({
}, createMecabTokenizer: () => new MecabTokenizer(),
updateMpvSubtitleRenderMetrics: (patch) => { setMecabTokenizer: (tokenizer) => {
updateMpvSubtitleRenderMetrics(patch); mecabTokenizer = tokenizer;
},
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics,
setPreviousSecondarySubVisibility: (value) => {
previousSecondarySubVisibility = value;
},
showMpvOsd: (text) => {
showMpvOsd(text);
}, },
}), }),
); createSubtitleTimingTracker: () =>
}, createSubtitleTimingTrackerRuntimeService({
reloadConfig: () => { createSubtitleTimingTracker: () => new SubtitleTimingTracker(),
configService.reloadConfig(); setSubtitleTimingTracker: (tracker) => {
}, subtitleTimingTracker = tracker;
getResolvedConfig: () => getResolvedConfig(), },
getConfigWarnings: () => configService.getWarnings(), }),
logConfigWarning: (warning) => appLogger.logConfigWarning(warning), loadYomitanExtension: async () => {
initRuntimeOptionsManager: () => { await loadYomitanExtension();
runtimeOptionsManager = createRuntimeOptionsManagerRuntimeService({ },
getAnkiConfig: () => configService.getConfig().ankiConnect, texthookerOnlyMode,
applyAnkiPatch: (patch) => { shouldAutoInitializeOverlayRuntimeFromConfig: () =>
if (ankiIntegration) { shouldAutoInitializeOverlayRuntimeFromConfig(),
ankiIntegration.applyRuntimeConfigPatch(patch); initializeOverlayRuntime: () => initializeOverlayRuntime(),
} handleInitialArgs: () => handleInitialArgs(),
}, }),
onOptionsChanged: () => { );
broadcastRuntimeOptionsChanged();
refreshOverlayShortcuts();
},
});
},
setSecondarySubMode: (mode) => {
secondarySubMode = mode;
},
defaultSecondarySubMode: "hover",
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
startSubtitleWebsocket: (port) => {
subtitleWsService.start(port, () => currentSubText);
},
log: (message) => appLogger.logInfo(message),
createMecabTokenizerAndCheck: async () =>
createMecabTokenizerAndCheckRuntimeService({
createMecabTokenizer: () => new MecabTokenizer(),
setMecabTokenizer: (tokenizer) => {
mecabTokenizer = tokenizer;
},
}),
createSubtitleTimingTracker: () =>
createSubtitleTimingTrackerRuntimeService({
createSubtitleTimingTracker: () => new SubtitleTimingTracker(),
setSubtitleTimingTracker: (tracker) => {
subtitleTimingTracker = tracker;
},
}),
loadYomitanExtension: async () => {
await loadYomitanExtension();
},
texthookerOnlyMode,
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
shouldAutoInitializeOverlayRuntimeFromConfig(),
initializeOverlayRuntime: () => initializeOverlayRuntime(),
handleInitialArgs: () => handleInitialArgs(),
});
}, },
onWillQuitCleanup: () => { onWillQuitCleanup: () => {
runAppShutdownRuntimeService({ runAppShutdownRuntimeService(
unregisterAllGlobalShortcuts: () => { createStartupAppShutdownDepsRuntimeService({
globalShortcut.unregisterAll(); unregisterAllGlobalShortcuts: () => {
}, globalShortcut.unregisterAll();
stopSubtitleWebsocket: () => { },
subtitleWsService.stop(); stopSubtitleWebsocket: () => {
}, subtitleWsService.stop();
stopTexthookerService: () => { },
texthookerService.stop(); stopTexthookerService: () => {
}, texthookerService.stop();
destroyYomitanParserWindow: () => { },
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) { destroyYomitanParserWindow: () => {
yomitanParserWindow.destroy(); if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
} yomitanParserWindow.destroy();
yomitanParserWindow = null; }
}, yomitanParserWindow = null;
clearYomitanParserPromises: () => { },
yomitanParserReadyPromise = null; clearYomitanParserPromises: () => {
yomitanParserInitPromise = null; yomitanParserReadyPromise = null;
}, yomitanParserInitPromise = null;
stopWindowTracker: () => { },
if (windowTracker) { stopWindowTracker: () => {
windowTracker.stop(); if (windowTracker) {
} windowTracker.stop();
}, }
destroyMpvSocket: () => { },
if (mpvClient && mpvClient.socket) { destroyMpvSocket: () => {
mpvClient.socket.destroy(); if (mpvClient && mpvClient.socket) {
} mpvClient.socket.destroy();
}, }
clearReconnectTimer: () => { },
if (reconnectTimer) { clearReconnectTimer: () => {
clearTimeout(reconnectTimer); if (reconnectTimer) {
} clearTimeout(reconnectTimer);
}, }
destroySubtitleTimingTracker: () => { },
if (subtitleTimingTracker) { destroySubtitleTimingTracker: () => {
subtitleTimingTracker.destroy(); if (subtitleTimingTracker) {
} subtitleTimingTracker.destroy();
}, }
destroyAnkiIntegration: () => { },
if (ankiIntegration) { destroyAnkiIntegration: () => {
ankiIntegration.destroy(); if (ankiIntegration) {
} ankiIntegration.destroy();
}, }
}); },
}),
);
}, },
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivate: () =>
overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0, overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,