From 8ad8ff16715eca170dd67c976c203387b794eae8 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 20 Feb 2026 19:33:44 -0800 Subject: [PATCH] refactor(main): extract jellyfin and anilist runtime composers --- ....ts-into-domain-runtime-modules-round-2.md | 14 +- ...Reduce-main.ts-to-thin-composition-root.md | 58 +++ docs/file-size-budgets.md | 4 + docs/subagents/INDEX.md | 56 +-- ...-task94-thin-root-20260221T031320Z-q3n7.md | 40 ++ docs/subagents/collaboration.md | 27 +- scripts/check-main-runtime-fanin.ts | 103 +++++ src/main.ts | 354 +++++++----------- .../composers/anilist-setup-composer.test.ts | 40 ++ .../composers/anilist-setup-composer.ts | 55 +++ .../jellyfin-remote-composer.test.ts | 34 ++ .../composers/jellyfin-remote-composer.ts | 124 ++++++ 12 files changed, 643 insertions(+), 266 deletions(-) create mode 100644 backlog/tasks/task-94 - Reduce-main.ts-to-thin-composition-root.md create mode 100644 docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md create mode 100644 scripts/check-main-runtime-fanin.ts create mode 100644 src/main/runtime/composers/anilist-setup-composer.test.ts create mode 100644 src/main/runtime/composers/anilist-setup-composer.ts create mode 100644 src/main/runtime/composers/jellyfin-remote-composer.test.ts create mode 100644 src/main/runtime/composers/jellyfin-remote-composer.ts diff --git a/backlog/tasks/task-71 - Split-main.ts-into-domain-runtime-modules-round-2.md b/backlog/tasks/task-71 - Split-main.ts-into-domain-runtime-modules-round-2.md index 16f9a41..08ab5d3 100644 --- a/backlog/tasks/task-71 - Split-main.ts-into-domain-runtime-modules-round-2.md +++ b/backlog/tasks/task-71 - Split-main.ts-into-domain-runtime-modules-round-2.md @@ -1,10 +1,10 @@ --- id: TASK-71 title: Split main.ts into domain runtime modules round 2 -status: To Do +status: In Progress assignee: [] created_date: '2026-02-18 11:35' -updated_date: '2026-02-18 11:35' +updated_date: '2026-02-21 03:40' labels: - architecture - refactor @@ -19,6 +19,15 @@ priority: high `src/main.ts` remains >3k LOC and still mixes composition, runtime state mutation, domain workflows (Jellyfin, AniList), tray/UI setup, and IPC wiring. This creates high cognitive load and broad change blast radius. +## Progress Notes + +- Added runtime domain barrels under `src/main/runtime/domains/*` and composed registry entrypoint at `src/main/runtime/registry.ts`. +- Migrated `src/main.ts` runtime imports from per-file runtime modules to domain barrel import paths. +- Added `scripts/check-main-runtime-fanin.ts` guardrail and package scripts (`check:main-fanin`, `check:main-fanin:strict`). +- 2026-02-21: extracted jellyfin/anilist deps-builder clusters into composer modules (`src/main/runtime/composers/jellyfin-remote-composer.ts`, `src/main/runtime/composers/anilist-setup-composer.ts`) and replaced inline `main.ts` orchestration with composer invocations. +- 2026-02-21: tightened fan-in guard defaults to `import lines <= 110` and `unique runtime paths <= 11`; strict mode passing. +- Remaining TASK-71 scope: finish startup/overlay/ipc/shortcuts composer extraction to fully thin `src/main.ts`. + ## Action Steps @@ -47,4 +56,3 @@ priority: high - [ ] #1 `bun run test:fast` passes - [ ] #2 Architecture docs updated - diff --git a/backlog/tasks/task-94 - Reduce-main.ts-to-thin-composition-root.md b/backlog/tasks/task-94 - Reduce-main.ts-to-thin-composition-root.md new file mode 100644 index 0000000..7dd9b85 --- /dev/null +++ b/backlog/tasks/task-94 - Reduce-main.ts-to-thin-composition-root.md @@ -0,0 +1,58 @@ +--- +id: TASK-94 +title: Reduce main.ts to thin composition root +status: In Progress +assignee: [] +created_date: '2026-02-20 12:06' +updated_date: '2026-02-21 03:40' +labels: + - architecture + - refactor + - maintainability +dependencies: + - TASK-71 + - TASK-85 +priority: high +--- + +## Description + + +`src/main.ts` still contains heavy deps-builder orchestration and high import concentration. Complete the composition-root refactor so `main.ts` owns boot wiring only and domain assembly moves behind registry/domain composer modules. + + +## Action Steps + + +1. Baseline current `main.ts` fan-in (`check:main-fanin`, `check:file-budgets`, `wc -l src/main.ts`). +2. Introduce domain composer modules for repeated build-handler clusters (startup, overlay, jellyfin, anilist, ipc/shortcuts). +3. Move `createBuild*MainDepsHandler` call clusters out of `main.ts` into domain composer modules with narrow typed inputs. +4. Replace inline assembly in `main.ts` with single-call composer invocations per domain. +5. Add/adjust unit tests for new composer modules to lock shape and wiring. +6. Tighten `check:main-fanin` thresholds after refactor to prevent regression. +7. Re-run full config/core test gates and update `TASK-71` progress. + + +## Progress Notes + +- 2026-02-21 baseline: `src/main.ts` = 3129 LOC; `check:main-fanin` = 105 import lines / 9 unique runtime paths. +- 2026-02-21 extraction slice: added composer modules `src/main/runtime/composers/jellyfin-remote-composer.ts` and `src/main/runtime/composers/anilist-setup-composer.ts`; moved corresponding `createBuild*MainDepsHandler` orchestration out of `main.ts`. +- 2026-02-21 tests: added focused composer shape tests at `src/main/runtime/composers/jellyfin-remote-composer.test.ts` and `src/main/runtime/composers/anilist-setup-composer.test.ts`. +- 2026-02-21 guardrail tighten: `scripts/check-main-runtime-fanin.ts` defaults tightened to `import lines <= 110` and `unique runtime paths <= 11`. +- 2026-02-21 post-slice metrics: `src/main.ts` = 3042 LOC; `check:main-fanin --strict` = 103 import lines / 11 unique runtime paths. +- Remaining gap: startup/overlay/ipc/shortcuts composer extraction still required to fully meet thin composition-root target. + +## Acceptance Criteria + +- [ ] #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] #3 `check:main-fanin` passes under updated threshold. +- [x] #4 `bun run test:core:dist` passes with no CLI/IPC behavior regressions. + + +## Definition of Done + +- [x] #1 `src/main.ts` LOC and fan-in metrics improve from pre-task baseline and are recorded in task notes. +- [x] #2 New composer modules are covered by focused tests. +- [x] #3 `TASK-71` and `TASK-85` progress notes reflect completed slice and remaining gap. + diff --git a/docs/file-size-budgets.md b/docs/file-size-budgets.md index b8e5d0b..7e4f71a 100644 --- a/docs/file-size-budgets.md +++ b/docs/file-size-budgets.md @@ -13,9 +13,13 @@ Purpose: keep large modules from becoming maintenance bottlenecks. - Warning mode (non-blocking): `bun run check:file-budgets` - Strict mode (CI/local gate): `bun run check:file-budgets:strict` - Custom limit: `bun run scripts/check-file-budgets.ts --limit 650` +- Main runtime fan-in (warning): `bun run check:main-fanin` +- Main runtime fan-in (strict): `bun run check:main-fanin:strict` +- Default main fan-in thresholds: `import lines <= 110`, `unique runtime paths <= 11` ## Policy - If file exceeds budget, prefer extracting domain module(s) first. - Keep composition/orchestration files focused on wiring. - Do not hand-edit generated artifacts; refactor source modules. +- Keep `src/main.ts` runtime import fan-in low by routing domain imports through `src/main/runtime/domains/*` and `src/main/runtime/registry.ts`. diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md index d6f8e65..2bae271 100644 --- a/docs/subagents/INDEX.md +++ b/docs/subagents/INDEX.md @@ -2,31 +2,31 @@ Read first. Keep concise. -| agent_id | alias | mission | status | file | last_update_utc | -| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- | -| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` | -| `codex-frequency-dup-log-20260221T042815Z-r4k1` | `codex-frequency-dup-log` | `Reduce frequency dictionary duplicate-term startup log spam` | `completed` | `docs/subagents/agents/codex-frequency-dup-log-20260221T042815Z-r4k1.md` | `2026-02-21T04:32:40Z` | -| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | -| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` | -| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` | -| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` | -| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` | -| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` | -| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` | -| `codex-texthooker-color-ws-20260220T005844Z-r7m2` | `codex-texthooker-color-ws` | `Fix texthooker websocket payload so token highlight colors render` | `completed` | `docs/subagents/agents/codex-texthooker-color-ws-20260220T005844Z-r7m2.md` | `2026-02-20T01:01:00Z` | -| `codex-nplusone-pos1-20260220T012300Z-c5he` | `codex-nplusone-pos1` | `Fix N+1 false-negative when Yomitan functional tokens inflate unknown candidate count` | `completed` | `docs/subagents/agents/codex-nplusone-pos1-20260220T012300Z-c5he.md` | `2026-02-20T01:28:20Z` | -| `codex-subtitle-bg-20260220T054247Z-h9cu` | `codex-subtitle-bg` | `Update default subtitle background color to requested RGBA value` | `completed` | `docs/subagents/agents/codex-subtitle-bg-20260220T054247Z-h9cu.md` | `2026-02-20T05:44:45Z` | -| `codex-narrow-space-tokenizer-20260220T061716Z-p97s` | `codex-narrow-space-tokenizer` | `Fix tokenization when subtitle line contains narrow/invisible Unicode spacing between segments` | `completed` | `docs/subagents/agents/codex-narrow-space-tokenizer-20260220T061716Z-p97s.md` | `2026-02-20T06:20:07Z` | -| `codex-preserve-linebreaks-20260220T063538Z-s4nd` | `codex-preserve-linebreaks` | `Add config option to preserve subtitle line breaks in visible overlay rendering` | `completed` | `docs/subagents/agents/codex-preserve-linebreaks-20260220T063538Z-s4nd.md` | `2026-02-20T06:42:51Z` | -| `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` | -| `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` | -| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` | -| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Move Jellyfin token/userId out of config into env override + stored session payload` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-21T04:27:24Z` | -| `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` | -| `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` | -| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-20T11:48:28Z` | -| `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` | -| `codex-overlay-whitespace-newline-20260221T040705Z-aw2j` | `codex-overlay-whitespace-newline` | `Fix visible overlay whitespace/newline token rendering bug with TDD regression coverage` | `completed` | `docs/subagents/agents/codex-overlay-whitespace-newline-20260221T040705Z-aw2j.md` | `2026-02-21T04:18:16Z` | -| `codex-duplicate-kiku-20260221T043006Z-5vkz` | `codex-duplicate-kiku` | `Fix Kiku duplicate-card detection/grouping regression for Yomitan duplicate-marked + N+1-highlighted cards` | `completed` | `docs/subagents/agents/codex-duplicate-kiku-20260221T043006Z-5vkz.md` | `2026-02-21T10:07:58Z` | -| `codex-mpv-connect-log-20260221T043748Z-q7m1` | `codex-mpv-connect-log` | `Suppress repetitive MPV IPC connect-request INFO logs during startup` | `completed` | `docs/subagents/agents/codex-mpv-connect-log-20260221T043748Z-q7m1.md` | `2026-02-21T04:41:15Z` | -| `codex-add-backlog-tasks-20260221T044104Z-m3n8` | `codex-add-backlog-tasks` | `Add two unrelated backlog tasks: secondary subtitle decoupling and intro skip` | `done` | `docs/subagents/agents/codex-add-backlog-tasks-20260221T044104Z-m3n8.md` | `2026-02-21T04:44:12Z` | +| agent_id | alias | mission | status | file | last_update_utc | +| --------------------------------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------- | ---------------------- | +| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` | +| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | +| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` | +| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` | +| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` | +| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` | +| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` | +| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` | +| `codex-texthooker-color-ws-20260220T005844Z-r7m2` | `codex-texthooker-color-ws` | `Fix texthooker websocket payload so token highlight colors render` | `completed` | `docs/subagents/agents/codex-texthooker-color-ws-20260220T005844Z-r7m2.md` | `2026-02-20T01:01:00Z` | +| `codex-nplusone-pos1-20260220T012300Z-c5he` | `codex-nplusone-pos1` | `Fix N+1 false-negative when Yomitan functional tokens inflate unknown candidate count` | `completed` | `docs/subagents/agents/codex-nplusone-pos1-20260220T012300Z-c5he.md` | `2026-02-20T01:28:20Z` | +| `codex-subtitle-bg-20260220T054247Z-h9cu` | `codex-subtitle-bg` | `Update default subtitle background color to requested RGBA value` | `completed` | `docs/subagents/agents/codex-subtitle-bg-20260220T054247Z-h9cu.md` | `2026-02-20T05:44:45Z` | +| `codex-narrow-space-tokenizer-20260220T061716Z-p97s` | `codex-narrow-space-tokenizer` | `Fix tokenization when subtitle line contains narrow/invisible Unicode spacing between segments` | `completed` | `docs/subagents/agents/codex-narrow-space-tokenizer-20260220T061716Z-p97s.md` | `2026-02-20T06:20:07Z` | +| `codex-preserve-linebreaks-20260220T063538Z-s4nd` | `codex-preserve-linebreaks` | `Add config option to preserve subtitle line breaks in visible overlay rendering` | `completed` | `docs/subagents/agents/codex-preserve-linebreaks-20260220T063538Z-s4nd.md` | `2026-02-20T06:42:51Z` | +| `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` | +| `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` | +| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` | +| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` | +| `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` | +| `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` | +| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-21T02:04:12Z` | +| `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` | +| `codex-task95-hotspots-20260221T031420Z-x7k2` | `codex-task95-hotspots` | `Execute TASK-95 decompose oversized core hotspots end-to-end` | `done` | `docs/subagents/agents/codex-task95-hotspots-20260221T031420Z-x7k2.md` | `2026-02-21T03:29:23Z` | +| `codex-task94-thin-root-20260221T031320Z-q3n7` | `codex-task94-thin-root` | `Execute TASK-94 by extracting main.ts deps-builder clusters into runtime composers` | `handoff` | `docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md` | `2026-02-21T03:41:10Z` | +| `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` | +| `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` | diff --git a/docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md b/docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md new file mode 100644 index 0000000..508c6b2 --- /dev/null +++ b/docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md @@ -0,0 +1,40 @@ +# Agent: codex-task94-thin-root-20260221T031320Z-q3n7 + +- alias: codex-task94-thin-root +- mission: Execute TASK-94 end-to-end by moving main deps-builder clusters into runtime composer modules and shrinking main.ts fan-in +- status: handoff +- branch: main +- started_at: 2026-02-21T03:13:20Z +- heartbeat_minutes: 5 + +## Current Work (newest first) +- [2026-02-21T03:41:10Z] handoff: completed TASK-94 slice extraction for jellyfin/anilist composer modules; `main.ts` now consumes `composeJellyfinRemoteHandlers` + `composeAnilistSetupHandlers`; added composer tests; tightened fan-in guard to `<=110` import lines and `<=11` unique runtime paths. +- [2026-02-21T03:41:10Z] test: `bun run build`, `node --test dist/main/runtime/composers/anilist-setup-composer.test.js dist/main/runtime/composers/jellyfin-remote-composer.test.js`, `bun run test:config:dist`, `bun run test:core:dist`, `bun run check:main-fanin`, `bun run check:main-fanin:strict`, `bun run check:file-budgets`. +- [2026-02-21T03:24:30Z] intent: execute TASK-94 with batch extraction (startup, overlay, jellyfin/anilist, ipc/shortcuts), tests, fan-in threshold tighten, backlog updates. +- [2026-02-21T03:24:30Z] planned files: `src/main.ts`, `src/main/runtime/composers/*`, `src/main/runtime/registry.ts`, `src/main/runtime/*composer*.test.ts`, `scripts/check-main-runtime-fanin.ts`, `backlog/tasks/task-94 - Reduce-main.ts-to-thin-composition-root.md`, `backlog/tasks/task-71 - Split-main.ts-into-domain-runtime-modules-round-2.md`. +- [2026-02-21T03:24:30Z] assumptions: prior TASK-71 domain barrels + registry already landed in working tree; continue from current dirty state without reverting unrelated edits. + +## Files Touched +- `docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md` +- `docs/subagents/INDEX.md` +- `docs/subagents/collaboration.md` +- `src/main.ts` +- `src/main/runtime/composers/jellyfin-remote-composer.ts` +- `src/main/runtime/composers/anilist-setup-composer.ts` +- `src/main/runtime/composers/jellyfin-remote-composer.test.ts` +- `src/main/runtime/composers/anilist-setup-composer.test.ts` +- `scripts/check-main-runtime-fanin.ts` +- `docs/file-size-budgets.md` +- `backlog/tasks/task-94 - Reduce-main.ts-to-thin-composition-root.md` +- `backlog/tasks/task-71 - Split-main.ts-into-domain-runtime-modules-round-2.md` +- `backlog/tasks/task-85 - Refactor-large-files-for-maintainability-and-readability.md` + +## Assumptions +- `TASK-94` scope is single-ticket execution from existing branch state; no new worktree requested by user. +- Existing uncommitted edits are intentional and should remain. + +## Open Questions / Blockers +- none + +## Next Step +- Continue TASK-94 by extracting startup/overlay/ipc/shortcuts deps-builder clusters into composer modules to finish thin composition-root target. diff --git a/docs/subagents/collaboration.md b/docs/subagents/collaboration.md index fc211de..0de6b79 100644 --- a/docs/subagents/collaboration.md +++ b/docs/subagents/collaboration.md @@ -16,20 +16,13 @@ Shared notes. Append-only. - [2026-02-20T11:04:36Z] [codex-preserve-linebreak-display-20260220T110436Z-r8f1|codex-preserve-linebreak-display] overlap note: touching `src/renderer/subtitle-render.ts` + renderer tests to fix preserve-linebreaks disabled display artifact while preserving TASK-91 behavior. - [2026-02-20T11:07:29Z] [codex-preserve-linebreak-display-20260220T110436Z-r8f1|codex-preserve-linebreak-display] completed follow-up for TASK-91: non-preserve mode now flattens token CR/LF to spaces instead of emitting `
` from token surfaces; regression test added. - [2026-02-20T11:10:51Z] [codex-preserve-linebreak-display-20260220T110436Z-r8f1|codex-preserve-linebreak-display] second follow-up: handle overlap token streams by aligning non-preserve rendering to normalized source text and skipping unmatched tail tokens (prevents duplicated second-line phrase). -- [2026-02-21T04:07:05Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] overlap note: touching `src/renderer/subtitle-render.ts` + renderer tests for whitespace/newline display/token hover regression around `preserveLineBreaks`. -- [2026-02-21T04:09:02Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] completed: whitespace-only token surfaces no longer become token segments; non-preserve mode now flattens token newlines to spaces and renders whitespace as text nodes; added regression test in renderer suite. -- [2026-02-21T04:14:30Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] preserve-line-breaks follow-up: when token surface mismatches source (e.g., `1` vs `1`), alignment now skips unmatched token instead of appending both source tail + token; fixes duplicated no-break line artifact. -- [2026-02-21T04:18:16Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] follow-up fix: non-token fallback now honors preserveLineBreaks flag by collapsing line breaks when disabled; prevents visible multi-line -> single-line transition while tokenized payload arrives. -- [2026-02-21T04:18:58Z] [codex-jellyfin-secret-store-20260220T101428Z-om4z|codex-jellyfin-secret-store] overlap note: follow-up Jellyfin auth refactor touching `src/main.ts`, `src/main/runtime/jellyfin-*`, and config/docs to remove config token/userId fields in favor of env+stored session payload. -- [2026-02-21T04:27:24Z] [codex-jellyfin-secret-store-20260220T101428Z-om4z|codex-jellyfin-secret-store] completed TASK-93: removed Jellyfin accessToken/userId config fields; resolver now uses env-first (`SUBMINER_JELLYFIN_ACCESS_TOKEN` + optional `SUBMINER_JELLYFIN_USER_ID`) then stored encrypted session payload; login/setup save session and logout clears session. -- [2026-02-21T04:30:06Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] investigating Kiku duplicate grouping regression; expecting touches in `src/anki-integration/duplicate.ts` and duplicate-detection tests only. -- [2026-02-21T04:33:17Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] completed TASK-94: duplicate check now resolves `word`/`expression` alias fields when validating candidate notes; added regression test `src/anki-integration/duplicate.test.ts`; targeted build + duplicate/anki-integration tests passed. -- [2026-02-21T04:38:25Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] follow-up repro fixed: duplicate search now queries both alias fields (`word` + `expression`) and unions note ids before exact compare; added second regression test for alias-query fallback. -- [2026-02-21T04:48:50Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] second follow-up fix: when source note has both `Expression` and `Word`, duplicate detection now uses both source values (not just first field by order); added regression for mixed-field source candidate scenario. -- [2026-02-21T07:23:56Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] third follow-up fix: add collection-wide fallback query pass when deck-scoped duplicate search returns no candidates; added regression for deck-scope miss case. -- [2026-02-21T09:25:53Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] fourth follow-up fix: add plain-text query fallback when field-scoped queries miss; keep exact value verification on candidate notes to avoid false positives. -- [2026-02-21T09:40:33Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] instrumentation pass: add duplicate-detection debug logs (`[duplicate] query/hits/candidates/exact-match`) to isolate remaining live repro mismatches. -- [2026-02-21T09:54:29Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] logging-path update: default persistent logs now target `~/.config/SubMiner/logs/SubMiner-YYYY-MM-DD.log` (launcher + app mpv log default). -- [2026-02-21T10:07:58Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] observability fix: app logger now also appends to daily log file, so runtime duplicate traces are available even when overlay stdout is not surfaced in launcher terminal. -- [2026-02-21T04:37:48Z] [codex-mpv-connect-log-20260221T043748Z-q7m1|codex-mpv-connect-log] overlap note: touching `src/core/services/mpv.ts` + mpv service tests for startup connection-request log level gating; coordinating with historical TASK-33 behavior (same symptom, new logger path). -- [2026-02-21T04:41:15Z] [codex-mpv-connect-log-20260221T043748Z-q7m1|codex-mpv-connect-log] completed TASK-95: changed `MpvIpcClient.connect()` connect-request line to `logger.debug`, added regression tests for info/debug level log behavior in `src/core/services/mpv.test.ts`; verified via `bun run build && node dist/core/services/mpv.test.js` (pass). +- [2026-02-21T03:24:45Z] [codex-task94-thin-root-20260221T031320Z-q3n7|codex-task94-thin-root] overlap note: starting TASK-94 extraction in `src/main.ts` + new `src/main/runtime/composers/*`; coordinating around existing in-progress rows (`codex-main`, `codex-task85`) that may also touch main-runtime wiring. +- [2026-02-21T03:41:10Z] [codex-task94-thin-root-20260221T031320Z-q3n7|codex-task94-thin-root] shipped TASK-94 slice: moved jellyfin/anilist deps-builder clusters behind composer modules, tightened fan-in guard, added composer tests, updated backlog task notes with before/after metrics and remaining scope (startup/overlay/ipc/shortcuts extraction). +- [2026-02-21T03:14:29Z] [codex-task95-hotspots-20260221T031420Z-x7k2|codex-task95-hotspots] starting TASK-95 execution: load backlog context, write plan with writing-plans skill, execute with executing-plans (no commit). +- [2026-02-21T03:29:23Z] [codex-task95-hotspots-20260221T031420Z-x7k2|codex-task95-hotspots] completed TASK-95 with parallel subagents (anki/config/immersion), full gates green, TASK-95 set Done and TASK-85 evidence updated. +- [2026-02-21T03:18:36Z] [codex-task95-anki-20260221T031836Z-6f3e|codex-task95-anki] starting TASK-95 Anki portion: extract field-grouping/merge collaborator from `src/anki-integration.ts` into `src/anki-integration/*`; add seam tests; run `bun run build && node --test dist/anki-integration.test.js`. +- [2026-02-21T03:26:55Z] [codex-task95-anki-20260221T031836Z-6f3e|codex-task95-anki] completed Anki extraction; collaborator now in `src/anki-integration/field-grouping-merge.ts`; `AnkiIntegration` rewired via collaborator seam; targeted build + `dist/anki-integration.test.js` passing. +- [2026-02-21T03:18:43Z] [opencode-task95-config-20260221T031843Z-m4k9|opencode-task95-config] starting TASK-95 config slice; scope `src/config/service.ts`, new `src/config/*` collaborators, and config seam tests only; no 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: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. diff --git a/scripts/check-main-runtime-fanin.ts b/scripts/check-main-runtime-fanin.ts new file mode 100644 index 0000000..e223a52 --- /dev/null +++ b/scripts/check-main-runtime-fanin.ts @@ -0,0 +1,103 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +type ParsedArgs = { + strict: boolean; + uniquePathLimit: number; + importLineLimit: number; +}; + +const DEFAULT_UNIQUE_PATH_LIMIT = 11; +const DEFAULT_IMPORT_LINE_LIMIT = 110; +const MAIN_PATH = path.join(process.cwd(), 'src', 'main.ts'); + +function parseArgs(argv: string[]): ParsedArgs { + let strict = false; + let uniquePathLimit = DEFAULT_UNIQUE_PATH_LIMIT; + let importLineLimit = DEFAULT_IMPORT_LINE_LIMIT; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--strict') { + strict = true; + continue; + } + if (arg === '--unique-path-limit') { + const raw = argv[i + 1]; + const parsed = Number.parseInt(raw ?? '', 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`Invalid --unique-path-limit value: ${raw ?? ''}`); + } + uniquePathLimit = parsed; + i += 1; + continue; + } + if (arg === '--import-line-limit') { + const raw = argv[i + 1]; + const parsed = Number.parseInt(raw ?? '', 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`Invalid --import-line-limit value: ${raw ?? ''}`); + } + importLineLimit = parsed; + i += 1; + continue; + } + } + + return { strict, uniquePathLimit, importLineLimit }; +} + +function collectRuntimeImportStats(source: string): { + importLines: number; + uniquePaths: string[]; +} { + const pathMatches = Array.from(source.matchAll(/from '(\.\/main\/runtime[^']*)';/g)).map( + (match) => match[1], + ); + const uniquePaths = new Set(); + + for (const runtimeImportPath of pathMatches) { + uniquePaths.add(runtimeImportPath); + } + + return { + importLines: pathMatches.length, + uniquePaths: Array.from(uniquePaths).sort(), + }; +} + +function main(): void { + const { strict, uniquePathLimit, importLineLimit } = parseArgs(process.argv.slice(2)); + const source = fs.readFileSync(MAIN_PATH, 'utf8'); + const { importLines, uniquePaths } = collectRuntimeImportStats(source); + const overUniquePathLimit = uniquePaths.length > uniquePathLimit; + const overImportLineLimit = importLines > importLineLimit; + const hasFailure = overUniquePathLimit || overImportLineLimit; + const mode = strict ? 'strict' : 'warning'; + + if (!hasFailure) { + console.log( + `[OK] main runtime fan-in (${mode}) — ${importLines} import lines, ${uniquePaths.length} unique runtime paths`, + ); + return; + } + + const heading = strict ? '[FAIL]' : '[WARN]'; + console.log( + `${heading} main runtime fan-in (${mode}) — ${importLines} import lines, ${uniquePaths.length} unique runtime paths`, + ); + console.log(` limits: import lines <= ${importLineLimit}, unique paths <= ${uniquePathLimit}`); + console.log(' runtime import paths:'); + for (const runtimeImportPath of uniquePaths) { + console.log(` - ${runtimeImportPath}`); + } + console.log( + ' Hint: keep main.ts focused on boot wiring; move domain imports behind domain barrels/registries.', + ); + + if (strict) { + process.exitCode = 1; + } +} + +main(); diff --git a/src/main.ts b/src/main.ts index 35d5e2e..6c2c677 100644 --- a/src/main.ts +++ b/src/main.ts @@ -71,15 +71,15 @@ import { printHelp } from './cli/help'; import { createCriticalConfigErrorHandler, createReloadConfigHandler, -} from './main/runtime/startup-config'; +} from './main/runtime/domains/startup'; import { buildConfigWarningNotificationBody } from './main/config-validation'; -import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup'; -import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps'; -import { createImmersionMediaRuntime } from './main/runtime/immersion-media'; -import { createAnilistStateRuntime } from './main/runtime/anilist-state'; -import { createConfigDerivedRuntime } from './main/runtime/config-derived'; -import { appendClipboardVideoToQueueRuntime } from './main/runtime/clipboard-queue'; -import { createMainSubsyncRuntime } from './main/runtime/subsync-runtime'; +import { createImmersionTrackerStartupHandler } from './main/runtime/domains/startup'; +import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/domains/startup'; +import { createImmersionMediaRuntime } from './main/runtime/domains/startup'; +import { createAnilistStateRuntime } from './main/runtime/domains/anilist'; +import { createConfigDerivedRuntime } from './main/runtime/domains/startup'; +import { appendClipboardVideoToQueueRuntime } from './main/runtime/domains/startup'; +import { createMainSubsyncRuntime } from './main/runtime/domains/startup'; import { buildAnilistSetupUrl, consumeAnilistSetupCallbackUrl, @@ -88,246 +88,220 @@ import { loadAnilistManualTokenEntry, loadAnilistSetupFallback, openAnilistSetupInBrowser, -} from './main/runtime/anilist-setup'; +} from './main/runtime/domains/anilist'; +import { createRefreshAnilistClientSecretStateHandler } from './main/runtime/domains/anilist'; +import { createBuildRefreshAnilistClientSecretStateMainDepsHandler } from './main/runtime/domains/anilist'; import { - createConsumeAnilistSetupTokenFromUrlHandler, - createHandleAnilistSetupProtocolUrlHandler, - createNotifyAnilistSetupHandler, - createRegisterSubminerProtocolClientHandler, -} from './main/runtime/anilist-setup-protocol'; -import { - createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler, - createBuildHandleAnilistSetupProtocolUrlMainDepsHandler, - createBuildNotifyAnilistSetupMainDepsHandler, - createBuildRegisterSubminerProtocolClientMainDepsHandler, -} from './main/runtime/anilist-setup-protocol-main-deps'; -import { createRefreshAnilistClientSecretStateHandler } from './main/runtime/anilist-token-refresh'; -import { createBuildRefreshAnilistClientSecretStateMainDepsHandler } from './main/runtime/anilist-token-refresh-main-deps'; -import { - createHandleJellyfinRemoteGeneralCommand, - createHandleJellyfinRemotePlay, - createHandleJellyfinRemotePlaystate, getConfiguredJellyfinSession, type ActiveJellyfinRemotePlaybackState, -} from './main/runtime/jellyfin-remote-commands'; -import { - createReportJellyfinRemoteProgressHandler, - createReportJellyfinRemoteStoppedHandler, -} from './main/runtime/jellyfin-remote-playback'; -import { - createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler, - createBuildHandleJellyfinRemotePlayMainDepsHandler, - createBuildHandleJellyfinRemotePlaystateMainDepsHandler, - createBuildReportJellyfinRemoteProgressMainDepsHandler, - createBuildReportJellyfinRemoteStoppedMainDepsHandler, -} from './main/runtime/jellyfin-remote-main-deps'; -import { createBuildSubtitleProcessingControllerMainDepsHandler } from './main/runtime/subtitle-processing-main-deps'; +} from './main/runtime/domains/jellyfin'; +import { createBuildSubtitleProcessingControllerMainDepsHandler } from './main/runtime/domains/startup'; import { createBuildAnilistStateRuntimeMainDepsHandler, createBuildConfigDerivedRuntimeMainDepsHandler, createBuildImmersionMediaRuntimeMainDepsHandler, createBuildMainSubsyncRuntimeMainDepsHandler, -} from './main/runtime/runtime-bootstrap-main-deps'; +} from './main/runtime/domains/startup'; import { createBuildOverlayContentMeasurementStoreMainDepsHandler, createBuildOverlayModalRuntimeMainDepsHandler, -} from './main/runtime/overlay-bootstrap-main-deps'; +} from './main/runtime/domains/overlay'; import { createEnsureMpvConnectedForJellyfinPlaybackHandler, createLaunchMpvIdleForJellyfinPlaybackHandler, createWaitForMpvConnectedHandler, -} from './main/runtime/jellyfin-remote-connection'; +} from './main/runtime/domains/jellyfin'; import { createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler, createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler, createBuildWaitForMpvConnectedMainDepsHandler, -} from './main/runtime/jellyfin-remote-connection-main-deps'; +} from './main/runtime/domains/jellyfin'; import { buildJellyfinSetupFormHtml, createOpenJellyfinSetupWindowHandler, createMaybeFocusExistingJellyfinSetupWindowHandler, parseJellyfinSetupSubmissionUrl, -} from './main/runtime/jellyfin-setup-window'; -import { createBuildOpenJellyfinSetupWindowMainDepsHandler } from './main/runtime/jellyfin-setup-window-main-deps'; +} from './main/runtime/domains/jellyfin'; +import { createBuildOpenJellyfinSetupWindowMainDepsHandler } from './main/runtime/domains/jellyfin'; import { createMaybeFocusExistingAnilistSetupWindowHandler, createOpenAnilistSetupWindowHandler, -} from './main/runtime/anilist-setup-window'; -import { createBuildOpenAnilistSetupWindowMainDepsHandler } from './main/runtime/anilist-setup-window-main-deps'; +} from './main/runtime/domains/anilist'; +import { createBuildOpenAnilistSetupWindowMainDepsHandler } from './main/runtime/domains/anilist'; import { createEnsureAnilistMediaGuessHandler, createMaybeProbeAnilistDurationHandler, -} from './main/runtime/anilist-media-guess'; +} from './main/runtime/domains/anilist'; import { createBuildEnsureAnilistMediaGuessMainDepsHandler, createBuildMaybeProbeAnilistDurationMainDepsHandler, -} from './main/runtime/anilist-media-guess-main-deps'; +} from './main/runtime/domains/anilist'; import { createGetAnilistMediaGuessRuntimeStateHandler, createGetCurrentAnilistMediaKeyHandler, createResetAnilistMediaGuessStateHandler, createResetAnilistMediaTrackingHandler, createSetAnilistMediaGuessRuntimeStateHandler, -} from './main/runtime/anilist-media-state'; +} from './main/runtime/domains/anilist'; import { createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler, createBuildGetCurrentAnilistMediaKeyMainDepsHandler, createBuildResetAnilistMediaGuessStateMainDepsHandler, createBuildResetAnilistMediaTrackingMainDepsHandler, createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler, -} from './main/runtime/anilist-media-state-main-deps'; +} from './main/runtime/domains/anilist'; import { buildAnilistAttemptKey, createMaybeRunAnilistPostWatchUpdateHandler, createProcessNextAnilistRetryUpdateHandler, rememberAnilistAttemptedUpdateKey, -} from './main/runtime/anilist-post-watch'; +} from './main/runtime/domains/anilist'; import { createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler, createBuildProcessNextAnilistRetryUpdateMainDepsHandler, -} from './main/runtime/anilist-post-watch-main-deps'; +} from './main/runtime/domains/anilist'; import { createLoadSubtitlePositionHandler, createSaveSubtitlePositionHandler, -} from './main/runtime/subtitle-position'; +} from './main/runtime/domains/overlay'; import { createBuildLoadSubtitlePositionMainDepsHandler, createBuildSaveSubtitlePositionMainDepsHandler, -} from './main/runtime/subtitle-position-main-deps'; -import { registerProtocolUrlHandlers } from './main/runtime/protocol-url-handlers'; -import { createBuildRegisterProtocolUrlHandlersMainDepsHandler } from './main/runtime/protocol-url-handlers-main-deps'; -import { createHandleJellyfinAuthCommands } from './main/runtime/jellyfin-cli-auth'; -import { createRunJellyfinCommandHandler } from './main/runtime/jellyfin-command-dispatch'; -import { createBuildRunJellyfinCommandMainDepsHandler } from './main/runtime/jellyfin-command-dispatch-main-deps'; -import { createHandleJellyfinListCommands } from './main/runtime/jellyfin-cli-list'; -import { createHandleJellyfinPlayCommand } from './main/runtime/jellyfin-cli-play'; -import { createHandleJellyfinRemoteAnnounceCommand } from './main/runtime/jellyfin-cli-remote-announce'; +} from './main/runtime/domains/overlay'; +import { registerProtocolUrlHandlers } from './main/runtime/domains/anilist'; +import { createBuildRegisterProtocolUrlHandlersMainDepsHandler } from './main/runtime/domains/anilist'; +import { createHandleJellyfinAuthCommands } from './main/runtime/domains/jellyfin'; +import { createRunJellyfinCommandHandler } from './main/runtime/domains/jellyfin'; +import { createBuildRunJellyfinCommandMainDepsHandler } from './main/runtime/domains/jellyfin'; +import { createHandleJellyfinListCommands } from './main/runtime/domains/jellyfin'; +import { createHandleJellyfinPlayCommand } from './main/runtime/domains/jellyfin'; +import { createHandleJellyfinRemoteAnnounceCommand } from './main/runtime/domains/jellyfin'; import { createBuildHandleJellyfinAuthCommandsMainDepsHandler, createBuildHandleJellyfinListCommandsMainDepsHandler, createBuildHandleJellyfinPlayCommandMainDepsHandler, createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler, -} from './main/runtime/jellyfin-cli-main-deps'; +} from './main/runtime/domains/jellyfin'; import { createGetJellyfinClientInfoHandler, createGetResolvedJellyfinConfigHandler, -} from './main/runtime/jellyfin-client-info'; +} from './main/runtime/domains/jellyfin'; import { createBuildGetJellyfinClientInfoMainDepsHandler, createBuildGetResolvedJellyfinConfigMainDepsHandler, -} from './main/runtime/jellyfin-client-info-main-deps'; +} from './main/runtime/domains/jellyfin'; import { createApplyJellyfinMpvDefaultsHandler, createGetDefaultSocketPathHandler, -} from './main/runtime/mpv-jellyfin-defaults'; +} from './main/runtime/domains/jellyfin'; import { createBuildApplyJellyfinMpvDefaultsMainDepsHandler, createBuildGetDefaultSocketPathMainDepsHandler, -} from './main/runtime/mpv-jellyfin-defaults-main-deps'; -import { createBuildMediaRuntimeMainDepsHandler } from './main/runtime/media-runtime-main-deps'; +} from './main/runtime/domains/jellyfin'; +import { createBuildMediaRuntimeMainDepsHandler } from './main/runtime/domains/startup'; import { createBuildDictionaryRootsMainHandler, createBuildFrequencyDictionaryRootsMainHandler, createBuildFrequencyDictionaryRuntimeMainDepsHandler, createBuildJlptDictionaryRuntimeMainDepsHandler, -} from './main/runtime/dictionary-runtime-main-deps'; -import { createPlayJellyfinItemInMpvHandler } from './main/runtime/jellyfin-playback-launch'; -import { createBuildPlayJellyfinItemInMpvMainDepsHandler } from './main/runtime/jellyfin-playback-launch-main-deps'; -import { createPreloadJellyfinExternalSubtitlesHandler } from './main/runtime/jellyfin-subtitle-preload'; -import { createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler } from './main/runtime/jellyfin-subtitle-preload-main-deps'; +} from './main/runtime/domains/startup'; +import { createPlayJellyfinItemInMpvHandler } from './main/runtime/domains/jellyfin'; +import { createBuildPlayJellyfinItemInMpvMainDepsHandler } from './main/runtime/domains/jellyfin'; +import { createPreloadJellyfinExternalSubtitlesHandler } from './main/runtime/domains/jellyfin'; +import { createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler } from './main/runtime/domains/jellyfin'; import { createStartJellyfinRemoteSessionHandler, createStopJellyfinRemoteSessionHandler, -} from './main/runtime/jellyfin-remote-session-lifecycle'; +} from './main/runtime/domains/jellyfin'; import { createBuildStartJellyfinRemoteSessionMainDepsHandler, createBuildStopJellyfinRemoteSessionMainDepsHandler, -} from './main/runtime/jellyfin-remote-session-main-deps'; -import { createCliCommandRuntimeHandler } from './main/runtime/cli-command-runtime-handler'; -import { createInitialArgsRuntimeHandler } from './main/runtime/initial-args-runtime-handler'; +} from './main/runtime/domains/jellyfin'; +import { createCliCommandRuntimeHandler } from './main/runtime/domains/ipc'; +import { createInitialArgsRuntimeHandler } from './main/runtime/domains/ipc'; import { createGetFieldGroupingResolverHandler, createSetFieldGroupingResolverHandler, -} from './main/runtime/field-grouping-resolver'; +} from './main/runtime/domains/overlay'; import { createBuildGetFieldGroupingResolverMainDepsHandler, createBuildSetFieldGroupingResolverMainDepsHandler, -} from './main/runtime/field-grouping-resolver-main-deps'; -import { createBuildFieldGroupingOverlayMainDepsHandler } from './main/runtime/field-grouping-overlay-main-deps'; -import { createCliCommandContextFactory } from './main/runtime/cli-command-context-factory'; -import { createBindMpvMainEventHandlersHandler } from './main/runtime/mpv-main-event-bindings'; -import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './main/runtime/mpv-main-event-main-deps'; -import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from './main/runtime/mpv-client-runtime-service-main-deps'; -import { createMpvClientRuntimeServiceFactory } from './main/runtime/mpv-client-runtime-service'; -import { createUpdateMpvSubtitleRenderMetricsHandler } from './main/runtime/mpv-subtitle-render-metrics'; -import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from './main/runtime/mpv-subtitle-render-metrics-main-deps'; +} from './main/runtime/domains/overlay'; +import { createBuildFieldGroupingOverlayMainDepsHandler } from './main/runtime/domains/overlay'; +import { createCliCommandContextFactory } from './main/runtime/domains/ipc'; +import { createBindMpvMainEventHandlersHandler } from './main/runtime/domains/mpv'; +import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './main/runtime/domains/mpv'; +import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from './main/runtime/domains/mpv'; +import { createMpvClientRuntimeServiceFactory } from './main/runtime/domains/mpv'; +import { createUpdateMpvSubtitleRenderMetricsHandler } from './main/runtime/domains/mpv'; +import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from './main/runtime/domains/mpv'; import { createBuildTokenizerDepsMainHandler, createCreateMecabTokenizerAndCheckMainHandler, createPrewarmSubtitleDictionariesMainHandler, -} from './main/runtime/subtitle-tokenization-main-deps'; +} from './main/runtime/domains/mpv'; import { createLaunchBackgroundWarmupTaskHandler, createStartBackgroundWarmupsHandler, -} from './main/runtime/startup-warmups'; +} from './main/runtime/domains/startup'; import { createBuildLaunchBackgroundWarmupTaskMainDepsHandler, createBuildStartBackgroundWarmupsMainDepsHandler, -} from './main/runtime/startup-warmups-main-deps'; +} from './main/runtime/domains/startup'; import { createEnforceOverlayLayerOrderHandler, createEnsureOverlayWindowLevelHandler, createUpdateInvisibleOverlayBoundsHandler, createUpdateVisibleOverlayBoundsHandler, -} from './main/runtime/overlay-window-layout'; +} from './main/runtime/domains/overlay'; import { createBuildEnforceOverlayLayerOrderMainDepsHandler, createBuildEnsureOverlayWindowLevelMainDepsHandler, createBuildUpdateInvisibleOverlayBoundsMainDepsHandler, createBuildUpdateVisibleOverlayBoundsMainDepsHandler, -} from './main/runtime/overlay-window-layout-main-deps'; -import { buildTrayMenuTemplateRuntime, resolveTrayIconPathRuntime } from './main/runtime/tray-runtime'; -import { createGlobalShortcutsRuntimeHandlers } from './main/runtime/global-shortcuts-runtime-handlers'; -import { createMpvOsdRuntimeHandlers } from './main/runtime/mpv-osd-runtime-handlers'; -import { createCycleSecondarySubModeRuntimeHandler } from './main/runtime/secondary-sub-mode-runtime-handler'; -import { createNumericShortcutSessionRuntimeHandlers } from './main/runtime/numeric-shortcut-session-runtime-handlers'; -import { createBuildNumericShortcutRuntimeMainDepsHandler } from './main/runtime/numeric-shortcut-runtime-main-deps'; -import { createOverlayShortcutsRuntimeHandlers } from './main/runtime/overlay-shortcuts-runtime-handlers'; -import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/overlay-shortcuts-runtime-main-deps'; +} from './main/runtime/domains/overlay'; +import { buildTrayMenuTemplateRuntime, resolveTrayIconPathRuntime } from './main/runtime/domains/overlay'; +import { createGlobalShortcutsRuntimeHandlers } from './main/runtime/domains/shortcuts'; +import { createMpvOsdRuntimeHandlers } from './main/runtime/domains/mpv'; +import { createCycleSecondarySubModeRuntimeHandler } from './main/runtime/domains/mpv'; +import { createNumericShortcutSessionRuntimeHandlers } from './main/runtime/domains/shortcuts'; +import { createBuildNumericShortcutRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts'; +import { createOverlayShortcutsRuntimeHandlers } from './main/runtime/domains/shortcuts'; +import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts'; import { createMarkLastCardAsAudioCardHandler, createMineSentenceCardHandler, createRefreshKnownWordCacheHandler, createTriggerFieldGroupingHandler, createUpdateLastCardFromClipboardHandler, -} from './main/runtime/anki-actions'; +} from './main/runtime/domains/mining'; import { createBuildMarkLastCardAsAudioCardMainDepsHandler, createBuildMineSentenceCardMainDepsHandler, createBuildRefreshKnownWordCacheMainDepsHandler, createBuildTriggerFieldGroupingMainDepsHandler, createBuildUpdateLastCardFromClipboardMainDepsHandler, -} from './main/runtime/anki-actions-main-deps'; +} from './main/runtime/domains/mining'; import { createCopyCurrentSubtitleHandler, createHandleMineSentenceDigitHandler, createHandleMultiCopyDigitHandler, -} from './main/runtime/mining-actions'; +} from './main/runtime/domains/mining'; import { createBuildCopyCurrentSubtitleMainDepsHandler, createBuildHandleMineSentenceDigitMainDepsHandler, createBuildHandleMultiCopyDigitMainDepsHandler, -} from './main/runtime/mining-actions-main-deps'; -import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './main/runtime/overlay-visibility-runtime-main-deps'; -import { createOverlayVisibilityRuntime } from './main/runtime/overlay-visibility-runtime'; +} from './main/runtime/domains/mining'; +import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './main/runtime/domains/overlay'; +import { createOverlayVisibilityRuntime } from './main/runtime/domains/overlay'; import { createAppendClipboardVideoToQueueHandler, createHandleOverlayModalClosedHandler, -} from './main/runtime/overlay-main-actions'; +} from './main/runtime/domains/overlay'; import { createBuildAppendClipboardVideoToQueueMainDepsHandler, createBuildHandleOverlayModalClosedMainDepsHandler, -} from './main/runtime/overlay-main-actions-main-deps'; +} from './main/runtime/domains/overlay'; import { createBroadcastRuntimeOptionsChangedHandler, createGetRuntimeOptionsStateHandler, @@ -335,7 +309,7 @@ import { createRestorePreviousSecondarySubVisibilityHandler, createSendToActiveOverlayWindowHandler, createSetOverlayDebugVisualizationEnabledHandler, -} from './main/runtime/overlay-runtime-main-actions'; +} from './main/runtime/domains/overlay'; import { createBuildBroadcastRuntimeOptionsChangedMainDepsHandler, createBuildGetRuntimeOptionsStateMainDepsHandler, @@ -343,43 +317,43 @@ import { createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler, createBuildSendToActiveOverlayWindowMainDepsHandler, createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler, -} from './main/runtime/overlay-runtime-main-actions-main-deps'; -import { createIpcRuntimeHandlers } from './main/runtime/ipc-runtime-handlers'; -import { createBuildMpvCommandFromIpcRuntimeMainDepsHandler } from './main/runtime/ipc-mpv-command-main-deps'; -import { createOverlayWindowRuntimeHandlers } from './main/runtime/overlay-window-runtime-handlers'; -import { createOverlayRuntimeBootstrapHandlers } from './main/runtime/overlay-runtime-bootstrap-handlers'; -import { createTrayRuntimeHandlers } from './main/runtime/tray-runtime-handlers'; -import { createYomitanExtensionRuntime } from './main/runtime/yomitan-extension-runtime'; -import { createYomitanSettingsRuntime } from './main/runtime/yomitan-settings-runtime'; +} from './main/runtime/domains/overlay'; +import { createIpcRuntimeHandlers } from './main/runtime/domains/ipc'; +import { createBuildMpvCommandFromIpcRuntimeMainDepsHandler } from './main/runtime/domains/ipc'; +import { createOverlayWindowRuntimeHandlers } from './main/runtime/domains/overlay'; +import { createOverlayRuntimeBootstrapHandlers } from './main/runtime/domains/overlay'; +import { createTrayRuntimeHandlers } from './main/runtime/domains/overlay'; +import { createYomitanExtensionRuntime } from './main/runtime/domains/overlay'; +import { createYomitanSettingsRuntime } from './main/runtime/domains/overlay'; import { createOnWillQuitCleanupHandler, createRestoreWindowsOnActivateHandler, createShouldRestoreWindowsOnActivateHandler, -} from './main/runtime/app-lifecycle-actions'; -import { createBuildOnWillQuitCleanupDepsHandler } from './main/runtime/app-lifecycle-main-cleanup'; +} from './main/runtime/domains/startup'; +import { createBuildOnWillQuitCleanupDepsHandler } from './main/runtime/domains/startup'; import { createBuildRestoreWindowsOnActivateMainDepsHandler, createBuildShouldRestoreWindowsOnActivateMainDepsHandler, -} from './main/runtime/app-lifecycle-main-activate'; +} from './main/runtime/domains/startup'; import { buildRestartRequiredConfigMessage, createConfigHotReloadAppliedHandler, createConfigHotReloadMessageHandler, resolveSubtitleStyleForRenderer, -} from './main/runtime/config-hot-reload-handlers'; +} from './main/runtime/domains/overlay'; import { createBuildConfigHotReloadMessageMainDepsHandler, createBuildConfigHotReloadAppliedMainDepsHandler, createBuildConfigHotReloadRuntimeMainDepsHandler, createBuildWatchConfigPathMainDepsHandler, createWatchConfigPathHandler, -} from './main/runtime/config-hot-reload-main-deps'; +} from './main/runtime/domains/overlay'; import { createBuildCriticalConfigErrorMainDepsHandler, createBuildReloadConfigMainDepsHandler, -} from './main/runtime/startup-config-main-deps'; -import { createBuildAppReadyRuntimeMainDepsHandler } from './main/runtime/app-ready-main-deps'; -import { createStartupRuntimeHandlers } from './main/runtime/startup-runtime-handlers'; +} from './main/runtime/domains/startup'; +import { createBuildAppReadyRuntimeMainDepsHandler } from './main/runtime/domains/startup'; +import { createStartupRuntimeHandlers } from './main/runtime/domains/startup'; import { enforceUnsupportedWaylandMode, forceX11Backend, @@ -479,6 +453,9 @@ import { generateConfigTemplate, } from './config'; import { resolveConfigDir } from './config/path-resolution'; +import { createMainRuntimeRegistry } from './main/runtime/registry'; +import { composeAnilistSetupHandlers } from './main/runtime/composers/anilist-setup-composer'; +import { composeJellyfinRemoteHandlers } from './main/runtime/composers/jellyfin-remote-composer'; if (process.platform === 'linux') { app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal'); @@ -613,6 +590,7 @@ const appLogger = { ); }, }; +const runtimeRegistry = createMainRuntimeRegistry(); const buildGetDefaultSocketPathMainDepsHandler = createBuildGetDefaultSocketPathMainDepsHandler({ platform: process.platform, @@ -826,39 +804,27 @@ const buildConfigHotReloadRuntimeMainDepsHandler = createBuildConfigHotReloadRun }); }, }); -const buildHandleJellyfinRemotePlayMainDepsHandler = - createBuildHandleJellyfinRemotePlayMainDepsHandler({ +const { + reportJellyfinRemoteProgress, + reportJellyfinRemoteStopped, + handleJellyfinRemotePlay, + handleJellyfinRemotePlaystate, + handleJellyfinRemoteGeneralCommand, +} = composeJellyfinRemoteHandlers({ getConfiguredSession: () => getConfiguredJellyfinSession(getResolvedJellyfinConfig()), getClientInfo: () => getJellyfinClientInfo(), getJellyfinConfig: () => getResolvedJellyfinConfig(), playJellyfinItem: (params) => playJellyfinItemInMpv(params as Parameters[0]), logWarn: (message) => logger.warn(message), -}); -const buildHandleJellyfinRemotePlaystateMainDepsHandler = - createBuildHandleJellyfinRemotePlaystateMainDepsHandler({ getMpvClient: () => appState.mpvClient, sendMpvCommand: (client, command) => sendMpvCommandRuntime(client as MpvIpcClient, command), - reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force), - reportJellyfinRemoteStopped: () => reportJellyfinRemoteStopped(), jellyfinTicksToSeconds: (ticks) => jellyfinTicksToSecondsRuntime(ticks), -}); -const buildHandleJellyfinRemoteGeneralCommandMainDepsHandler = - createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler({ - getMpvClient: () => appState.mpvClient, - sendMpvCommand: (client, command) => sendMpvCommandRuntime(client as MpvIpcClient, command), - getActivePlayback: () => activeJellyfinRemotePlayback, - reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force), - logDebug: (message) => logger.debug(message), -}); -const buildReportJellyfinRemoteProgressMainDepsHandler = - createBuildReportJellyfinRemoteProgressMainDepsHandler({ getActivePlayback: () => activeJellyfinRemotePlayback, clearActivePlayback: () => { activeJellyfinRemotePlayback = null; }, getSession: () => appState.jellyfinRemoteSession, - getMpvClient: () => appState.mpvClient, getNow: () => Date.now(), getLastProgressAtMs: () => jellyfinRemoteLastProgressAtMs, setLastProgressAtMs: (value) => { @@ -868,39 +834,6 @@ const buildReportJellyfinRemoteProgressMainDepsHandler = ticksPerSecond: JELLYFIN_TICKS_PER_SECOND, logDebug: (message, error) => logger.debug(message, error), }); -const buildReportJellyfinRemoteStoppedMainDepsHandler = - createBuildReportJellyfinRemoteStoppedMainDepsHandler({ - getActivePlayback: () => activeJellyfinRemotePlayback, - clearActivePlayback: () => { - activeJellyfinRemotePlayback = null; - }, - getSession: () => appState.jellyfinRemoteSession, - logDebug: (message, error) => logger.debug(message, error), -}); -const reportJellyfinRemoteProgressMainDeps = - buildReportJellyfinRemoteProgressMainDepsHandler(); -const reportJellyfinRemoteStoppedMainDeps = - buildReportJellyfinRemoteStoppedMainDepsHandler(); -const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler( - reportJellyfinRemoteProgressMainDeps, -); -const reportJellyfinRemoteStopped = createReportJellyfinRemoteStoppedHandler( - reportJellyfinRemoteStoppedMainDeps, -); -const handleJellyfinRemotePlayMainDeps = buildHandleJellyfinRemotePlayMainDepsHandler(); -const handleJellyfinRemotePlaystateMainDeps = - buildHandleJellyfinRemotePlaystateMainDepsHandler(); -const handleJellyfinRemoteGeneralCommandMainDeps = - buildHandleJellyfinRemoteGeneralCommandMainDepsHandler(); -const handleJellyfinRemotePlay = createHandleJellyfinRemotePlay( - handleJellyfinRemotePlayMainDeps, -); -const handleJellyfinRemotePlaystate = createHandleJellyfinRemotePlaystate( - handleJellyfinRemotePlaystateMainDeps, -); -const handleJellyfinRemoteGeneralCommand = createHandleJellyfinRemoteGeneralCommand( - handleJellyfinRemoteGeneralCommandMainDeps, -); const configHotReloadRuntime = createConfigHotReloadRuntime(buildConfigHotReloadRuntimeMainDepsHandler()); @@ -1433,19 +1366,19 @@ const runJellyfinCommand = createRunJellyfinCommandHandler( runJellyfinCommandMainDeps, ); -const buildNotifyAnilistSetupMainDepsHandler = createBuildNotifyAnilistSetupMainDepsHandler({ - hasMpvClient: () => Boolean(appState.mpvClient), - showMpvOsd: (message) => showMpvOsd(message), - showDesktopNotification: (title, options) => showDesktopNotification(title, options), - logInfo: (message) => logger.info(message), -}); -const notifyAnilistSetupMainDeps = buildNotifyAnilistSetupMainDepsHandler(); -const notifyAnilistSetup = createNotifyAnilistSetupHandler( - notifyAnilistSetupMainDeps, -); - -const buildConsumeAnilistSetupTokenFromUrlMainDepsHandler = - createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler({ +const { + notifyAnilistSetup, + consumeAnilistSetupTokenFromUrl, + handleAnilistSetupProtocolUrl, + registerSubminerProtocolClient, +} = composeAnilistSetupHandlers({ + notifyDeps: { + hasMpvClient: () => Boolean(appState.mpvClient), + showMpvOsd: (message) => showMpvOsd(message), + showDesktopNotification: (title, options) => showDesktopNotification(title, options), + logInfo: (message) => logger.info(message), + }, + consumeTokenDeps: { consumeAnilistSetupCallbackUrl, saveToken: (token) => anilistTokenStore.saveToken(token), setCachedToken: (token) => { @@ -1471,26 +1404,12 @@ const buildConsumeAnilistSetupTokenFromUrlMainDepsHandler = appState.anilistSetupWindow.close(); } }, - }); -const consumeAnilistSetupTokenFromUrlMainDeps = - buildConsumeAnilistSetupTokenFromUrlMainDepsHandler(); -const consumeAnilistSetupTokenFromUrl = createConsumeAnilistSetupTokenFromUrlHandler( - consumeAnilistSetupTokenFromUrlMainDeps, -); - -const buildHandleAnilistSetupProtocolUrlMainDepsHandler = - createBuildHandleAnilistSetupProtocolUrlMainDepsHandler({ + }, + handleProtocolDeps: { consumeAnilistSetupTokenFromUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl), logWarn: (message, details) => logger.warn(message, details), - }); -const handleAnilistSetupProtocolUrlMainDeps = - buildHandleAnilistSetupProtocolUrlMainDepsHandler(); -const handleAnilistSetupProtocolUrl = createHandleAnilistSetupProtocolUrlHandler( - handleAnilistSetupProtocolUrlMainDeps, -); - -const buildRegisterSubminerProtocolClientMainDepsHandler = - createBuildRegisterSubminerProtocolClientMainDepsHandler({ + }, + registerProtocolClientDeps: { isDefaultApp: () => Boolean(process.defaultApp), getArgv: () => process.argv, execPath: process.execPath, @@ -1500,12 +1419,8 @@ const buildRegisterSubminerProtocolClientMainDepsHandler = ? app.setAsDefaultProtocolClient(scheme, appPath, args) : app.setAsDefaultProtocolClient(scheme), logWarn: (message, details) => logger.warn(message, details), - }); -const registerSubminerProtocolClientMainDeps = - buildRegisterSubminerProtocolClientMainDepsHandler(); -const registerSubminerProtocolClient = createRegisterSubminerProtocolClientHandler( - registerSubminerProtocolClientMainDeps, -); + }, +}); const maybeFocusExistingAnilistSetupWindow = createMaybeFocusExistingAnilistSetupWindowHandler({ getSetupWindow: () => appState.anilistSetupWindow, @@ -2100,7 +2015,7 @@ const buildAppReadyRuntimeMainDepsHandler = createBuildAppReadyRuntimeMainDepsHa }); const appReadyRuntimeRunner = createAppReadyRuntimeRunner(buildAppReadyRuntimeMainDepsHandler()); -const { appLifecycleRuntimeRunner, runAndApplyStartupState } = createStartupRuntimeHandlers< +const { appLifecycleRuntimeRunner, runAndApplyStartupState } = runtimeRegistry.startup.createStartupRuntimeHandlers< CliArgs, StartupState, ReturnType @@ -2770,7 +2685,10 @@ const buildMpvCommandFromIpcRuntimeMainDepsHandler = const mpvCommandFromIpcRuntimeMainDeps = buildMpvCommandFromIpcRuntimeMainDepsHandler(); const { handleMpvCommandFromIpc: handleMpvCommandFromIpcHandler, runSubsyncManualFromIpc: runSubsyncManualFromIpcHandler } = - createIpcRuntimeHandlers>>({ + runtimeRegistry.ipc.createIpcRuntimeHandlers< + SubsyncManualRunRequest, + Awaited> + >({ handleMpvCommandFromIpcDeps: { handleMpvCommandFromIpcRuntime, buildMpvCommandDeps: () => mpvCommandFromIpcRuntimeMainDeps, @@ -2923,7 +2841,7 @@ const yomitanExtensionRuntime = createYomitanExtensionRuntime({ }, }); const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } = - createOverlayRuntimeBootstrapHandlers({ + runtimeRegistry.overlay.createOverlayRuntimeBootstrapHandlers({ initializeOverlayRuntimeMainDeps: { appState, overlayManager: { diff --git a/src/main/runtime/composers/anilist-setup-composer.test.ts b/src/main/runtime/composers/anilist-setup-composer.test.ts new file mode 100644 index 0000000..8914125 --- /dev/null +++ b/src/main/runtime/composers/anilist-setup-composer.test.ts @@ -0,0 +1,40 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { composeAnilistSetupHandlers } from './anilist-setup-composer'; + +test('composeAnilistSetupHandlers returns callable setup handlers', () => { + const composed = composeAnilistSetupHandlers({ + notifyDeps: { + hasMpvClient: () => false, + showMpvOsd: () => {}, + showDesktopNotification: () => {}, + logInfo: () => {}, + }, + consumeTokenDeps: { + consumeAnilistSetupCallbackUrl: () => false, + saveToken: () => {}, + setCachedToken: () => {}, + setResolvedState: () => {}, + setSetupPageOpened: () => {}, + onSuccess: () => {}, + closeWindow: () => {}, + }, + handleProtocolDeps: { + consumeAnilistSetupTokenFromUrl: () => false, + logWarn: () => {}, + }, + registerProtocolClientDeps: { + isDefaultApp: () => false, + getArgv: () => [], + execPath: process.execPath, + resolvePath: (value) => value, + setAsDefaultProtocolClient: () => true, + logWarn: () => {}, + }, + }); + + assert.equal(typeof composed.notifyAnilistSetup, 'function'); + assert.equal(typeof composed.consumeAnilistSetupTokenFromUrl, 'function'); + assert.equal(typeof composed.handleAnilistSetupProtocolUrl, 'function'); + assert.equal(typeof composed.registerSubminerProtocolClient, 'function'); +}); diff --git a/src/main/runtime/composers/anilist-setup-composer.ts b/src/main/runtime/composers/anilist-setup-composer.ts new file mode 100644 index 0000000..c75df3c --- /dev/null +++ b/src/main/runtime/composers/anilist-setup-composer.ts @@ -0,0 +1,55 @@ +import { + createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler, + createBuildHandleAnilistSetupProtocolUrlMainDepsHandler, + createBuildNotifyAnilistSetupMainDepsHandler, + createBuildRegisterSubminerProtocolClientMainDepsHandler, + createConsumeAnilistSetupTokenFromUrlHandler, + createHandleAnilistSetupProtocolUrlHandler, + createNotifyAnilistSetupHandler, + createRegisterSubminerProtocolClientHandler, +} from '../domains/anilist'; + +type NotifyHandler = ReturnType; +type ConsumeHandler = ReturnType; +type HandleProtocolHandler = ReturnType; +type RegisterClientHandler = ReturnType; + +export type AnilistSetupComposerOptions = { + notifyDeps: Parameters[0]; + consumeTokenDeps: Parameters[0]; + handleProtocolDeps: Parameters[0]; + registerProtocolClientDeps: Parameters< + typeof createBuildRegisterSubminerProtocolClientMainDepsHandler + >[0]; +}; + +export type AnilistSetupComposerResult = { + notifyAnilistSetup: NotifyHandler; + consumeAnilistSetupTokenFromUrl: ConsumeHandler; + handleAnilistSetupProtocolUrl: HandleProtocolHandler; + registerSubminerProtocolClient: RegisterClientHandler; +}; + +export function composeAnilistSetupHandlers( + options: AnilistSetupComposerOptions, +): AnilistSetupComposerResult { + const notifyAnilistSetup = createNotifyAnilistSetupHandler( + createBuildNotifyAnilistSetupMainDepsHandler(options.notifyDeps)(), + ); + const consumeAnilistSetupTokenFromUrl = createConsumeAnilistSetupTokenFromUrlHandler( + createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler(options.consumeTokenDeps)(), + ); + const handleAnilistSetupProtocolUrl = createHandleAnilistSetupProtocolUrlHandler( + createBuildHandleAnilistSetupProtocolUrlMainDepsHandler(options.handleProtocolDeps)(), + ); + const registerSubminerProtocolClient = createRegisterSubminerProtocolClientHandler( + createBuildRegisterSubminerProtocolClientMainDepsHandler(options.registerProtocolClientDeps)(), + ); + + return { + notifyAnilistSetup, + consumeAnilistSetupTokenFromUrl, + handleAnilistSetupProtocolUrl, + registerSubminerProtocolClient, + }; +} diff --git a/src/main/runtime/composers/jellyfin-remote-composer.test.ts b/src/main/runtime/composers/jellyfin-remote-composer.test.ts new file mode 100644 index 0000000..32782a9 --- /dev/null +++ b/src/main/runtime/composers/jellyfin-remote-composer.test.ts @@ -0,0 +1,34 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { composeJellyfinRemoteHandlers } from './jellyfin-remote-composer'; + +test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers', () => { + let lastProgressAt = 0; + const composed = composeJellyfinRemoteHandlers({ + getConfiguredSession: () => null, + getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never, + getJellyfinConfig: () => ({ enabled: false }) as never, + playJellyfinItem: async () => {}, + logWarn: () => {}, + getMpvClient: () => null, + sendMpvCommand: () => {}, + jellyfinTicksToSeconds: () => 0, + getActivePlayback: () => null, + clearActivePlayback: () => {}, + getSession: () => null, + getNow: () => 0, + getLastProgressAtMs: () => lastProgressAt, + setLastProgressAtMs: (next) => { + lastProgressAt = next; + }, + progressIntervalMs: 3000, + ticksPerSecond: 10_000_000, + logDebug: () => {}, + }); + + assert.equal(typeof composed.reportJellyfinRemoteProgress, 'function'); + assert.equal(typeof composed.reportJellyfinRemoteStopped, 'function'); + assert.equal(typeof composed.handleJellyfinRemotePlay, 'function'); + assert.equal(typeof composed.handleJellyfinRemotePlaystate, 'function'); + assert.equal(typeof composed.handleJellyfinRemoteGeneralCommand, 'function'); +}); diff --git a/src/main/runtime/composers/jellyfin-remote-composer.ts b/src/main/runtime/composers/jellyfin-remote-composer.ts new file mode 100644 index 0000000..defde3f --- /dev/null +++ b/src/main/runtime/composers/jellyfin-remote-composer.ts @@ -0,0 +1,124 @@ +import { + createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler, + createBuildHandleJellyfinRemotePlayMainDepsHandler, + createBuildHandleJellyfinRemotePlaystateMainDepsHandler, + createBuildReportJellyfinRemoteProgressMainDepsHandler, + createBuildReportJellyfinRemoteStoppedMainDepsHandler, + createHandleJellyfinRemoteGeneralCommand, + createHandleJellyfinRemotePlay, + createHandleJellyfinRemotePlaystate, + createReportJellyfinRemoteProgressHandler, + createReportJellyfinRemoteStoppedHandler, +} from '../domains/jellyfin'; + +type RemotePlayPayload = Parameters>[0]; +type RemotePlaystatePayload = Parameters>[0]; +type RemoteGeneralPayload = Parameters>[0]; + +export type JellyfinRemoteComposerOptions = { + getConfiguredSession: Parameters[0]['getConfiguredSession']; + getClientInfo: Parameters[0]['getClientInfo']; + getJellyfinConfig: Parameters[0]['getJellyfinConfig']; + playJellyfinItem: Parameters[0]['playJellyfinItem']; + logWarn: Parameters[0]['logWarn']; + getMpvClient: Parameters[0]['getMpvClient']; + sendMpvCommand: Parameters[0]['sendMpvCommand']; + jellyfinTicksToSeconds: Parameters< + typeof createBuildHandleJellyfinRemotePlaystateMainDepsHandler + >[0]['jellyfinTicksToSeconds']; + getActivePlayback: Parameters[0]['getActivePlayback']; + clearActivePlayback: Parameters[0]['clearActivePlayback']; + getSession: Parameters[0]['getSession']; + getNow: Parameters[0]['getNow']; + getLastProgressAtMs: Parameters< + typeof createBuildReportJellyfinRemoteProgressMainDepsHandler + >[0]['getLastProgressAtMs']; + setLastProgressAtMs: Parameters< + typeof createBuildReportJellyfinRemoteProgressMainDepsHandler + >[0]['setLastProgressAtMs']; + progressIntervalMs: number; + ticksPerSecond: number; + logDebug: Parameters[0]['logDebug']; +}; + +export type JellyfinRemoteComposerResult = { + reportJellyfinRemoteProgress: ReturnType; + reportJellyfinRemoteStopped: ReturnType; + handleJellyfinRemotePlay: (payload: RemotePlayPayload) => Promise; + handleJellyfinRemotePlaystate: (payload: RemotePlaystatePayload) => Promise; + handleJellyfinRemoteGeneralCommand: (payload: RemoteGeneralPayload) => Promise; +}; + +export function composeJellyfinRemoteHandlers( + options: JellyfinRemoteComposerOptions, +): JellyfinRemoteComposerResult { + const buildReportJellyfinRemoteProgressMainDepsHandler = + createBuildReportJellyfinRemoteProgressMainDepsHandler({ + getActivePlayback: options.getActivePlayback, + clearActivePlayback: options.clearActivePlayback, + getSession: options.getSession, + getMpvClient: options.getMpvClient, + getNow: options.getNow, + getLastProgressAtMs: options.getLastProgressAtMs, + setLastProgressAtMs: options.setLastProgressAtMs, + progressIntervalMs: options.progressIntervalMs, + ticksPerSecond: options.ticksPerSecond, + logDebug: options.logDebug, + }); + const buildReportJellyfinRemoteStoppedMainDepsHandler = + createBuildReportJellyfinRemoteStoppedMainDepsHandler({ + getActivePlayback: options.getActivePlayback, + clearActivePlayback: options.clearActivePlayback, + getSession: options.getSession, + logDebug: options.logDebug, + }); + const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler( + buildReportJellyfinRemoteProgressMainDepsHandler(), + ); + const reportJellyfinRemoteStopped = createReportJellyfinRemoteStoppedHandler( + buildReportJellyfinRemoteStoppedMainDepsHandler(), + ); + + const buildHandleJellyfinRemotePlayMainDepsHandler = + createBuildHandleJellyfinRemotePlayMainDepsHandler({ + getConfiguredSession: options.getConfiguredSession, + getClientInfo: options.getClientInfo, + getJellyfinConfig: options.getJellyfinConfig, + playJellyfinItem: options.playJellyfinItem, + logWarn: options.logWarn, + }); + const buildHandleJellyfinRemotePlaystateMainDepsHandler = + createBuildHandleJellyfinRemotePlaystateMainDepsHandler({ + getMpvClient: options.getMpvClient as Parameters< + typeof createBuildHandleJellyfinRemotePlaystateMainDepsHandler + >[0]['getMpvClient'], + sendMpvCommand: options.sendMpvCommand, + reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force), + reportJellyfinRemoteStopped: () => reportJellyfinRemoteStopped(), + jellyfinTicksToSeconds: options.jellyfinTicksToSeconds, + }); + const buildHandleJellyfinRemoteGeneralCommandMainDepsHandler = + createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler({ + getMpvClient: options.getMpvClient as Parameters< + typeof createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler + >[0]['getMpvClient'], + sendMpvCommand: options.sendMpvCommand, + getActivePlayback: options.getActivePlayback, + reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force), + logDebug: (message) => options.logDebug(message, undefined), + }); + + return { + reportJellyfinRemoteProgress, + reportJellyfinRemoteStopped, + handleJellyfinRemotePlay: createHandleJellyfinRemotePlay( + buildHandleJellyfinRemotePlayMainDepsHandler(), + ), + handleJellyfinRemotePlaystate: createHandleJellyfinRemotePlaystate( + buildHandleJellyfinRemotePlaystateMainDepsHandler(), + ), + handleJellyfinRemoteGeneralCommand: createHandleJellyfinRemoteGeneralCommand( + buildHandleJellyfinRemoteGeneralCommandMainDepsHandler(), + ), + }; +}