refactor(main): finish TASK-94 composition-root extraction

Move IPC, shortcuts, startup lifecycle, and app-ready assembly behind dedicated runtime composers so main.ts stays focused on boot wiring while preserving behavior and test coverage.
This commit is contained in:
2026-02-20 20:14:39 -08:00
parent 8ad8ff1671
commit 23b88bf20e
13 changed files with 1421 additions and 890 deletions

View File

@@ -1,10 +1,10 @@
--- ---
id: TASK-94 id: TASK-94
title: Reduce main.ts to thin composition root title: Reduce main.ts to thin composition root
status: In Progress status: Done
assignee: [] assignee: []
created_date: '2026-02-20 12:06' created_date: '2026-02-20 12:06'
updated_date: '2026-02-21 03:40' updated_date: '2026-02-21 04:12'
labels: labels:
- architecture - architecture
- refactor - refactor
@@ -44,12 +44,38 @@ priority: high
## Acceptance Criteria ## Acceptance Criteria
<!-- AC:BEGIN --> <!-- AC:BEGIN -->
- [ ] #1 `src/main.ts` is composition-focused (boot/runtime wiring only; no broad deps-builder clusters). - [x] #1 `src/main.ts` is composition-focused (boot/runtime wiring only; no broad deps-builder clusters).
- [x] #2 Runtime import paths in `src/main.ts` stay domain-registry oriented (no relapse to per-leaf runtime imports). - [x] #2 Runtime import paths in `src/main.ts` stay domain-registry oriented (no relapse to per-leaf runtime imports).
- [x] #3 `check:main-fanin` passes under updated threshold. - [x] #3 `check:main-fanin` passes under updated threshold.
- [x] #4 `bun run test:core:dist` passes with no CLI/IPC behavior regressions. - [x] #4 `bun run test:core:dist` passes with no CLI/IPC behavior regressions.
<!-- AC:END --> <!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1) Extract IPC composition from src/main.ts into src/main/runtime/composers/ipc-runtime-composer.ts with focused tests (handler deps build, runtime handler creation, registration wiring).
2) Rewire src/main.ts IPC command and IPC registration blocks to consume the IPC composer while preserving existing helper wrappers and behavior.
3) Extract shortcuts composition from src/main.ts into src/main/runtime/composers/shortcuts-runtime-composer.ts with focused tests (global shortcuts runtime, numeric sessions, overlay shortcuts lifecycle).
4) Rewire src/main.ts shortcut runtime block to consume shortcuts composer while preserving downstream callsites.
5) Run targeted composer tests + check:main-fanin + test:core:dist; update TASK-94 notes with before/after metrics and finalize AC/status if all green.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
2026-02-21: finished extraction pass by moving IPC command/registration, shortcuts runtime wiring, startup lifecycle wiring, and app-ready startup composition from `src/main.ts` into dedicated composer modules under `src/main/runtime/composers/*`.
Metrics: `src/main.ts` reduced from 3043 LOC to 2955 LOC in this pass; `check:main-fanin` import lines improved from 99 to 90 while remaining under enforced threshold.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Completed TASK-94 composition-root finish pass by extracting remaining large assembly clusters from `src/main.ts` into dedicated composer modules: IPC runtime (`ipc-runtime-composer`), shortcuts runtime (`shortcuts-runtime-composer`), startup lifecycle (`startup-lifecycle-composer`), and app-ready bootstrap (`app-ready-composer`). Main process behavior was preserved while reducing direct deps-builder orchestration in `main.ts` and keeping runtime wiring through domain/composer boundaries.
Added focused composer tests for each new module and reran project gates. Verification run: `bun run build`, `bun test src/main/runtime/composers/ipc-runtime-composer.test.ts`, `bun test src/main/runtime/composers/shortcuts-runtime-composer.test.ts`, `bun test src/main/runtime/composers/startup-lifecycle-composer.test.ts`, `bun test src/main/runtime/composers/app-ready-composer.test.ts`, `bun run check:main-fanin`, and `bun run test:core:dist` (all passing).
<!-- SECTION:FINAL_SUMMARY:END -->
## Definition of Done ## Definition of Done
<!-- DOD:BEGIN --> <!-- DOD:BEGIN -->
- [x] #1 `src/main.ts` LOC and fan-in metrics improve from pre-task baseline and are recorded in task notes. - [x] #1 `src/main.ts` LOC and fan-in metrics improve from pre-task baseline and are recorded in task notes.

View File

@@ -30,3 +30,4 @@ Read first. Keep concise.
| `opencode-task95-immersion-tracker-20260221T031846Z-p4k9` | `opencode-task95-immersion-tracker` | `Implement TASK-95 immersion-tracker extraction into focused collaborators and seam tests` | `handoff` | `docs/subagents/agents/opencode-task95-immersion-tracker-20260221T031846Z-p4k9.md` | `2026-02-21T03:26:51Z` | | `opencode-task95-immersion-tracker-20260221T031846Z-p4k9` | `opencode-task95-immersion-tracker` | `Implement TASK-95 immersion-tracker extraction into focused collaborators and seam tests` | `handoff` | `docs/subagents/agents/opencode-task95-immersion-tracker-20260221T031846Z-p4k9.md` | `2026-02-21T03:26:51Z` |
| `opencode-task95-config-20260221T031843Z-m4k9` | `opencode-task95-config` | `Implement TASK-95 config extraction for src/config/service.ts` | `done` | `docs/subagents/agents/opencode-task95-config-20260221T031843Z-m4k9.md` | `2026-02-21T03:26:57Z` | | `opencode-task95-config-20260221T031843Z-m4k9` | `opencode-task95-config` | `Implement TASK-95 config extraction for src/config/service.ts` | `done` | `docs/subagents/agents/opencode-task95-config-20260221T031843Z-m4k9.md` | `2026-02-21T03:26:57Z` |
| `codex-task95-anki-20260221T031836Z-6f3e` | `codex-task95-anki` | `Implement TASK-95 anki-integration extraction for field-grouping merge collaborator` | `done` | `docs/subagents/agents/codex-task95-anki-20260221T031836Z-6f3e.md` | `2026-02-21T03:26:55Z` | | `codex-task95-anki-20260221T031836Z-6f3e` | `codex-task95-anki` | `Implement TASK-95 anki-integration extraction for field-grouping merge collaborator` | `done` | `docs/subagents/agents/codex-task95-anki-20260221T031836Z-6f3e.md` | `2026-02-21T03:26:55Z` |
| `opencode-task-94-20260221T033647Z-7ou2` | `opencode-task-94` | `Finish TASK-94 thin composition root refactor and close acceptance criteria` | `done` | `docs/subagents/agents/opencode-task-94-20260221T033647Z-7ou2.md` | `2026-02-21T04:12:45Z` |

View File

@@ -0,0 +1,48 @@
# Agent Session: opencode-task-94-20260221T033647Z-7ou2
- alias: `opencode-task-94`
- mission: `Finish TASK-94 thin composition root refactor and close acceptance criteria`
- status: `done`
- last_update_utc: `2026-02-21T04:12:45Z`
## Intent
- Pull TASK-94 context from Backlog MCP; verify remaining gap.
- Use writing-plans skill to produce execution plan.
- Use executing-plans skill to implement remaining extraction and verify gates.
## Planned Files
- `src/main.ts`
- `src/main/runtime/composers/*`
- `src/main/runtime/registry/*`
- `src/main/runtime/shared/*`
- `src/main/runtime/**/*.test.ts`
- `backlog/tasks/task-94 - Reduce-main.ts-to-thin-composition-root.md` (via MCP task edits only)
## Assumptions
- TASK-94 already has partial progress from prior slice.
- Remaining work targets acceptance criterion #1 only.
- No commit requested in this run.
## Activity Log
- `2026-02-21T03:36:58Z` session start; backlog/task context loaded; preparing planning skill.
- `2026-02-21T03:47:00Z` extracted IPC + shortcuts composition into dedicated composers; rewired `src/main.ts`; added composer tests.
- `2026-02-21T04:05:00Z` extracted startup-lifecycle + app-ready composition clusters into composer modules; rewired `src/main.ts` lifecycle/app-ready assembly.
- `2026-02-21T04:11:57Z` verification complete: `bun run build`, composer tests, `bun run check:main-fanin`, `bun run test:core:dist` all passing.
- `2026-02-21T04:12:45Z` TASK-94 updated via Backlog MCP: AC #1 checked, final summary captured, status set to Done.
## Touched Files
- `src/main.ts`
- `src/main/runtime/composers/ipc-runtime-composer.ts`
- `src/main/runtime/composers/ipc-runtime-composer.test.ts`
- `src/main/runtime/composers/shortcuts-runtime-composer.ts`
- `src/main/runtime/composers/shortcuts-runtime-composer.test.ts`
- `src/main/runtime/composers/startup-lifecycle-composer.ts`
- `src/main/runtime/composers/startup-lifecycle-composer.test.ts`
- `src/main/runtime/composers/app-ready-composer.ts`
- `src/main/runtime/composers/app-ready-composer.test.ts`
- `docs/plans/2026-02-21-task-94-thin-composition-root-finish.md`

View File

@@ -26,3 +26,6 @@ Shared notes. Append-only.
- [2026-02-21T03:18:46Z] [opencode-task95-immersion-tracker-20260221T031846Z-p4k9|opencode-task95-immersion-tracker] overlap note: implementing TASK-95 immersion-tracker slice in `src/core/services/immersion-tracker-service.ts` + new `src/core/services/immersion-tracker/*` + seam tests; avoiding backlog file edits. - [2026-02-21T03:18:46Z] [opencode-task95-immersion-tracker-20260221T031846Z-p4k9|opencode-task95-immersion-tracker] overlap note: implementing TASK-95 immersion-tracker slice in `src/core/services/immersion-tracker-service.ts` + new `src/core/services/immersion-tracker/*` + seam tests; avoiding backlog file edits.
- [2026-02-21T03:26:51Z] [opencode-task95-immersion-tracker-20260221T031846Z-p4k9|opencode-task95-immersion-tracker] completed immersion-tracker slice: extracted reducer/query/maintenance/queue/types collaborators, kept public API stable, added seam tests, and verified via `bun run build && node --test dist/core/services/immersion-tracker-service.test.js`. - [2026-02-21T03:26:51Z] [opencode-task95-immersion-tracker-20260221T031846Z-p4k9|opencode-task95-immersion-tracker] completed immersion-tracker slice: extracted reducer/query/maintenance/queue/types collaborators, kept public API stable, added seam tests, and verified via `bun run build && node --test dist/core/services/immersion-tracker-service.test.js`.
- [2026-02-21T03:26:57Z] [opencode-task95-config-20260221T031843Z-m4k9|opencode-task95-config] completed config slice: extracted `load/parse/warnings/resolve` collaborators, reduced `src/config/service.ts` to facade, added loader precedence + strict non-mutation + warning determinism seam tests, build+config tests green. - [2026-02-21T03:26:57Z] [opencode-task95-config-20260221T031843Z-m4k9|opencode-task95-config] completed config slice: extracted `load/parse/warnings/resolve` collaborators, reduced `src/config/service.ts` to facade, added loader precedence + strict non-mutation + warning determinism seam tests, build+config tests green.
- [2026-02-21T03:36:58Z] [opencode-task-94-20260221T033647Z-7ou2|opencode-task-94] starting TASK-94 finish pass: pull backlog context, write+execute plan via writing-plans/executing-plans, and close remaining AC without commit.
- [2026-02-21T04:11:57Z] [opencode-task-94-20260221T033647Z-7ou2|opencode-task-94] extracted IPC/shortcuts/startup-lifecycle/app-ready clusters behind composer modules, rewired `src/main.ts`, added focused composer tests, and revalidated build + `check:main-fanin` + `test:core:dist`.
- [2026-02-21T04:12:45Z] [opencode-task-94-20260221T033647Z-7ou2|opencode-task-94] TASK-94 finalized in Backlog MCP: AC checklist complete, notes+final summary recorded, status moved to Done.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeAppReadyRuntime } from './app-ready-composer';
test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => {
const composed = composeAppReadyRuntime({
reloadConfigMainDeps: {
reloadConfigStrict: () => ({ config: {} as never, warnings: [] }),
logInfo: () => {},
logWarning: () => {},
showDesktopNotification: () => {},
startConfigHotReload: () => {},
refreshAnilistClientSecretState: async () => {},
failHandlers: {
logError: () => {},
showErrorBox: () => {},
quit: () => {},
},
},
criticalConfigErrorMainDeps: {
getConfigPath: () => '/tmp/config.jsonc',
failHandlers: {
logError: () => {},
showErrorBox: () => {},
quit: () => {},
},
},
appReadyRuntimeMainDeps: {
loadSubtitlePosition: () => {},
resolveKeybindings: () => {},
createMpvClient: () => {},
getResolvedConfig: () => ({}) as never,
getConfigWarnings: () => [],
logConfigWarning: () => {},
setLogLevel: () => {},
initRuntimeOptionsManager: () => {},
setSecondarySubMode: () => {},
defaultSecondarySubMode: 'hover',
defaultWebsocketPort: 5174,
hasMpvWebsocketPlugin: () => false,
startSubtitleWebsocket: () => {},
log: () => {},
createMecabTokenizerAndCheck: async () => {},
createSubtitleTimingTracker: () => {},
loadYomitanExtension: async () => {},
startJellyfinRemoteSession: async () => {},
prewarmSubtitleDictionaries: async () => {},
startBackgroundWarmups: () => {},
texthookerOnlyMode: false,
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
initializeOverlayRuntime: () => {},
handleInitialArgs: () => {},
logDebug: () => {},
now: () => Date.now(),
},
immersionTrackerStartupMainDeps: {
getResolvedConfig: () => ({}) as never,
getConfiguredDbPath: () => '/tmp/immersion.sqlite',
createTrackerService: () =>
({
startSession: () => {},
}) as never,
setTracker: () => {},
getMpvClient: () => null,
seedTrackerFromCurrentMedia: () => {},
logInfo: () => {},
logDebug: () => {},
logWarn: () => {},
},
});
assert.equal(typeof composed.reloadConfig, 'function');
assert.equal(typeof composed.criticalConfigError, 'function');
assert.equal(typeof composed.appReadyRuntimeRunner, 'function');
});

View File

@@ -0,0 +1,58 @@
import { createAppReadyRuntimeRunner } from '../../app-lifecycle';
import { createBuildAppReadyRuntimeMainDepsHandler } from '../app-ready-main-deps';
import {
createBuildCriticalConfigErrorMainDepsHandler,
createBuildReloadConfigMainDepsHandler,
} from '../startup-config-main-deps';
import { createCriticalConfigErrorHandler, createReloadConfigHandler } from '../startup-config';
import { createBuildImmersionTrackerStartupMainDepsHandler } from '../immersion-startup-main-deps';
import { createImmersionTrackerStartupHandler } from '../immersion-startup';
type ReloadConfigMainDeps = Parameters<typeof createBuildReloadConfigMainDepsHandler>[0];
type CriticalConfigErrorMainDeps = Parameters<
typeof createBuildCriticalConfigErrorMainDepsHandler
>[0];
type AppReadyRuntimeMainDeps = Parameters<typeof createBuildAppReadyRuntimeMainDepsHandler>[0];
export type AppReadyComposerOptions = {
reloadConfigMainDeps: ReloadConfigMainDeps;
criticalConfigErrorMainDeps: CriticalConfigErrorMainDeps;
appReadyRuntimeMainDeps: Omit<AppReadyRuntimeMainDeps, 'reloadConfig' | 'onCriticalConfigErrors'>;
immersionTrackerStartupMainDeps: Parameters<
typeof createBuildImmersionTrackerStartupMainDepsHandler
>[0];
};
export type AppReadyComposerResult = {
reloadConfig: ReturnType<typeof createReloadConfigHandler>;
criticalConfigError: ReturnType<typeof createCriticalConfigErrorHandler>;
appReadyRuntimeRunner: ReturnType<typeof createAppReadyRuntimeRunner>;
};
export function composeAppReadyRuntime(options: AppReadyComposerOptions): AppReadyComposerResult {
const reloadConfig = createReloadConfigHandler(
createBuildReloadConfigMainDepsHandler(options.reloadConfigMainDeps)(),
);
const criticalConfigError = createCriticalConfigErrorHandler(
createBuildCriticalConfigErrorMainDepsHandler(options.criticalConfigErrorMainDeps)(),
);
const appReadyRuntimeRunner = createAppReadyRuntimeRunner(
createBuildAppReadyRuntimeMainDepsHandler({
...options.appReadyRuntimeMainDeps,
reloadConfig,
createImmersionTracker: createImmersionTrackerStartupHandler(
createBuildImmersionTrackerStartupMainDepsHandler(
options.immersionTrackerStartupMainDeps,
)(),
),
onCriticalConfigErrors: criticalConfigError,
})(),
);
return {
reloadConfig,
criticalConfigError,
appReadyRuntimeRunner,
};
}

View File

@@ -0,0 +1,97 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeIpcRuntimeHandlers } from './ipc-runtime-composer';
test('composeIpcRuntimeHandlers returns callable IPC handlers and registration bridge', async () => {
let registered = false;
const composed = composeIpcRuntimeHandlers<{ value: number }, { ok: boolean; received: number }>({
mpvCommandMainDeps: {
triggerSubsyncFromConfig: async () => {},
openRuntimeOptionsPalette: () => {},
cycleRuntimeOption: () => ({ ok: true }),
showMpvOsd: () => {},
replayCurrentSubtitle: () => {},
playNextSubtitle: () => {},
sendMpvCommand: () => {},
isMpvConnected: () => false,
hasRuntimeOptionsManager: () => true,
},
handleMpvCommandFromIpcRuntime: () => {},
runSubsyncManualFromIpc: async (request) => ({ ok: true, received: request.value }),
registration: {
runtimeOptions: {
getRuntimeOptionsManager: () => null,
showMpvOsd: () => {},
},
mainDeps: {
getInvisibleWindow: () => null,
getMainWindow: () => null,
getVisibleOverlayVisibility: () => false,
getInvisibleOverlayVisibility: () => false,
focusMainWindow: () => {},
onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => ({}) as never,
getSubtitlePosition: () => ({}) as never,
getSubtitleStyle: () => ({}) as never,
saveSubtitlePosition: () => {},
getMecabTokenizer: () => null,
getKeybindings: () => [],
getConfiguredShortcuts: () => ({}) as never,
getSecondarySubMode: () => 'hover' as never,
getMpvClient: () => null,
getAnkiConnectStatus: () => false,
getRuntimeOptions: () => [],
reportOverlayContentBounds: () => {},
getAnilistStatus: () => ({}) as never,
clearAnilistToken: () => {},
openAnilistSetup: () => {},
getAnilistQueueStatus: () => ({}) as never,
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
},
ankiJimakuDeps: {
patchAnkiConnectEnabled: () => {},
getResolvedConfig: () => ({}) as never,
getRuntimeOptionsManager: () => null,
getSubtitleTimingTracker: () => null,
getMpvClient: () => null,
getAnkiIntegration: () => null,
setAnkiIntegration: () => {},
getKnownWordCacheStatePath: () => '',
showDesktopNotification: () => {},
createFieldGroupingCallback: () => (() => {}) as never,
broadcastRuntimeOptionsChanged: () => {},
getFieldGroupingResolver: () => null,
setFieldGroupingResolver: () => {},
parseMediaInfo: () => ({}) as never,
getCurrentMediaPath: () => null,
jimakuFetchJson: async () => ({ data: null }) as never,
getJimakuMaxEntryResults: () => 0,
getJimakuLanguagePreference: () => 'ja' as never,
resolveJimakuApiKey: async () => null,
isRemoteMediaPath: () => false,
downloadToFile: async () => ({ ok: true, path: '/tmp/file' }),
},
registerIpcRuntimeServices: () => {
registered = true;
},
},
});
assert.equal(typeof composed.handleMpvCommandFromIpc, 'function');
assert.equal(typeof composed.runSubsyncManualFromIpc, 'function');
assert.equal(typeof composed.registerIpcRuntimeHandlers, 'function');
const result = await composed.runSubsyncManualFromIpc({ value: 7 });
assert.deepEqual(result, { ok: true, received: 7 });
composed.registerIpcRuntimeHandlers();
assert.equal(registered, true);
});

View File

@@ -0,0 +1,75 @@
import type { RegisterIpcRuntimeServicesParams } from '../../ipc-runtime';
import {
createBuildMpvCommandFromIpcRuntimeMainDepsHandler,
createIpcRuntimeHandlers,
} from '../domains/ipc';
type MpvCommand = (string | number)[];
type IpcMainDepsWithoutHandlers = Omit<
RegisterIpcRuntimeServicesParams['mainDeps'],
'handleMpvCommand' | 'runSubsyncManual'
>;
type IpcRuntimeDeps<TRequest, TResult> = Parameters<
typeof createIpcRuntimeHandlers<TRequest, TResult>
>[0];
export type IpcRuntimeComposerOptions<TRequest, TResult> = {
mpvCommandMainDeps: Parameters<typeof createBuildMpvCommandFromIpcRuntimeMainDepsHandler>[0];
handleMpvCommandFromIpcRuntime: IpcRuntimeDeps<
TRequest,
TResult
>['handleMpvCommandFromIpcDeps']['handleMpvCommandFromIpcRuntime'];
runSubsyncManualFromIpc: (request: TRequest) => Promise<TResult>;
registration: {
runtimeOptions: RegisterIpcRuntimeServicesParams['runtimeOptions'];
mainDeps: IpcMainDepsWithoutHandlers;
ankiJimakuDeps: RegisterIpcRuntimeServicesParams['ankiJimakuDeps'];
registerIpcRuntimeServices: (params: RegisterIpcRuntimeServicesParams) => void;
};
};
export type IpcRuntimeComposerResult<TRequest, TResult> = {
handleMpvCommandFromIpc: (command: MpvCommand) => void;
runSubsyncManualFromIpc: (request: TRequest) => Promise<TResult>;
registerIpcRuntimeHandlers: () => void;
};
export function composeIpcRuntimeHandlers<TRequest, TResult>(
options: IpcRuntimeComposerOptions<TRequest, TResult>,
): IpcRuntimeComposerResult<TRequest, TResult> {
const mpvCommandFromIpcRuntimeMainDeps = createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
options.mpvCommandMainDeps,
)();
const { handleMpvCommandFromIpc, runSubsyncManualFromIpc } = createIpcRuntimeHandlers<
TRequest,
TResult
>({
handleMpvCommandFromIpcDeps: {
handleMpvCommandFromIpcRuntime: options.handleMpvCommandFromIpcRuntime,
buildMpvCommandDeps: () => mpvCommandFromIpcRuntimeMainDeps,
},
runSubsyncManualFromIpcDeps: {
runManualFromIpc: (request) => options.runSubsyncManualFromIpc(request),
},
});
const registerIpcRuntimeHandlers = (): void => {
options.registration.registerIpcRuntimeServices({
runtimeOptions: options.registration.runtimeOptions,
mainDeps: {
...options.registration.mainDeps,
handleMpvCommand: (command) => handleMpvCommandFromIpc(command),
runSubsyncManual: (request) => runSubsyncManualFromIpc(request as TRequest),
},
ankiJimakuDeps: options.registration.ankiJimakuDeps,
});
};
return {
handleMpvCommandFromIpc: (command) => handleMpvCommandFromIpc(command),
runSubsyncManualFromIpc: (request) => runSubsyncManualFromIpc(request),
registerIpcRuntimeHandlers,
};
}

View File

@@ -0,0 +1,62 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeShortcutRuntimes } from './shortcuts-runtime-composer';
test('composeShortcutRuntimes returns callable shortcut runtime handlers', () => {
const composed = composeShortcutRuntimes({
globalShortcuts: {
getConfiguredShortcutsMainDeps: {
getResolvedConfig: () => ({}) as never,
defaultConfig: {} as never,
resolveConfiguredShortcuts: () => ({}) as never,
},
buildRegisterGlobalShortcutsMainDeps: () => ({
getConfiguredShortcuts: () => ({}) as never,
registerGlobalShortcutsCore: () => {},
toggleVisibleOverlay: () => {},
toggleInvisibleOverlay: () => {},
openYomitanSettings: () => {},
isDev: false,
getMainWindow: () => null,
}),
buildRefreshGlobalAndOverlayShortcutsMainDeps: () => ({
unregisterAllGlobalShortcuts: () => {},
registerGlobalShortcuts: () => {},
syncOverlayShortcuts: () => {},
}),
},
numericShortcutRuntimeMainDeps: {
globalShortcut: {
register: () => true,
unregister: () => {},
},
showMpvOsd: () => {},
setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs),
clearTimer: (timer) => clearTimeout(timer),
},
numericSessions: {
onMultiCopyDigit: () => {},
onMineSentenceDigit: () => {},
},
overlayShortcutsRuntimeMainDeps: {
overlayShortcutsRuntime: {
registerOverlayShortcuts: () => {},
unregisterOverlayShortcuts: () => {},
syncOverlayShortcuts: () => {},
refreshOverlayShortcuts: () => {},
},
},
});
assert.equal(typeof composed.getConfiguredShortcuts, 'function');
assert.equal(typeof composed.registerGlobalShortcuts, 'function');
assert.equal(typeof composed.refreshGlobalAndOverlayShortcuts, 'function');
assert.equal(typeof composed.cancelPendingMultiCopy, 'function');
assert.equal(typeof composed.startPendingMultiCopy, 'function');
assert.equal(typeof composed.cancelPendingMineSentenceMultiple, 'function');
assert.equal(typeof composed.startPendingMineSentenceMultiple, 'function');
assert.equal(typeof composed.registerOverlayShortcuts, 'function');
assert.equal(typeof composed.unregisterOverlayShortcuts, 'function');
assert.equal(typeof composed.syncOverlayShortcuts, 'function');
assert.equal(typeof composed.refreshOverlayShortcuts, 'function');
});

View File

@@ -0,0 +1,59 @@
import { createNumericShortcutRuntime } from '../../../core/services/numeric-shortcut';
import {
createBuildNumericShortcutRuntimeMainDepsHandler,
createGlobalShortcutsRuntimeHandlers,
createNumericShortcutSessionRuntimeHandlers,
createOverlayShortcutsRuntimeHandlers,
} from '../domains/shortcuts';
type GlobalShortcutsOptions = Parameters<typeof createGlobalShortcutsRuntimeHandlers>[0];
type NumericShortcutRuntimeMainDeps = Parameters<
typeof createBuildNumericShortcutRuntimeMainDepsHandler
>[0];
type NumericSessionOptions = Omit<
Parameters<typeof createNumericShortcutSessionRuntimeHandlers>[0],
'multiCopySession' | 'mineSentenceSession'
>;
type OverlayShortcutsMainDeps = Parameters<
typeof createOverlayShortcutsRuntimeHandlers
>[0]['overlayShortcutsRuntimeMainDeps'];
export type ShortcutsRuntimeComposerOptions = {
globalShortcuts: GlobalShortcutsOptions;
numericShortcutRuntimeMainDeps: NumericShortcutRuntimeMainDeps;
numericSessions: NumericSessionOptions;
overlayShortcutsRuntimeMainDeps: OverlayShortcutsMainDeps;
};
export type ShortcutsRuntimeComposerResult = ReturnType<
typeof createGlobalShortcutsRuntimeHandlers
> &
ReturnType<typeof createNumericShortcutSessionRuntimeHandlers> &
ReturnType<typeof createOverlayShortcutsRuntimeHandlers>;
export function composeShortcutRuntimes(
options: ShortcutsRuntimeComposerOptions,
): ShortcutsRuntimeComposerResult {
const globalShortcuts = createGlobalShortcutsRuntimeHandlers(options.globalShortcuts);
const numericShortcutRuntimeMainDeps = createBuildNumericShortcutRuntimeMainDepsHandler(
options.numericShortcutRuntimeMainDeps,
)();
const numericShortcutRuntime = createNumericShortcutRuntime(numericShortcutRuntimeMainDeps);
const numericSessions = createNumericShortcutSessionRuntimeHandlers({
multiCopySession: numericShortcutRuntime.createSession(),
mineSentenceSession: numericShortcutRuntime.createSession(),
onMultiCopyDigit: options.numericSessions.onMultiCopyDigit,
onMineSentenceDigit: options.numericSessions.onMineSentenceDigit,
});
const overlayShortcuts = createOverlayShortcutsRuntimeHandlers({
overlayShortcutsRuntimeMainDeps: options.overlayShortcutsRuntimeMainDeps,
});
return {
...globalShortcuts,
...numericSessions,
...overlayShortcuts,
};
}

View File

@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeStartupLifecycleHandlers } from './startup-lifecycle-composer';
test('composeStartupLifecycleHandlers returns callable startup lifecycle handlers', () => {
const composed = composeStartupLifecycleHandlers({
registerProtocolUrlHandlersMainDeps: {
registerOpenUrl: () => {},
registerSecondInstance: () => {},
handleAnilistSetupProtocolUrl: () => false,
findAnilistSetupDeepLinkArgvUrl: () => null,
logUnhandledOpenUrl: () => {},
logUnhandledSecondInstanceUrl: () => {},
},
onWillQuitCleanupMainDeps: {
destroyTray: () => {},
stopConfigHotReload: () => {},
restorePreviousSecondarySubVisibility: () => {},
unregisterAllGlobalShortcuts: () => {},
stopSubtitleWebsocket: () => {},
stopTexthookerService: () => {},
getYomitanParserWindow: () => null,
clearYomitanParserState: () => {},
getWindowTracker: () => null,
getMpvSocket: () => null,
getReconnectTimer: () => null,
clearReconnectTimerRef: () => {},
getSubtitleTimingTracker: () => null,
getImmersionTracker: () => null,
clearImmersionTracker: () => {},
getAnkiIntegration: () => null,
getAnilistSetupWindow: () => null,
clearAnilistSetupWindow: () => {},
getJellyfinSetupWindow: () => null,
clearJellyfinSetupWindow: () => {},
stopJellyfinRemoteSession: async () => {},
},
shouldRestoreWindowsOnActivateMainDeps: {
isOverlayRuntimeInitialized: () => false,
getAllWindowCount: () => 0,
},
restoreWindowsOnActivateMainDeps: {
createMainWindow: () => {},
createInvisibleWindow: () => {},
updateVisibleOverlayVisibility: () => {},
updateInvisibleOverlayVisibility: () => {},
},
});
assert.equal(typeof composed.registerProtocolUrlHandlers, 'function');
assert.equal(typeof composed.onWillQuitCleanup, 'function');
assert.equal(typeof composed.shouldRestoreWindowsOnActivate, 'function');
assert.equal(typeof composed.restoreWindowsOnActivate, 'function');
});

View File

@@ -0,0 +1,65 @@
import {
createOnWillQuitCleanupHandler,
createRestoreWindowsOnActivateHandler,
createShouldRestoreWindowsOnActivateHandler,
} from '../app-lifecycle-actions';
import { createBuildOnWillQuitCleanupDepsHandler } from '../app-lifecycle-main-cleanup';
import {
createBuildRestoreWindowsOnActivateMainDepsHandler,
createBuildShouldRestoreWindowsOnActivateMainDepsHandler,
} from '../app-lifecycle-main-activate';
import { createBuildRegisterProtocolUrlHandlersMainDepsHandler } from '../protocol-url-handlers-main-deps';
import { registerProtocolUrlHandlers } from '../protocol-url-handlers';
type RegisterProtocolUrlHandlersMainDeps = Parameters<
typeof createBuildRegisterProtocolUrlHandlersMainDepsHandler
>[0];
type OnWillQuitCleanupDeps = Parameters<typeof createBuildOnWillQuitCleanupDepsHandler>[0];
type ShouldRestoreWindowsOnActivateMainDeps = Parameters<
typeof createBuildShouldRestoreWindowsOnActivateMainDepsHandler
>[0];
type RestoreWindowsOnActivateMainDeps = Parameters<
typeof createBuildRestoreWindowsOnActivateMainDepsHandler
>[0];
export type StartupLifecycleComposerOptions = {
registerProtocolUrlHandlersMainDeps: RegisterProtocolUrlHandlersMainDeps;
onWillQuitCleanupMainDeps: OnWillQuitCleanupDeps;
shouldRestoreWindowsOnActivateMainDeps: ShouldRestoreWindowsOnActivateMainDeps;
restoreWindowsOnActivateMainDeps: RestoreWindowsOnActivateMainDeps;
};
export type StartupLifecycleComposerResult = {
registerProtocolUrlHandlers: () => void;
onWillQuitCleanup: () => void;
shouldRestoreWindowsOnActivate: () => boolean;
restoreWindowsOnActivate: () => void;
};
export function composeStartupLifecycleHandlers(
options: StartupLifecycleComposerOptions,
): StartupLifecycleComposerResult {
const registerProtocolUrlHandlersMainDeps = createBuildRegisterProtocolUrlHandlersMainDepsHandler(
options.registerProtocolUrlHandlersMainDeps,
)();
const onWillQuitCleanupHandler = createOnWillQuitCleanupHandler(
createBuildOnWillQuitCleanupDepsHandler(options.onWillQuitCleanupMainDeps)(),
);
const shouldRestoreWindowsOnActivateHandler = createShouldRestoreWindowsOnActivateHandler(
createBuildShouldRestoreWindowsOnActivateMainDepsHandler(
options.shouldRestoreWindowsOnActivateMainDeps,
)(),
);
const restoreWindowsOnActivateHandler = createRestoreWindowsOnActivateHandler(
createBuildRestoreWindowsOnActivateMainDepsHandler(options.restoreWindowsOnActivateMainDeps)(),
);
return {
registerProtocolUrlHandlers: () =>
registerProtocolUrlHandlers(registerProtocolUrlHandlersMainDeps),
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
};
}