refactor: extract startup bootstrap runtime orchestration

This commit is contained in:
2026-02-10 01:40:57 -08:00
parent cb93601e16
commit 31f76ad476
3 changed files with 376 additions and 208 deletions

View File

@@ -0,0 +1,105 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
runStartupBootstrapRuntimeService,
} from "./startup-bootstrap-runtime-service";
import { CliArgs } from "../../cli/args";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return {
start: false,
stop: false,
toggle: false,
toggleVisibleOverlay: false,
toggleInvisibleOverlay: false,
settings: false,
show: false,
hide: false,
showVisibleOverlay: false,
hideVisibleOverlay: false,
showInvisibleOverlay: false,
hideInvisibleOverlay: false,
copySubtitle: false,
copySubtitleMultiple: false,
mineSentence: false,
mineSentenceMultiple: false,
updateLastCardFromClipboard: false,
toggleSecondarySub: false,
triggerFieldGrouping: false,
triggerSubsync: false,
markAudioCard: false,
openRuntimeOptions: false,
texthooker: false,
help: false,
autoStartOverlay: false,
generateConfig: false,
backupOverwrite: false,
verbose: false,
...overrides,
};
}
test("runStartupBootstrapRuntimeService configures startup state and starts lifecycle", () => {
const calls: string[] = [];
const args = makeArgs({
verbose: true,
socketPath: "/tmp/custom.sock",
texthookerPort: 9001,
backend: "x11",
autoStartOverlay: true,
texthooker: true,
});
const result = runStartupBootstrapRuntimeService({
argv: ["node", "main.ts", "--verbose"],
parseArgs: () => args,
setLogLevelEnv: (level) => calls.push(`setLog:${level}`),
enableVerboseLogging: () => calls.push("enableVerbose"),
forceX11Backend: () => calls.push("forceX11"),
enforceUnsupportedWaylandMode: () => calls.push("enforceWayland"),
getDefaultSocketPath: () => "/tmp/default.sock",
defaultTexthookerPort: 5174,
runGenerateConfigFlow: () => false,
startAppLifecycle: () => calls.push("startLifecycle"),
});
assert.equal(result.initialArgs, args);
assert.equal(result.mpvSocketPath, "/tmp/custom.sock");
assert.equal(result.texthookerPort, 9001);
assert.equal(result.backendOverride, "x11");
assert.equal(result.autoStartOverlay, true);
assert.equal(result.texthookerOnlyMode, true);
assert.deepEqual(calls, [
"enableVerbose",
"forceX11",
"enforceWayland",
"startLifecycle",
]);
});
test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flow handled", () => {
const calls: string[] = [];
const args = makeArgs({ generateConfig: true, logLevel: "warn" });
const result = runStartupBootstrapRuntimeService({
argv: ["node", "main.ts", "--generate-config"],
parseArgs: () => args,
setLogLevelEnv: (level) => calls.push(`setLog:${level}`),
enableVerboseLogging: () => calls.push("enableVerbose"),
forceX11Backend: () => calls.push("forceX11"),
enforceUnsupportedWaylandMode: () => calls.push("enforceWayland"),
getDefaultSocketPath: () => "/tmp/default.sock",
defaultTexthookerPort: 5174,
runGenerateConfigFlow: () => true,
startAppLifecycle: () => calls.push("startLifecycle"),
});
assert.equal(result.mpvSocketPath, "/tmp/default.sock");
assert.equal(result.texthookerPort, 5174);
assert.equal(result.backendOverride, null);
assert.deepEqual(calls, [
"setLog:warn",
"forceX11",
"enforceWayland",
]);
});

View File

@@ -0,0 +1,53 @@
import { CliArgs } from "../../cli/args";
export interface StartupBootstrapRuntimeState {
initialArgs: CliArgs;
mpvSocketPath: string;
texthookerPort: number;
backendOverride: string | null;
autoStartOverlay: boolean;
texthookerOnlyMode: boolean;
}
export interface StartupBootstrapRuntimeDeps {
argv: string[];
parseArgs: (argv: string[]) => CliArgs;
setLogLevelEnv: (level: string) => void;
enableVerboseLogging: () => void;
forceX11Backend: (args: CliArgs) => void;
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
getDefaultSocketPath: () => string;
defaultTexthookerPort: number;
runGenerateConfigFlow: (args: CliArgs) => boolean;
startAppLifecycle: (args: CliArgs) => void;
}
export function runStartupBootstrapRuntimeService(
deps: StartupBootstrapRuntimeDeps,
): StartupBootstrapRuntimeState {
const initialArgs = deps.parseArgs(deps.argv);
if (initialArgs.logLevel) {
deps.setLogLevelEnv(initialArgs.logLevel);
} else if (initialArgs.verbose) {
deps.enableVerboseLogging();
}
deps.forceX11Backend(initialArgs);
deps.enforceUnsupportedWaylandMode(initialArgs);
const state: StartupBootstrapRuntimeState = {
initialArgs,
mpvSocketPath: initialArgs.socketPath ?? deps.getDefaultSocketPath(),
texthookerPort: initialArgs.texthookerPort ?? deps.defaultTexthookerPort,
backendOverride: initialArgs.backend ?? null,
autoStartOverlay: initialArgs.autoStartOverlay,
texthookerOnlyMode: initialArgs.texthooker,
};
if (!deps.runGenerateConfigFlow(initialArgs)) {
deps.startAppLifecycle(initialArgs);
}
return state;
}

View File

@@ -236,6 +236,7 @@ import {
createSubtitleTimingTrackerRuntimeService,
} from "./core/services/startup-resource-runtime-service";
import { runGenerateConfigFlowRuntimeService } from "./core/services/config-generation-runtime-service";
import { runStartupBootstrapRuntimeService } from "./core/services/startup-bootstrap-runtime-service";
import {
runSubsyncManualFromIpcRuntimeService,
triggerSubsyncFromConfigRuntimeService,
@@ -479,27 +480,28 @@ function updateCurrentMediaPath(mediaPath: unknown): void {
let subsyncInProgress = false;
const initialArgs = parseArgs(process.argv);
if (initialArgs.logLevel) {
process.env.SUBMINER_LOG_LEVEL = initialArgs.logLevel;
} else if (initialArgs.verbose) {
const startupState = runStartupBootstrapRuntimeService({
argv: process.argv,
parseArgs: (argv) => parseArgs(argv),
setLogLevelEnv: (level) => {
process.env.SUBMINER_LOG_LEVEL = level;
},
enableVerboseLogging: () => {
process.env.SUBMINER_LOG_LEVEL = "debug";
}
forceX11Backend(initialArgs);
enforceUnsupportedWaylandMode(initialArgs);
let mpvSocketPath = initialArgs.socketPath ?? getDefaultSocketPath();
let texthookerPort = initialArgs.texthookerPort ?? DEFAULT_TEXTHOOKER_PORT;
const backendOverride = initialArgs.backend ?? null;
const autoStartOverlay = initialArgs.autoStartOverlay;
const texthookerOnlyMode = initialArgs.texthooker;
if (
!runGenerateConfigFlowRuntimeService(initialArgs, {
shouldStartApp: (args) => shouldStartApp(args),
generateConfig: async (args) =>
generateDefaultConfigFile(args, {
},
forceX11Backend: (args) => {
forceX11Backend(args);
},
enforceUnsupportedWaylandMode: (args) => {
enforceUnsupportedWaylandMode(args);
},
getDefaultSocketPath: () => getDefaultSocketPath(),
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
runGenerateConfigFlow: (args) =>
runGenerateConfigFlowRuntimeService(args, {
shouldStartApp: (nextArgs) => shouldStartApp(nextArgs),
generateConfig: async (nextArgs) =>
generateDefaultConfigFile(nextArgs, {
configDir: CONFIG_DIR,
defaultConfig: DEFAULT_CONFIG,
generateTemplate: (config) => generateConfigTemplate(config as never),
@@ -513,14 +515,14 @@ if (
process.exitCode = 1;
app.quit();
},
})
) {
startAppLifecycleService(initialArgs, createAppLifecycleDepsRuntimeService({
}),
startAppLifecycle: (args) => {
startAppLifecycleService(args, createAppLifecycleDepsRuntimeService({
app,
platform: process.platform,
shouldStartApp: (args) => shouldStartApp(args),
shouldStartApp: (nextArgs) => shouldStartApp(nextArgs),
parseArgs: (argv) => parseArgs(argv),
handleCliCommand: (args, source) => handleCliCommand(args, source),
handleCliCommand: (nextArgs, source) => handleCliCommand(nextArgs, source),
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
onReady: async () => {
@@ -557,8 +559,8 @@ if (
},
getOverlayWindowsCount: () => getOverlayWindows().length,
tokenizeSubtitle: (text) => tokenizeSubtitle(text),
broadcastToOverlayWindows: (channel, ...args) => {
broadcastToOverlayWindows(channel, ...args);
broadcastToOverlayWindows: (channel, ...channelArgs) => {
broadcastToOverlayWindows(channel, ...channelArgs);
},
updateCurrentMediaPath: (mediaPath) => {
updateCurrentMediaPath(mediaPath);
@@ -690,7 +692,15 @@ if (
updateInvisibleOverlayVisibility();
},
}));
}
},
});
const initialArgs = startupState.initialArgs;
let mpvSocketPath = startupState.mpvSocketPath;
let texthookerPort = startupState.texthookerPort;
const backendOverride = startupState.backendOverride;
const autoStartOverlay = startupState.autoStartOverlay;
const texthookerOnlyMode = startupState.texthookerOnlyMode;
function handleCliCommand(
args: CliArgs,