mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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` |
|
||||||
|
|||||||
@@ -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`
|
||||||
@@ -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.
|
||||||
|
|||||||
1682
src/main.ts
1682
src/main.ts
File diff suppressed because it is too large
Load Diff
75
src/main/runtime/composers/app-ready-composer.test.ts
Normal file
75
src/main/runtime/composers/app-ready-composer.test.ts
Normal 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');
|
||||||
|
});
|
||||||
58
src/main/runtime/composers/app-ready-composer.ts
Normal file
58
src/main/runtime/composers/app-ready-composer.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
97
src/main/runtime/composers/ipc-runtime-composer.test.ts
Normal file
97
src/main/runtime/composers/ipc-runtime-composer.test.ts
Normal 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);
|
||||||
|
});
|
||||||
75
src/main/runtime/composers/ipc-runtime-composer.ts
Normal file
75
src/main/runtime/composers/ipc-runtime-composer.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
59
src/main/runtime/composers/shortcuts-runtime-composer.ts
Normal file
59
src/main/runtime/composers/shortcuts-runtime-composer.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
65
src/main/runtime/composers/startup-lifecycle-composer.ts
Normal file
65
src/main/runtime/composers/startup-lifecycle-composer.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user