- Pages now start with setup and usage, and reference material is in compact tables - Configuration reference gives each config block a short explanation and a key/default table - Internal detail removed from user pages, and docs that had drifted from current behavior fixed - The status line shows today's date, set on the client, instead of the page's last-updated date - Add changelog fragment
6.4 KiB
IPC + runtime contracts
The Electron main and renderer processes talk only through IPC channels. The renderer is an untrusted surface: it loads Yomitan and renders subtitle text SubMiner did not write. Every payload that crosses the bridge goes through a validator before domain code sees it.
Channel names, payload validators, the preload bridge, and the handler change together. When you touch an IPC surface, update all four in the same commit.
Message flow
Renderer calls pass through the preload bridge, the main-process handler, and a validator before they reach a service. Malformed payloads stop at the validator.
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
IPC_CHANNELS in src/shared/ipc/contracts.ts groups channels by pattern:
request: invoke channels. The renderer awaits a result, for example lookups, config reads, and mining actions. Invalid payloads return a structured failure such as{ ok: false, ... }instead of throwing.command: fire-and-forget sends, for example focus events, UI state hints, and position updates. Invalid payloads are dropped.event: messages pushed from main to the renderer.
Runtime sockets
The bridge above lives inside the Electron app. Separate OS sockets connect the app to mpv and to the launcher and plugin. They carry no renderer payloads and do not go through the contract and validator layer.
- mpv IPC socket:
/tmp/subminer-socket, or\\.\pipe\subminer-socketon Windows. mpv creates it with--input-ipc-server. The app'sMpvIpcClientsends JSON commands here and observes playback and subtitle properties. - App control socket:
subminer-control-<uid>-<hash>.sockin the temp directory, or\\.\pipe\subminer-control-<hash>on Windows. The launcher and plugin send CLI-style commands (--start,--show-visible-overlay,--texthooker) to a running app here. It also routes a secondsubminerinvocation into the existing instance.
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
Playback startup flow shows when each socket comes up during a launch.
Core files
| File | Role |
|---|---|
src/shared/ipc/contracts.ts |
Channel names and payload types, shared by both processes |
src/shared/ipc/validators.ts |
Runtime payload parsers and type guards |
src/preload.ts |
Typed renderer API; only approved channels are exposed |
src/core/services/ipc.ts |
Registers overlay handlers and validates payloads before calling domain logic |
src/core/services/anki-jimaku-ipc.ts |
Same boundary for Anki and Jimaku operations |
src/main/ipc-runtime.ts |
Builds handler dependencies (via src/main/dependencies.ts) and registers handlers |
src/main/cli-runtime.ts |
Handles commands from the launcher or mpv plugin, not the renderer |
Contract rules
- Use the shared constants. Take channel names from
contracts.ts, never string literals. - Validate before handling. Every renderer payload goes through
validators.tsbefore domain logic. - Return structured failures. Invoke handlers return
{ ok: false, ... }on failure instead of throwing, so the renderer can tell success from failure without try/catch. - Keep payloads narrow. Send only what the handler needs, not whole state objects.
- Keep handlers thin. Validate, delegate to a service or composer, return. Route shared state changes through the transition helpers in
src/main/state.ts.
Add a new IPC action
- Add the channel constant in
src/shared/ipc/contracts.ts. - Add or extend the validator in
src/shared/ipc/validators.ts. - Expose a typed bridge method in
src/preload.ts. - Register the handler in
src/core/services/ipc.ts(oranki-jimaku-ipc.ts), and supply any new dependency throughsrc/main/ipc-runtime.ts. - Test valid and malformed payloads in
src/core/services/*. - Update renderer tests if behavior or state transitions change.
Troubleshooting
- Handler receives an unexpected payload: the validator is not applied. Check that the channel is registered through the IPC service with validation, not directly.
- Renderer invoke fails: check that the preload method exists, uses the right channel constant, and that the handler is registered and returns instead of throwing.
- Invoke returns an unexpected shape: compare the contract, validator, preload method, and handler side by side. One of them changed without the others.