Files
adiuvAI/src/renderer/lib/ipcLink.ts
2026-03-10 09:10:57 +01:00

101 lines
2.9 KiB
TypeScript

/**
* Renderer-side tRPC IPC link for Electron.
*
* Replaces electron-trpc's ipcLink with a custom implementation that
* works with our custom IPC handler + tRPC v11.
*/
import { observable } from '@trpc/server/observable';
import type { TRPCLink } from '@trpc/client';
import type { AnyRouter } from '@trpc/server';
interface ElectronTRPC {
sendMessage: (msg: unknown) => void;
onMessage: (cb: (data: unknown) => void) => (() => void) | void;
}
type V3StreamEvent =
| { type: 'stream_start'; requestId: string }
| { type: 'stream_text'; requestId: string; chunk: string }
| { type: 'stream_block'; requestId: string; blockType: 'chart' | 'entity_ref' | 'table' | 'timeline'; data: Record<string, unknown> }
| { type: 'stream_end'; requestId: string; mutations?: Record<string, unknown> }
| { type: 'floating_domain'; requestId: string; domain: 'tasks' | 'notes' | 'checkpoints' | 'projects' };
interface ElectronAI {
onStreamEvent: (cb: (data: V3StreamEvent) => void) => () => void;
onBriefUpdated: (cb: (content: string) => void) => () => void;
}
interface ElectronDialog {
showOpenDialog: (options: {
properties?: string[];
title?: string;
defaultPath?: string;
filters?: { name: string; extensions: string[] }[];
multiSelections?: boolean;
}) => Promise<{ canceled: boolean; filePaths: string[] }>;
}
declare global {
interface Window {
electronTRPC: ElectronTRPC;
electronAI: ElectronAI;
electronDialog: ElectronDialog;
}
}
type TRPCResponse = {
id: number | null;
result?: { type: string; data?: unknown };
error?: unknown;
};
let nextId = 0;
export function ipcLink<TRouter extends AnyRouter>(): TRPCLink<TRouter> {
return () =>
({ op }) =>
observable((observer) => {
const id = ++nextId;
const { electronTRPC } = window;
if (!electronTRPC) {
observer.error(
new Error(
'Could not find `electronTRPC` global. ' +
'Check that the preload script has been loaded.',
) as any, // eslint-disable-line @typescript-eslint/no-explicit-any
);
return;
}
const unsubscribe = electronTRPC.onMessage((response: unknown) => {
const msg = response as TRPCResponse;
if (msg.id !== id) return;
if ('error' in msg) {
observer.error(msg.error as any); // eslint-disable-line @typescript-eslint/no-explicit-any
return;
}
observer.next({
result: msg.result as { type: 'data'; data: unknown },
});
observer.complete();
});
electronTRPC.sendMessage({
method: 'request',
operation: {
id,
type: op.type,
path: op.path,
input: op.input,
},
});
return () => {
if (typeof unsubscribe === 'function') unsubscribe();
};
});
}