Files
SubMiner/docs/architecture.md

5.1 KiB

Architecture

SubMiner uses a service-oriented Electron main-process architecture where src/main.ts acts as the composition root and behavior lives in small runtime services under src/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

Current Structure

  • src/main.ts
    • Composition root for lifecycle wiring and non-overlay runtime state.
    • Owns long-lived process state for trackers, runtime flags, and client instances.
    • Delegates behavior to services.
  • src/core/services/overlay-manager-service.ts
    • Owns overlay/window state (mainWindow, invisibleWindow, visible/invisible overlay flags).
    • Provides a narrow state API used by main.ts and overlay services.
  • src/core/services/*
    • Stateless or narrowly stateful units for a specific responsibility.
    • Examples: startup bootstrap/ready flow, app lifecycle wiring, CLI command handling, IPC registration, overlay visibility, MPV IPC behavior, shortcut registration, subtitle websocket, jimaku/subsync helpers.
  • src/core/utils/*
    • Pure helpers and coercion/config utilities.
  • src/cli/*
    • CLI parsing and help output.
  • src/config/*
    • Config schema/definitions, defaults, validation, and template generation.
  • src/window-trackers/*
    • Backend-specific tracker implementations plus selection index.
  • src/jimaku/*, src/subsync/*
    • Domain-specific integration helpers.

Flow Diagram

flowchart TD
  Main["src/main.ts\n(composition root)"] --> Startup["runStartupBootstrapRuntimeService"]
  Main --> Lifecycle["startAppLifecycleService"]
  Lifecycle --> AppReady["runAppReadyRuntimeService"]

  Main --> OverlayMgr["overlay-manager-service"]
  Main --> Ipc["ipc-service / ipc-command-service"]
  Main --> Mpv["mpv-service / mpv-control-service"]
  Main --> Shortcuts["shortcut-service / overlay-shortcut-service"]
  Main --> RuntimeOpts["runtime-options-ipc-service"]
  Main --> Subtitle["subtitle-ws-service / secondary-subtitle-service"]

  Main --> Config["src/config/*"]
  Main --> Cli["src/cli/*"]
  Main --> Trackers["src/window-trackers/*"]
  Main --> Integrations["src/jimaku/* + src/subsync/*"]

  OverlayMgr --> OverlayWindow["overlay-window-service"]
  OverlayMgr --> OverlayVisibility["overlay-visibility-service"]
  Mpv --> Subtitle
  Ipc --> RuntimeOpts
  Shortcuts --> OverlayMgr

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 main.ts; extract an adapter/helper only when it adds meaningful behavior or reuse.
  4. Call the service from lifecycle/command wiring points.

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

Lifecycle Model

  • Startup:
    • runStartupBootstrapRuntimeService handles initial argv/env/backend setup and decides generate-config flow vs app lifecycle start.
    • app-lifecycle-service handles Electron single-instance + lifecycle event registration.
    • runAppReadyRuntimeService performs ready-time initialization (config load, websocket policy, tokenizer/tracker setup, overlay auto-init decisions).
  • Runtime:
    • CLI/shortcut/IPC events map to service calls.
    • Overlay and MPV state sync through dedicated services.
    • Runtime options and mining flows are coordinated via service boundaries.
  • Shutdown:
    • startAppLifecycleService registers cleanup hooks (will-quit) while teardown behavior stays delegated to focused services from main.ts.
flowchart LR
  Args["CLI args"] --> Bootstrap["runStartupBootstrapRuntimeService"]
  Bootstrap -->|generate-config| Exit["exit"]
  Bootstrap -->|normal start| AppLifecycle["startAppLifecycleService"]
  AppLifecycle --> Ready["runAppReadyRuntimeService"]
  Ready --> Runtime["IPC + shortcuts + mpv events"]
  Runtime --> Overlay["overlay visibility + mining actions"]
  Runtime --> Subsync["subsync + secondary sub flows"]
  Runtime --> WillQuit["app will-quit"]
  WillQuit --> Cleanup["service-level cleanup + unregister"]

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.

Extension Rules

  • Add behavior to an existing service or a new src/core/services/* file, not as ad-hoc logic in main.ts.
  • Keep service APIs explicit and narrowly scoped.
  • Prefer additive changes that preserve existing CLI flags and IPC channel behavior.
  • Add/update unit tests for each service extraction or behavior change.
  • For cross-cutting changes, extract-first then refactor internals after parity is verified.