Contributing Adding An IPC Channel - drizztdourden08/relic-of-the-past GitHub Wiki
The renderer reaches the main process only through IPC. Every channel is typed
end-to-end against a single contract in shared/ipc/, so the compiler keeps the
three sites (handler, preload, renderer) in sync without relying on convention. See
Electron & IPC for the architecture.
Golden rule: reach for the typed wrappers in
electron/lib/ipc/instead of callingipcMain/ipcRenderer/webContents.senddirectly. Those two files are the only place raw Electron IPC belongs.
| Direction | Contract | Main | Preload |
|---|---|---|---|
| Request → response | InvokeContract |
handle |
invoke |
| Renderer → main, fire-and-forget | SendContract |
on |
send |
| Main → renderer event | EventContract |
emit |
subscribe |
Channels are named domain:action (e.g. profiles:list, saves:normal:create).
This is the single source of truth for the signature:
'foo:get': (id: string) => Promise<FooResult>;Add one line to INVOKE_MAP, the only place the method↔channel link is written.
satisfies rejects a typo'd channel. IpcApi (the window.api type) and the
preload method are derived automatically, so env.d.ts stays untouched.
getFoo: 'foo:get',Use the typed handle; args and return type are inferred from the contract, so a
mismatch won't compile:
import { handle } from '../lib/ipc/handle';
const registerFooHandlers = (): void => {
handle('foo:get', (_event, id) => getFoo(id)); // id: string, must return Promise<FooResult>
};
export { registerFooHandlers };Register it in main.ts by adding to the IPC_HANDLERS list ({ register: registerFooHandlers },
or { register, devOnly: true } for dev-only tools).
Flat methods are generated from the maps by buildInvoke/buildSend/buildEvents
in preload.ts. The nested namespaces (updater, shadowCasting,
screenEditor) are wired by hand, so add there with the typed invoke/subscribe
if your channel belongs to one.
Call it from the renderer as window.api.getFoo(id). The types come through.
Add to EventContract (the value is the listener signature), then to
EVENT_MAP as onFooChange: 'foo:change'. Emit from main with
emit(win, 'foo:change', payload); the renderer gets window.api.onFooChange(cb)
returning an unsubscribe function (generated).
- Channel named
domain:action, with the correct direction (invoke/send/event). - Signature added to the matching contract in
shared/ipc/. - Method↔channel line added to the join map (flat methods), or wired in a
nested namespace (preload). Leave
env.d.tsalone. - Handler uses the typed
handle/on/emitand is registered inmain.ts. - Raw
ipcMain/ipcRenderer/webContents.sendstays insideelectron/lib/ipc/. -
npm run lintgreen (the contract makes any drift a type error). - No native module imported into the renderer, and no security flags weakened.