Back to Cherry Studio

IpcApi Schema Guide

docs/references/ipc/ipc-schema-guide.md

2.0.04.8 KB
Original Source

IpcApi Schema Guide

File Organization

One file per domain under src/shared/ipc/schemas/, each split into two blocks:

BlockFormDirectionTrust
Request (*RequestSchemas)zod values (defineRoute)renderer→mainuntrusted → parsed
Event (*EventSchemas)pure typesmain→renderertrusted → not parsed

A domain with no events simply omits the Event block. schemas/ipcSchemas.ts composes the per-domain pieces:

ts
export const ipcRequestSchemas = { ...windowRequestSchemas, ...appRequestSchemas } satisfies Record<string, RouteDef>
export type IpcRequestSchemas = typeof ipcRequestSchemas
export type IpcRoute = keyof IpcRequestSchemas

export type IpcEventSchemas = WindowEventSchemas & AppEventSchemas
export type IpcEventName = keyof IpcEventSchemas

Naming

ElementRuleExample
Route namedot snake_case namespace[.subdomain…].actionfile.read_doc, window.main.set_minimum_size, ai.agent.task.create
Event namedot snake_case namespace[.subdomain…].eventwindow.maximized_changed, ai.stream.chunk
Payload fieldsJS camelCase (snake constrains only the route/event string){ minWidth: number }
Request value/type*RequestSchemas / IpcRequestSchemas / IpcRoutewindowRequestSchemas
Event contract/type*EventSchemas / IpcEventSchemas / IpcEventNameWindowEventSchemas

Any depth ≥ 2 segments is allowed. Add a subdomain once a namespace's routes fall into distinct groups — put the resource path first and the verb last (ai.agent.task.create, not ai.create_agent_task), and never fake a level with an underscore (ai.stream.open, not ai.stream_open). Group by domain, not by owning service: ai.tool.get_result and ai.tool.respond_approval share a subtree while delegating to different services. Existing subdomain trees: ai.*, mcp.{server,tool,package}, window.{main,sub}, system.{mac,shell}, app.{updater,data_reset,user_data_relocation}, channel.{feishu,wechat}, export.{obsidian,word}.

The dot structure is a naming convention, not type syntax — IpcRoute is the strong-typed union keyof IpcRequestSchemas; an undeclared route is a compile error. Reuse Preference's data-schema-key/valid-key ESLint rule for the snake-case keys.

ESLint enforcement: the data-schema-key/valid-key rule's files glob includes src/shared/ipc/schemas/**/*.ts, so every route/event key in this directory is lint-enforced and any new domain file is covered automatically. zod data-field names are exempt — keys inside a z.*(...) object literal (e.g. z.object({ 'content-type': ... })) are skipped, so only the route/event strings are constrained. This relies on zod being imported as z (the repo convention).

Types Derived From Schemas

TypeMeaning
defineRoute({ input, output })declare one route; identity at runtime, captures the zod schemas
InputFor<R> / OutputFor<R>parsed input / output type for a global route R
EventPayload<E>payload type for a global event E
IpcHandlersFor<S>exhaustive, closed handler map for a schema set S
IpcContext{ senderId: WindowId | null } — handler's controlled second argument

InputFor/OutputFor/EventPayload/IpcRoute/IpcEventName are bound to the global registry, so they resolve to never until at least one domain is migrated. The reusable inference (IpcHandlersFor<S>) is generic and verifiable against any schema set today.

zod Across Processes (critical)

zod schemas are runtime values.

  • Main (IpcRouter) imports ipcRequestSchemas as a value to parse.
  • Renderer must import type from the @shared/ipc/schemas/* modules and @shared/ipc/types only. A value import would pull the entire zod schema set into the renderer bundle. This is enforced by an ESLint rule (@typescript-eslint/no-restricted-imports with allowTypeImports, scoped to src/renderer/** in eslint.config.mjs) that flags any value import under @shared/ipc/schemas. IpcError is the one exception — it is a value import, but plain TS with no zod dependency, so it is bundle-safe.

Validation is always on: the router parses every request route. There is no skip-validation knob (add a field later only if profiling proves a hot route needs it).

Not Unit-Tested

Schemas are thin structural contracts, not behavior — do not add a per-domain schemas/__tests__. Asserting zod primitives (e.g. z.boolean() rejects a string, or z.infer yields boolean) only tests zod. Correctness is already locked by compile-time IpcHandlersFor exhaustiveness, z.infer driving handler/renderer types, and the one framework type test (src/shared/ipc/__tests__/schema.types.test.ts). Test the handler instead — see ipc-usage.md.