From 904ffbdd5b690eeb8d88246d1bba138b67b8377b Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 09:34:19 +0530 Subject: [PATCH] feat(renderer): implement typed IPC wrapper with IpcResult discriminated union --- src/renderer/lib/ipc.ts | 94 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/renderer/lib/ipc.ts diff --git a/src/renderer/lib/ipc.ts b/src/renderer/lib/ipc.ts new file mode 100644 index 0000000..7f0b4be --- /dev/null +++ b/src/renderer/lib/ipc.ts @@ -0,0 +1,94 @@ +import type { + IpcResult, + FileResult, + FileEntry, + PdfOptions, + DocxOptions, + HtmlOptions, + ExportResult, + BatchItem, + BatchOptions, + BatchResult, +} from '@/types/ipc'; + +type ChannelMissing = { code: 'CHANNEL_MISSING'; message: string }; + +function wrap(fn: () => Promise): Promise> { + if (typeof window === 'undefined' || !window.electronAPI) { + return Promise.resolve({ + ok: false, + error: { code: 'NO_BRIDGE', message: 'window.electronAPI is unavailable' }, + }); + } + return fn().then( + (data) => ({ ok: true as const, data }), + (err: Error) => ({ + ok: false as const, + error: { code: err.name || 'IPC_ERROR', message: err.message || String(err) }, + }), + ); +} + +function safeCall Promise>( + channel: string, + method: string, + ...args: Parameters +): Promise> | ChannelMissing>> { + if (typeof window === 'undefined' || !window.electronAPI) { + return Promise.resolve({ + ok: false, + error: { code: 'NO_BRIDGE', message: 'window.electronAPI is unavailable' }, + }); + } + const target = (window.electronAPI as any)[channel]?.[method]; + if (!target) { + return Promise.resolve({ + ok: false, + error: { code: 'CHANNEL_MISSING', message: `Missing channel: ${channel}.${method}` }, + }); + } + if (typeof target !== 'function') { + return Promise.resolve({ + ok: false, + error: { code: 'CHANNEL_MISSING', message: `Not a function: ${channel}.${method}` }, + }); + } + return wrap(() => target(...args)); +} + +export const ipc = { + file: { + open: (): Promise> => + safeCall('file', 'open'), + read: (path: string): Promise> => + safeCall('file', 'read', path), + write: (path: string, content: string): Promise> => + safeCall('file', 'write', path, content), + list: (dir: string): Promise> => + safeCall('file', 'list', dir), + onChange: (cb: (path: string) => void): (() => void) => { + if (typeof window === 'undefined' || !window.electronAPI?.file?.onChange) { + return () => {}; + } + return window.electronAPI.file.onChange(cb); + }, + }, + export: { + pdf: (opts: PdfOptions): Promise> => + safeCall('export', 'pdf', opts), + docx: (opts: DocxOptions): Promise> => + safeCall('export', 'docx', opts), + html: (opts: HtmlOptions): Promise> => + safeCall('export', 'html', opts), + batch: (items: BatchItem[], opts: BatchOptions): Promise> => + safeCall('export', 'batch', items, opts), + }, + app: { + getVersion: (): Promise> => + safeCall('app', 'getVersion'), + openExternal: (url: string): Promise> => + safeCall('app', 'openExternal', url), + showItemInFolder: (path: string): Promise> => + safeCall('app', 'showItemInFolder', path), + }, +}; \ No newline at end of file