import { IpcMain as EIpcMain, IpcRenderer as EIpcRenderer, WebContents } from 'electron'; import { capitalize } from './string-helpers'; import log from './logging'; type Handler = (callback: (arg: T) => R) => void; type Sender = (arg: T) => R; type Notifier = (webContents: WebContents | undefined, arg: T) => void; type Listener = (callback: (arg: T) => void) => void; interface MainToRenderer { direction: 'main-to-renderer'; send: (event: string, ipcMain: EIpcMain) => Notifier; receive: (event: string, ipcRenderer: EIpcRenderer) => Listener; } interface RendererToMain { direction: 'renderer-to-main'; send: (event: string, ipcRenderer: EIpcRenderer) => Sender; receive: (event: string, ipcMain: EIpcMain) => Handler; } // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyIpcCall = MainToRenderer | RendererToMain; type Schema = Record>; // Renames all IPC calls, e.g. `callName` to either `notifyCallName` or `handleCallName` depending // on direction. type IpcMainKey = I['direction'] extends 'main-to-renderer' ? `notify${Capitalize}` : `handle${Capitalize}`; // Selects either the send or receive function depending on direction. type IpcMainFn = I['direction'] extends 'main-to-renderer' ? ReturnType : ReturnType; // Renames all receiving IPC calls, e.g. `callName` to `listenCallName`. type IpcRendererKey< N extends string, I extends AnyIpcCall > = I['direction'] extends 'main-to-renderer' ? `listen${Capitalize}` : N; // Selects either the send or receive function depending on direction. type IpcRendererFn = I['direction'] extends 'main-to-renderer' ? ReturnType : ReturnType; // Transforms the provided schema to the correct type for the main event channel. type IpcMain = { [G in keyof S]: { [K in keyof S[G] as IpcMainKey]: IpcMainFn; }; }; // Transforms the provided schema to the correct type for the renderer event channel. type IpcRenderer = { [G in keyof S]: { [K in keyof S[G] as IpcRendererKey]: IpcRendererFn; }; }; // Preforms the transformation of the main event channel in accordance with the above types. export function createIpcMain(schema: S, ipcMain: EIpcMain): IpcMain { return createIpc(schema, (event, key, spec) => { const capitalizedKey = capitalize(key); const newKey = spec.direction === 'main-to-renderer' ? `notify${capitalizedKey}` : `handle${capitalizedKey}`; const newValue = spec.direction === 'main-to-renderer' ? spec.send(event, ipcMain) : spec.receive(event, ipcMain); return [newKey, newValue]; }); } // Preforms the transformation of the renderer event channel in accordance with the above types. export function createIpcRenderer( schema: S, ipcRenderer: EIpcRenderer, ): IpcRenderer { return createIpc(schema, (event, key, spec) => { const newKey = spec.direction === 'main-to-renderer' ? `listen${capitalize(key)}` : key; const newValue = spec.direction === 'main-to-renderer' ? spec.receive(event, ipcRenderer) : spec.send(event, ipcRenderer); return [newKey, newValue]; }); } function createIpc | IpcRenderer>( ipc: S, fn: (event: string, key: string, spec: AnyIpcCall) => [newKey: string, newValue: T], ): R { return Object.fromEntries( Object.entries(ipc).map(([groupKey, group]) => { const newGroup = Object.fromEntries( Object.entries(group).map(([key, spec]) => fn(`${groupKey}-${key}`, key, spec)), ); return [groupKey, newGroup]; }), ) as R; } // Sends a request from the renderer process to the main process without any possibility to respond. export function send(): RendererToMain { return { direction: 'renderer-to-main', send: (event, ipcRenderer) => (newValue: T) => ipcRenderer.send(event, newValue), receive: (event, ipcMain) => (handlerFn: (value: T) => void) => { ipcMain.on(event, (_event, newValue: T) => { handlerFn(newValue); }); }, }; } // Sends a synchronous request from the renderer process to the main process. export function invokeSync(): RendererToMain { return { direction: 'renderer-to-main', send: (event, ipcRenderer) => (newValue: T) => ipcRenderer.sendSync(event, newValue), receive: (event, ipcMain) => (handlerFn: (value: T) => R) => { ipcMain.on(event, (ipcEvent, newValue: T) => { ipcEvent.returnValue = handlerFn(newValue); }); }, }; } // Sends an asynchronous request from the renderer process to the main process. export function invoke(): RendererToMain> { return { direction: 'renderer-to-main', send: invokeImpl, receive: handle, }; } // Sends a request from the main process to the renderer process without any possibility to respond. export function notifyRenderer(): MainToRenderer { return { direction: 'main-to-renderer', send: notifyRendererImpl, receive: (event, ipcRenderer) => (fn: (value: T) => void) => { ipcRenderer.on(event, (_event, newState: T) => fn(newState)); }, }; } function notifyRendererImpl(event: string, _ipcMain: EIpcMain): Notifier { return (webContents, value) => { if (webContents === undefined) { log.error(`sender(${event}): webContents is already destroyed!`); } else { webContents.send(event, value); } }; } type RequestResult = { type: 'success'; value: T } | { type: 'error'; message: string }; function invokeImpl(event: string, ipcRenderer: EIpcRenderer): Sender> { return async (arg: T): Promise => { const result: RequestResult = await ipcRenderer.invoke(event, arg); switch (result.type) { case 'error': throw new Error(result.message); case 'success': return result.value; } }; } function handle(event: string, ipcMain: EIpcMain): Handler> { return (fn: (arg: T) => Promise) => { ipcMain.handle(event, async (_ipcEvent, arg: T) => { try { return { type: 'success', value: await fn(arg) }; } catch (e) { const error = e as Error; return { type: 'error', message: error.message || '' }; } }); }; }