mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-09 17:16:19 -07:00
- Clarify current integration, media review, and Anki behavior - Remove obsolete AI translation and configuration references - Standardize headings and prose across docs
125 lines
7.9 KiB
Markdown
125 lines
7.9 KiB
Markdown
# IPC + runtime contracts
|
|
|
|
SubMiner's Electron app runs two isolated processes, main and renderer, and IPC channels are the only way they talk. That boundary is deliberate. The renderer is an untrusted surface: it loads Yomitan, renders subtitle text SubMiner did not write, and runs in a Chromium sandbox. Every message crossing the bridge goes through a validator before any domain code sees it.
|
|
|
|
Channel names, payload shapes, and validators all live together, so they change together. Touching an IPC surface means updating the contract, the validator, the preload bridge, and the handler in one commit. Drift between those four layers is a bug, not a style preference.
|
|
|
|
## Message flow
|
|
|
|
Renderer-initiated calls (`invoke`) pass through four boundaries before reaching a service. Fire-and-forget messages (`send`) follow the same path but skip the response leg. Malformed payloads are caught at the validator and never reach domain code.
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
classDef rend fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
classDef valid fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
classDef handler fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
classDef err fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
|
|
R["Renderer"]:::rend
|
|
P(["preload.ts"]):::bridge
|
|
M["ipcMain handler"]:::handler
|
|
V{"Validator"}:::valid
|
|
S["Service"]:::svc
|
|
E["Structured error"]:::err
|
|
|
|
R -->|"invoke / send"| P
|
|
P -->|"ipcRenderer"| M
|
|
M --> V
|
|
V -->|"valid"| S
|
|
V -->|"malformed"| E
|
|
S -->|"result"| P
|
|
E -->|"{ ok: false }"| P
|
|
P -->|"return"| R
|
|
|
|
style E fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
```
|
|
|
|
## Runtime sockets
|
|
|
|
The renderer↔main bridge above lives *inside* the Electron app. A separate set of OS sockets connects the app to the other runtimes - mpv and the launcher/plugin. These carry no renderer payloads and bypass the contract/validator layer; they are command and property channels between processes.
|
|
|
|
- **mpv IPC socket** (`/tmp/subminer-socket`, or `\\.\pipe\subminer-socket` on Windows): the `MpvIpcClient` in the main process connects here to send JSON commands and subscribe to playback/subtitle properties via `observe_property`. Created by mpv's `--input-ipc-server`.
|
|
- **App control socket** (`/tmp/subminer-control-<uid>-<hash>.sock`, or a named pipe on Windows): the launcher and the mpv plugin send CLI-style commands (`--start`, `--show-visible-overlay`, `--texthooker`) to a running app here. It also dedupes a second `subminer` invocation into the existing instance instead of launching twice.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
classDef app fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
classDef ext fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
|
|
subgraph MpvProc["mpv process"]
|
|
direction TB
|
|
Mpv["mpv core"]:::ext
|
|
Plugin["SubMiner plugin (Lua)"]:::extrt
|
|
end
|
|
|
|
Launcher["Launcher CLI"]:::extrt
|
|
App["SubMiner app (Electron main)"]:::app
|
|
|
|
App <-->|"mpv IPC socket · /tmp/subminer-socket<br/>JSON commands + property observe"| Mpv
|
|
Launcher -->|"app control socket · /tmp/subminer-control-*<br/>--start, --show-visible-overlay, …"| App
|
|
Plugin -->|"app control socket<br/>spawn / attach"| App
|
|
|
|
style MpvProc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
|
```
|
|
|
|
How these sockets are established during launch is covered in [Playback Startup Flow](./architecture#playback-startup-flow).
|
|
|
|
## Core surfaces
|
|
|
|
| File | Role |
|
|
| --- | --- |
|
|
| `src/shared/ipc/contracts.ts` | Canonical channel names and payload type contracts. Single source of truth for both processes. |
|
|
| `src/shared/ipc/validators.ts` | Runtime payload parsers and type guards. Every `invoke` payload is validated here before the handler runs. |
|
|
| `src/preload.ts` | Renderer-side bridge. Exposes a typed API surface to the renderer - only approved channels are accessible. |
|
|
| `src/main/ipc-runtime.ts` | Main-process handler registration and routing. Wires validated channels to domain handlers. |
|
|
| `src/core/services/ipc.ts` | Service-level invoke handling. Applies guardrails (validation, error wrapping) before calling domain logic. |
|
|
| `src/core/services/anki-jimaku-ipc.ts` | Integration-specific IPC boundary for Anki and Jimaku operations. |
|
|
| `src/main/cli-runtime.ts` | CLI/runtime command boundary. Handles commands that originate from the launcher or mpv plugin rather than the renderer. |
|
|
|
|
## Contract rules
|
|
|
|
These rules exist to prevent a class of bugs where the renderer and main process silently disagree about message shapes - which surfaces as undefined fields, swallowed errors, or state corruption.
|
|
|
|
- **Use shared constants.** Channel names come from `contracts.ts`, never ad-hoc literal strings. This makes channels greppable and refactor-safe.
|
|
- **Validate before handling.** Every `invoke` payload passes through `validators.ts` before reaching domain logic. This catches shape drift at the boundary instead of deep inside a service.
|
|
- **Return structured failures.** Handlers return `{ ok: false, error: string }` on failure rather than throwing. The renderer can always distinguish success from failure without try/catch.
|
|
- **Keep payloads narrow.** Send only what the handler needs. Avoid passing entire state objects across the bridge - it couples the renderer to internal main-process structure.
|
|
- **Co-evolve all layers.** When a payload shape changes, update `contracts.ts`, `validators.ts`, `preload.ts`, and the handler in the same commit. Partial updates are treated as bugs.
|
|
|
|
## Two message patterns
|
|
|
|
**Invoke (request/response):** The renderer calls a typed bridge method and awaits a result. The main process validates the payload, runs the handler, and returns a structured response. Used for operations where the renderer needs a result - lookups, config reads, mining actions.
|
|
|
|
**Fire-and-forget (send):** The renderer sends a message with no response. The main process validates and handles it silently. Malformed payloads are dropped. Used for notifications where the renderer doesn't need confirmation - UI state hints, focus events, position updates.
|
|
|
|
## Add a new IPC action
|
|
|
|
1. Add the channel constant in `src/shared/ipc/contracts.ts`.
|
|
2. Add or extend the payload validator in `src/shared/ipc/validators.ts`.
|
|
3. Expose a typed bridge method in `src/preload.ts`.
|
|
4. Register the handler in `src/main/ipc-runtime.ts` (or the relevant domain runtime module).
|
|
5. Add tests for both valid and malformed payload cases in `src/core/services/*`.
|
|
6. Update renderer tests when behavior or state transitions change.
|
|
|
|
## Runtime state notes
|
|
|
|
- Prefer runtime/domain composition via `src/main/runtime/composers/*` and `src/main/runtime/domains/*`. IPC handlers should delegate to composers rather than containing orchestration logic.
|
|
- Route shared mutable state updates through transition helpers in `src/main/state.ts` for migrated domains. Direct mutation from IPC handlers bypasses invariant checks.
|
|
- Keep IPC handlers thin - they validate, delegate, and return. Business logic belongs in services.
|
|
|
|
## Troubleshooting
|
|
|
|
- **Unknown payload in handler:** The validator is not being applied before the handler runs. Check that the channel is routed through `ipc-runtime.ts` with validation, not registered directly.
|
|
- **Renderer invoke fails:** Verify the preload bridge method exists and matches the channel constant. Check that the handler is registered and returning (not throwing).
|
|
- **Contract drift:** When invoke calls return unexpected shapes, compare the shared contract, validator, preload bridge, and main handler signatures side by side. One of them was updated without the others.
|
|
|
|
## Related docs
|
|
|
|
- [Architecture](/architecture)
|
|
- [Development](/development)
|
|
- [Configuration](/configuration)
|
|
- [Troubleshooting](/troubleshooting)
|