The ipcMain module receives messages sent from renderer processes. It registers fire-and-forget channel listeners (on/once) and request/response handlers (handle) that respond to ipcRenderer.invoke. In Bunmaska the transport is WebKit-backed (a WKScriptMessageHandler inbound, evaluateJavaScript outbound) rather than Chromium’s IPC, but the router itself is transport-agnostic and the API mirrors Electron’s.
Unlike Electron, Bunmaska’s ipcMain is not a Node.js EventEmitter - it is a small purpose-built router. That distinction matters in a few places, called out below. It is a singleton, imported from the main entry point:
import { ipcMain } from 'bunmaska';To push messages the other way (main to renderer), use webContents.send; ipcMain only receives. Messages come from the preload’s ipcRenderer (see ipcRenderer), and only from a window’s top frame.
Methods
ipcMain.on(channel, listener)
channelstringlistenerFunctioneventIpcMainEvent...argsunknown[]
Listens on channel. When a renderer calls ipcRenderer.send(channel, ...args), listener is called with listener(event, ...args). Returns this, so calls chain. The event object carries sender (the originating WebContents) and reply(channel, ...args); see Properties. A listener that throws is caught and logged, and the other listeners on the channel still run.
import { ipcMain } from 'bunmaska';
ipcMain.on('counter:increment', (event, by: number) => {
console.log('increment by', by, 'from', event.sender);
});// preload.js
import { ipcRenderer } from 'bunmaska/renderer';
ipcRenderer.send('counter:increment', 1);ipcMain.once(channel, listener)
channelstringlistenerFunctioneventIpcMainEvent...argsunknown[]
Adds a one-time listener. It fires the next time a message arrives on channel, then removes itself. Returns this.
import { ipcMain } from 'bunmaska';
ipcMain.once('app:ready-handshake', (event) => {
console.log('renderer handshook once', event.sender);
});ipcMain.removeListener(channel, listener)
channelstringlistenerFunction
Removes a specific listener previously added with on or once for channel. Returns this. Pass the same function reference you registered.
import { ipcMain } from 'bunmaska';
const onPing = (event: unknown) => console.log('ping', event);
ipcMain.on('net:ping', onPing);
ipcMain.removeListener('net:ping', onPing);ipcMain.removeAllListeners([channel])
channelstring (optional)
Removes every listener registered on channel. With no argument, removes all listeners on all channels. Returns this. Note this only clears on/once listeners - it does not touch handle handlers (use removeHandler for those).
import { ipcMain } from 'bunmaska';
ipcMain.removeAllListeners('net:ping'); // one channel
ipcMain.removeAllListeners(); // everythingipcMain.handle(channel, listener)
channelstringlistenerFunction<Promise<unknown> | unknown>eventIpcMainInvokeEvent...argsunknown[]
Registers a handler for an invokable IPC. It is called whenever a renderer runs ipcRenderer.invoke(channel, ...args). If listener returns a Promise, its resolved value is sent back as the reply; otherwise the plain return value is used. There is exactly one handler per channel - calling handle again on the same channel replaces the previous handler.
If the handler throws (or rejects), the error is caught and only its message string is serialized back to the renderer, where the invoke Promise rejects. The original error object, stack, and custom properties do not cross the boundary. A result JSON cannot carry (a function, symbol or bigint) also rejects the invoke, instead of leaving it waiting forever.
import { ipcMain } from 'bunmaska';
ipcMain.handle('fs:read-config', async (event, name: string) => {
const file = Bun.file(`./config/${name}.json`);
return await file.json();
});// preload.js (bundled to a classic script, so no top-level await)
import { ipcRenderer } from 'bunmaska/renderer';
ipcRenderer.invoke('fs:read-config', 'app').then((config) => console.log(config));ipcMain.handleOnce(channel, listener)
channelstringlistenerFunction<Promise<unknown> | unknown>eventIpcMainInvokeEvent...argsunknown[]
Like handle, but the handler is removed after it responds to the first invoke. Subsequent invokes on the channel get the “no handler registered” rejection until you register again.
import { ipcMain } from 'bunmaska';
ipcMain.handleOnce('license:activate', async (event, key: string) => {
return activate(key); // only honored once
});ipcMain.removeHandler(channel)
channelstring
Removes the handler registered for channel, if any. After this, an invoke on the channel rejects with No handler registered for '<channel>'.
import { ipcMain } from 'bunmaska';
ipcMain.removeHandler('fs:read-config');Events
ipcMain is a plain router in Bunmaska, not an EventEmitter, so it has no module-level lifecycle events of its own. All “events” are the user-defined channels you subscribe to via on/once.
Properties
ipcMain exposes no public properties - it is the bare router singleton.
The event argument passed to your listeners and handlers carries:
event.sender- theWebContentsthat sent the message, on bothIpcMainEventandIpcMainInvokeEvent.BrowserWindow.fromWebContents(event.sender)finds its window.event.reply(channel, ...args)- onIpcMainEventonly (theon/onceevent), as in Electron: sends back to the renderer the message came from, the same asevent.sender.send(...).
The richer Electron event shape is not present (see below).
Not in Bunmaska (yet)
The router covers the everyday on/once/handle flow, but several Electron members are absent. Document-worthy gaps:
ipcMain.off,ipcMain.addListener- these Electron aliases forremoveListener/ondo not exist. UseremoveListenerandondirectly.- Synchronous IPC (
event.returnValue) - there is no synchronousipcRenderer.sendSyncpath, so listeners cannot setevent.returnValueto reply inline. Usehandle/invokefor request/response instead. event.frameId/event.processId/event.senderFrame- frame and process routing metadata is not exposed;event.senderis all you get, and iframe-level addressing is not modeled.event.portsandMessagePorttransfer -MessagePortMain/postMessagechannels are not implemented; payloads cross as JSON, not structured clone. AMap,Set,ArrayBuffer, typed array, function, symbol orbigintis refused on both sides rather than arriving mangled: the preload throws aTypeError, and main throws anInvalidArgumentError(a handler that returns one makes the page’sinvokereject). A NodeBuffersent from main arrives as{ type: 'Buffer', data: [...] }.EventEmittersurface - becauseipcMainis not anEventEmitter, methods likeeventNames(),listenerCount(),setMaxListeners(), andprependListener()are unavailable.- Full-fidelity error propagation -
handleerrors are flattened to themessagestring only; stack traces and custom error properties are lost across the boundary (the same limitation Electron documents, noted here for parity).
Everything in the Methods section above is genuinely wired and exercised without FFI. The router is platform-neutral and the renderer-to-main transport (the WebKit script-message channel) works on all three platforms - macOS, Linux, and Windows.
One security note for Windows: there is no isolated world there yet, so the bridge lives in the page world and any page script can invoke any handler. If a window may load content you do not control, check event.sender.getURL() in the handlers that matter.