Files
SubMiner/docs/architecture.md
sudacode 1d3f099e44 docs: refresh architecture and development docs
Update docs content to match current launcher/plugin/runtime structure and fix stale home demo media assets with cache-busted URLs plus poster refresh. Add supporting backlog/subagent tracking records and docs asset regression coverage.
2026-02-22 20:25:55 -08:00

19 KiB

Architecture

SubMiner is split into three cooperating runtimes:

  • Electron desktop app (src/) for overlay/UI/runtime orchestration.
  • Launcher CLI (launcher/) for mpv/app command workflows.
  • mpv Lua plugin (plugin/subminer.lua) for player-side controls and IPC handoff.

Within the desktop app, src/main.ts is a composition root that wires small runtime/domain modules plus core services.

Goals

  • Keep behavior stable while reducing coupling.
  • Prefer small, single-purpose units that can be tested in isolation.
  • Keep main.ts focused on wiring and state ownership, not implementation detail.
  • Follow Unix-style composability:
    • each service does one job
    • services compose through explicit inputs/outputs
    • orchestration is separate from implementation

Project Structure

launcher/                 # Standalone CLI launcher wrapper and mpv helpers
  commands/               # Command modules (doctor/config/mpv/jellyfin/playback/app passthrough)
  config/                 # Launcher config parsers + CLI parser builder
  main.ts                 # Launcher entrypoint and command dispatch
plugin/
  subminer.lua            # mpv plugin (auto-start, IPC, AniSkip + hover controls)
src/
  main-entry.ts           # Background-mode bootstrap wrapper before loading main.js
  main.ts                  # Entry point — delegates to runtime composers/domain modules
  preload.ts               # Electron preload bridge
  types.ts                 # Shared type definitions
  main/                    # Main-process composition/runtime adapters
    app-lifecycle.ts       # App lifecycle + app-ready runtime runner factories
    cli-runtime.ts         # CLI command runtime service adapters
    config-validation.ts   # Startup/hot-reload config error formatting and fail-fast helpers
    dependencies.ts        # Shared dependency builders for IPC/runtime services
    ipc-runtime.ts         # IPC runtime registration wrappers
    overlay-runtime.ts     # Overlay modal routing + active-window selection
    overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling
    overlay-visibility-runtime.ts # Overlay visibility + tracker-driven bounds service
    frequency-dictionary-runtime.ts # Frequency dictionary runtime adapter
    jlpt-runtime.ts         # JLPT dictionary runtime adapter
    media-runtime.ts        # Media path/title/subtitle-position runtime service
    startup.ts              # Startup bootstrap dependency builder
    startup-lifecycle.ts    # Lifecycle runtime runner adapter
    state.ts                # Application runtime state container + reducer transitions
    subsync-runtime.ts      # Subsync command runtime adapter
    runtime/
      composers/            # High-level composition clusters used by main.ts
      domains/              # Domain barrel exports (startup/overlay/mpv/jellyfin/...)
      registry.ts           # Domain registry consumed by main.ts
  core/
    services/              # Focused runtime services (Electron adapters + pure logic)
      anilist/             # AniList token store/update queue/update helpers
      immersion-tracker/   # Immersion persistence/session/metadata modules
      tokenizer/           # Tokenizer stage modules (selection/enrichment/annotation)
    utils/                 # Pure helpers and coercion/config utilities
  cli/                     # CLI parsing and help output
  config/                  # Config defaults/definitions, loading, parse, resolution pipeline
    definitions/           # Domain-specific defaults + option registries
    resolve/               # Domain-specific config resolution pipeline stages
  shared/ipc/              # Cross-process IPC channel constants + payload validators
  renderer/                # Overlay renderer (modularized UI/runtime)
    handlers/              # Keyboard/mouse interaction modules
    modals/                # Jimaku/Kiku/subsync/runtime-options/session-help modals
    positioning/           # Invisible-layer layout + offset controllers
  window-trackers/         # Backend-specific tracker implementations (Hyprland, Sway, X11, macOS)
  jimaku/                  # Jimaku API integration helpers
  subsync/                 # Subtitle sync (alass/ffsubsync) helpers
  subtitle/                # Subtitle processing utilities
  tokenizers/              # Tokenizer implementations
  token-mergers/           # Token merge strategies
  translators/             # AI translation providers

Service Layer (src/core/services/)

  • Overlay/window runtime: overlay-manager.ts, overlay-window.ts, overlay-window-geometry.ts, overlay-visibility.ts, overlay-bridge.ts, overlay-runtime-init.ts, overlay-content-measurement.ts, overlay-drop.ts
  • Shortcuts/input: shortcut.ts, overlay-shortcut.ts, overlay-shortcut-handler.ts, shortcut-fallback.ts, numeric-shortcut.ts
  • MPV runtime: mpv.ts, mpv-transport.ts, mpv-protocol.ts, mpv-properties.ts, mpv-render-metrics.ts
  • Mining + Anki/Jimaku runtime: mining.ts, field-grouping.ts, field-grouping-overlay.ts, anki-jimaku.ts, anki-jimaku-ipc.ts
  • Subtitle/token pipeline: subtitle-processing-controller.ts, subtitle-position.ts, subtitle-ws.ts, tokenizer.ts + tokenizer/* stage modules
  • Integrations: jimaku.ts, subsync.ts, subsync-runner.ts, texthooker.ts, jellyfin.ts, jellyfin-remote.ts, discord-presence.ts, yomitan-extension-loader.ts, yomitan-settings.ts
  • Config/runtime controls: config-hot-reload.ts, runtime-options-ipc.ts, cli-command.ts, startup.ts
  • Domain submodules: anilist/* (token/update queue/updater), immersion-tracker/* (storage/session/metadata/query/reducer)

Renderer Layer (src/renderer/)

The renderer keeps renderer.ts focused on orchestration. UI behavior is delegated to per-concern modules.

src/renderer/
  renderer.ts              # Entrypoint/orchestration only
  context.ts               # Shared runtime context contract
  state.ts                 # Centralized renderer mutable state
  error-recovery.ts        # Global renderer error boundary + recovery actions
  overlay-content-measurement.ts # Reports rendered bounds to main process
  subtitle-render.ts       # Primary/secondary subtitle rendering + style application
  positioning.ts           # Facade export for positioning controller
  positioning/
    controller.ts          # Position controller orchestration
    invisible-layout*.ts   # Invisible layer layout computations
    position-state.ts      # Position state helpers
  handlers/
    keyboard.ts            # Keybindings, chord handling, modal key routing
    mouse.ts               # Hover/drag behavior, selection + observer wiring
  modals/
    jimaku.ts              # Jimaku modal flow
    kiku.ts                # Kiku field-grouping modal flow
    runtime-options.ts     # Runtime options modal flow
    session-help.ts        # Keyboard shortcuts/help modal flow
    subsync.ts             # Manual subsync modal flow
  utils/
    dom.ts                 # Required DOM lookups + typed handles
    platform.ts            # Layer/platform capability detection

Launcher + Plugin Runtimes

  • launcher/main.ts dispatches commands through launcher/commands/* and shared config readers in launcher/config/*. It handles mpv startup, app passthrough, Jellyfin helper commands, and playback handoff.
  • plugin/subminer.lua runs inside mpv and handles IPC startup checks, overlay toggles, hover-token messages, and AniSkip intro-skip UX.

Flow Diagram

The main process has three layers: main.ts delegates to composition modules that wire together domain services. The renderer runs in a separate Electron process, connected through preload.ts.

flowchart TD
  classDef entry fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
  classDef comp fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef svc fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef bridge fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef rend fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef ext fill:#a6da95,stroke:#363a4f,color:#24273a,stroke-width:1.5px

  Main["main.ts"]:::entry

  subgraph Comp["Composition — src/main/"]
    Startup["Startup & Lifecycle<br/>startup · app-lifecycle<br/>startup-lifecycle · state"]:::comp
    Wiring["Runtime Wiring<br/>ipc-runtime · cli-runtime<br/>overlay-runtime · subsync-runtime"]:::comp
  end

  subgraph Svc["Services — src/core/services/"]
    direction LR
    Mpv["MPV Stack<br/>transport · protocol<br/>state · properties"]:::svc
    Overlay["Overlay<br/>manager · window<br/>visibility · bridge"]:::svc
    Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
    Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
  end

  Bridge(["preload.ts — Electron IPC"]):::bridge

  subgraph Rend["Renderer — src/renderer/"]
    Orchestration["renderer.ts<br/>orchestration · IPC wiring"]:::rend
    UI["subtitle-render · positioning<br/>handlers · modals"]:::rend
  end

  subgraph Ext["External Systems"]
    direction LR
    mpv["mpv"]:::ext
    Anki["AnkiConnect"]:::ext
    Jimaku["Jimaku API"]:::ext
    Tracker["Window Tracker"]:::ext
  end

  Main -->|delegates| Comp
  Startup -->|initializes| Svc
  Wiring -->|dispatches to| Svc

  Overlay <--> Bridge
  Mining <--> Bridge
  Bridge <--> Orchestration
  Orchestration --> UI

  Mpv <-->|JSON socket| mpv
  Mining -->|HTTP| Anki
  Integrations -->|HTTP| Jimaku
  Overlay --> Tracker

  style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
  style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
  style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5
  style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5

Composition Pattern

Most runtime code follows a dependency-injection pattern:

  1. Define a service interface in src/core/services/*.
  2. Keep core logic in pure or side-effect-bounded functions.
  3. Build runtime deps in src/main/ composition modules; extract an adapter/helper only when it adds meaningful behavior or reuse.
  4. Call the service from lifecycle/command wiring points.

The composition root (src/main.ts) delegates to focused modules in src/main/ and src/main/runtime/composers/:

  • startup.ts — argv/env processing and bootstrap flow
  • app-lifecycle.ts — Electron lifecycle event registration
  • startup-lifecycle.ts — app-ready initialization sequence
  • state.ts — centralized application runtime state container
  • ipc-runtime.ts — IPC channel registration and handler wiring
  • cli-runtime.ts — CLI command parsing and dispatch
  • overlay-runtime.ts — overlay window selection and modal state management
  • subsync-runtime.ts — subsync command orchestration
  • runtime/composers/anilist-tracking-composer.ts — AniList media tracking/probe/retry wiring
  • runtime/composers/jellyfin-runtime-composer.ts — Jellyfin config/client/playback/command/setup composition wiring
  • runtime/composers/mpv-runtime-composer.ts — MPV event/factory/tokenizer/warmup wiring

Composer modules share contract conventions via src/main/runtime/composers/contracts.ts:

  • composer input surfaces are declared with ComposerInputs<T> so required dependencies cannot be omitted at compile time
  • composer outputs are declared with ComposerOutputs<T> to keep result contracts explicit and stable
  • builder return payload extraction should use shared type helpers instead of inline ad-hoc inference

This keeps side effects explicit and makes behavior easy to unit-test with fakes.

Additional conventions in the current code:

  • main.ts uses createMainRuntimeRegistry() (src/main/runtime/registry.ts) to access domain handlers (startup, overlay, mpv, ipc, shortcuts, anilist, jellyfin, mining) without importing every runtime module directly.
  • Domain barrels in src/main/runtime/domains/* re-export runtime handlers + main-deps builders, while composers in src/main/runtime/composers/* assemble larger runtime clusters.
  • Many runtime handlers accept *MainDeps objects generated by createBuild*MainDepsHandler builders to isolate side effects and keep units testable.

IPC Contract + Validation Boundary

  • Central channel constants live in src/shared/ipc/contracts.ts and are consumed by both main (ipcMain) and renderer preload (ipcRenderer) wiring.
  • Runtime payload parsers/type guards live in src/shared/ipc/validators.ts.
  • Rule: renderer-supplied payloads must be validated at IPC entry points (src/core/services/ipc.ts, src/core/services/anki-jimaku-ipc.ts) before calling domain handlers.
  • Malformed invoke payloads return explicit structured errors (for example { ok: false, error: ... }) and malformed fire-and-forget payloads are ignored safely.

Runtime State Ownership (Migrated Domains)

For domains migrated to reducer-style transitions (for example AniList token/queue/media-guess runtime state), follow these rules:

  • Composition/runtime modules own mutable state cells and expose narrow get*/set* accessors.
  • Domain handlers do not mutate foreign state directly; they call explicit transition helpers that encode invariants.
  • Transition helpers may sync derived counters/snapshots, but must preserve non-owned metadata unless the transition explicitly owns that metadata.
  • Reducer boundary: when a domain has transition helpers in src/main/state.ts, new callsites should route updates through those helpers instead of ad-hoc object mutation in main.ts or composers.
  • Tests for migrated domains should assert both the intended field changes and non-targeted field invariants.

Program Lifecycle

  • Startup: startup.ts parses CLI args and detects the compositor backend. If --generate-config is passed, it writes the template and exits. Otherwise app-lifecycle.ts acquires the single-instance lock and registers Electron lifecycle hooks.
  • Initialization: Once app.whenReady() fires, composeAppReadyRuntime() runs the critical path first (strict config reload, runtime options + keybindings, mpv client creation, overlay/IPC setup). Non-critical warmups are launched asynchronously (mecab, yomitan-extension, dictionary prewarm, optional Jellyfin remote session).
  • Runtime: Event-driven. mpv events, IPC messages, CLI commands, overlay shortcuts, hot-reload notifications, and integration callbacks route through runtime handlers/composers, update AppState, and broadcast to overlay windows.
  • Overlay window model: runtime manages three overlay windows: visible, invisible, and secondary. splitOverlayGeometryForSecondaryBar() reserves the top 20% for the secondary subtitle bar and routes the remaining area to the active primary overlay layer.
  • Shutdown: onWillQuitCleanup tears down tray + watchers + integrations, stops subtitle/texthooker servers, flushes buffered MPV OSD log writes, closes token/session windows, and stops Jellyfin/Discord runtime services.
flowchart TD
  classDef start fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
  classDef phase fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef decision fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef init fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef runtime fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
  classDef shutdown fill:#ed8796,stroke:#363a4f,color:#24273a,stroke-width:1.5px

  CLI["CLI args & environment"]:::start
  CLI --> Parse["startup.ts<br/>Parse argv · detect backend · resolve config"]:::phase
  Parse --> GenCheck{"--generate-config?"}:::decision
  GenCheck -->|yes| GenExit["Write config template & exit"]:::phase
  GenCheck -->|no| Lifecycle["app-lifecycle.ts<br/>Acquire single-instance lock<br/>Register Electron lifecycle hooks"]:::phase
  Lifecycle -->|"app.whenReady()"| Ready["startup-lifecycle.ts"]:::phase

  Ready --> Init
  subgraph Init["Initialization"]
    direction LR
    Config["Load config<br/>resolve keybindings"]:::init
    Runtime["Create mpv client<br/>init runtime options"]:::init
    Platform["Start window tracker<br/>WebSocket policy"]:::init
  end

  Init --> Create["Create overlay window<br/>Establish IPC bridge"]:::phase
  Create --> Warm["Background warmups<br/>MeCab · Yomitan · dictionaries · Jellyfin"]:::phase

  Warm --> Loop
  subgraph Loop["Runtime — event-driven"]
    direction LR
    Events["mpv · IPC · CLI<br/>shortcut events"]:::runtime
    Dispatch["Route to service<br/>via composition layer"]:::runtime
    State["Update state<br/>broadcast to renderer"]:::runtime
    Events --> Dispatch --> State
  end

  Loop -->|"app close"| Quit["Electron will-quit"]:::shutdown
  Quit --> Teardown["Close mpv socket · unregister shortcuts<br/>Stop WebSocket & texthooker<br/>Destroy tracker · clean Anki state"]:::shutdown

  style Init fill:#363a4f,stroke:#494d64,color:#cad3f5
  style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5

Why This Design

  • Smaller blast radius: changing one feature usually touches one service.
  • Better testability: most behavior can be tested without Electron windows/mpv.
  • Better reviewability: PRs can be scoped to one subsystem.
  • Backward compatibility: CLI flags and IPC channels can remain stable while internals evolve.
  • Runtime registry + domain barrels: src/main/runtime/registry.ts and src/main/runtime/domains/* reduce direct fan-in inside main.ts while keeping domain ownership explicit.
  • Extracted composition root: main.ts delegates to focused modules under src/main/ and src/main/runtime/composers/ for lifecycle, IPC, overlay, mpv, shortcut, and integration wiring.
  • Split MPV service layers: MPV internals are separated into transport (mpv-transport.ts), protocol (mpv-protocol.ts), and properties/render metrics modules for maintainability.
  • Config by domain: defaults, option registries, and resolution are split by domain under src/config/definitions/* and src/config/resolve/*, keeping config evolution localized.

Extension Rules

  • Add behavior to an existing service in src/core/services/* or create a focused runtime module under src/main/runtime/*; avoid ad-hoc logic in main.ts.
  • Add new cross-process channels in src/shared/ipc/contracts.ts first, validate payloads in src/shared/ipc/validators.ts, then wire handlers in IPC runtime modules.
  • If change spans startup/overlay/mpv/integration wiring, prefer composing through src/main/runtime/domains/* + src/main/runtime/composers/* rather than direct wiring in main.ts.
  • Keep service APIs explicit and narrowly scoped, and preserve existing CLI flag / IPC channel behavior unless the change is intentionally breaking.
  • Add or update focused tests (including malformed-payload IPC tests) when runtime boundaries or contracts change.