feat: make IPC fully type-safe with contract + typed wrappers#7
Open
skalkii wants to merge 1 commit into
Open
Conversation
Add a single source of truth for every IPC channel and route every
ipcMain.handle / ipcRenderer.invoke / webContents.send call through
generic wrappers that enforce channel name + args + return / payload
against the contract.
New files:
- src/shared/ipc-contract.ts — IpcInvokeChannels (channel → {args, return})
and IpcSendChannels (channel → payload) maps, with a TrayAction union
for the tray push payload.
- src/main/ipc-utils.ts — ipcMainHandle, ipcWebContentsSend, and a
best-effort validateEventFrame hook. Frame validation logs unexpected
origins (warn-only) so adoption is non-breaking; future hardening can
flip the warn into a throw once expected origins are confirmed.
- src/preload/ipc-utils.ts — ipcInvoke, ipcOn (returns unsubscribe).
Migrated:
- src/main/ipc-handlers.ts — every ipcMain.handle replaced with
ipcMainHandle; the unused _e parameter is dropped from each handler;
sendToRenderer is now generic over IpcSendChannels.
- src/preload/index.ts — every ipcRenderer.invoke / on replaced with
ipcInvoke / ipcOn. Listener boilerplate collapses to one line per
channel.
- src/main/index.ts — webContents.send('tray-action', ...) replaced with
ipcWebContentsSend.
No runtime behavior change. No files deleted. The four orphan/dead-listener
channels (app:logDir, idle-state, tray-action, new-summary) are typed in
the contract so call sites compile, but their feature wiring is left as-is
and tracked separately.
Fixes video-db#6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/shared/ipc-contract.ts).ipcMain.handle,ipcRenderer.invoke, andwebContents.sendthrough generic wrappers that enforce channel name + args + return/payload at the type level.validateEventFramehook (warn-only, non-breaking) that future PRs can flip to enforcing.Why
Pre-change IPC had partial type-safety:
FocusdAPIwas strongly typed.ipcMain.handle('foo:bar', ...)andipcRenderer.invoke('foo:bar', ...)were decoupled string literals; main-side handlers had no link toFocusdAPI's return types;sendToRendererwas(string, ...unknown[]); push-channel payloads had no central type.Changes
New
src/shared/ipc-contract.tsIpcInvokeChannels+IpcSendChannelsmaps;TrayActionunionsrc/main/ipc-utils.tsipcMainHandle<K>,ipcWebContentsSend<K>,validateEventFramesrc/preload/ipc-utils.tsipcInvoke<K>,ipcOn<K>(returns unsubscribe)Migrated
src/main/ipc-handlers.ts—ipcMain.handle→ipcMainHandle;_edropped from handler signatures;sendToRendereris now generic overIpcSendChannels.src/preload/index.ts—ipcRenderer.invoke/on→ipcInvoke/ipcOn. Listener boilerplate collapsed to one line per channel.src/main/index.ts—webContents.send('tray-action', …)→ipcWebContentsSend(...).Frame validation
validateEventFrameis wired into everyipcMainHandle. In dev it allows the electron-vite renderer URL andlocalhost; in production it allowsfile://URLs. Currently logs unexpected origins viawarn(...)rather than throwing, so the hook ships non-breaking. Future hardening can flip the warn into a throw once field telemetry shows no false positives.Out of scope
FocusdAPIvia mapped types — kept parallel by hand for now.app:logDir,idle-state,tray-action,new-summary). They're typed in the contract so call sites compile; feature changes are separate.ipcOn).validateEventFrameto throw — separate follow-up.Test plan
npx tsc --noEmit -p tsconfig.node.jsonand-p tsconfig.web.jsonproduce no new errors. (Two pre-existing errors insrc/main/services/capture.ts:308andsrc/main/services/config.ts:54are unrelated and predate this change.)npm run buildsucceeds.npm run preview— exercise onboarding (validate/save key, permission cards), capture (list screens, start, stop), settings (each toggle and numeric input), summaries (today + daily refresh). Verifyrecording-statepush reaches the renderer.npm run package:mac— confirms the deploy script unchanged.[IPC-UTIL] Unexpected frame URL: ...warnings — none expected during normal use.Fixes #6