/** * Custom electron IPC ↔ tRPC v11 bridge. * * Replaces the incompatible electron-trpc package (v0.7.x bundles tRPC v10's * callProcedure which checks `procedure._def[type]`, while tRPC v11 uses * `procedure._def.type`). */ import { ipcMain, type BrowserWindow } from 'electron'; import { callTRPCProcedure, getErrorShape, getTRPCErrorFromUnknown, transformTRPCResponse, type AnyRouter, } from '@trpc/server'; export const IPC_CHANNEL = 'trpc'; /** Context passed to every tRPC procedure via the IPC bridge. */ export type TRPCContext = { /** The IPC sender — available for streaming chunks back to the renderer. */ sender?: Electron.WebContents; }; interface IPCRequest { method: 'request'; operation: { id: number; type: 'query' | 'mutation' | 'subscription'; path: string; input?: unknown; }; } export function createIPCHandler({ router, windows = [], }: { router: TRouter; windows?: BrowserWindow[]; }) { const config = router._def._config; ipcMain.on(IPC_CHANNEL, async (event, message: IPCRequest) => { if (message.method !== 'request') return; const { id, type, path, input } = message.operation; // Deserialize input through transformer (identity by default) const rawInput = input !== undefined ? config.transformer.input.deserialize(input) : undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any const respond = (response: any) => { if (event.sender.isDestroyed()) return; const serialised = transformTRPCResponse(config, response); event.reply(IPC_CHANNEL, serialised); }; try { const result = await callTRPCProcedure({ router, path, getRawInput: async () => rawInput, ctx: { sender: event.sender } satisfies TRPCContext, type, signal: undefined as unknown as AbortSignal, batchIndex: 0, }); respond({ id, result: { type: 'data', data: result } }); } catch (cause) { const error = getTRPCErrorFromUnknown(cause); respond({ id, error: getErrorShape({ config, error, type, path, input: rawInput, ctx: {}, }), }); } }); // Attach additional windows later if needed return { attachWindow(_win: BrowserWindow) { // No per-window setup needed — ipcMain.on is global }, }; }