IPC
Novadesk widgets have two script layers. The Main script (index.js) handles widget logic and data, while UI scripts (*.ui.js) handle rendering. The IPC API lets these two layers exchange messages over named channels, modelled closely after Electron's ipcMain / ipcRenderer pattern.
Availability
| Global | Available in |
|---|---|
ipcMain | Main script only |
ipcRenderer | UI script only |
Both are injected as globals. No import is needed.
In-process, not inter-process
Despite the name, Novadesk IPC runs entirely within a single QuickJS context. Messages are direct function calls with no serialization, no async overhead, and no cross-process boundary. Any JavaScript value type can be passed as a payload.
Table of Contents
IPC Message Object
Every on listener and handle handler receives an event object as its first argument:
| Property | Type | Description |
|---|---|---|
type | string | The channel name the message was sent on. |
payload | any | The data that was sent. undefined if no payload was provided. |
from | string | Origin side: "main" or "ui". |
to | string | Destination side: "main" or "ui". |
channel | string | Same as type for channel-based listeners. |
ipcMain
Available in the Main script only. Used to receive messages from UI scripts and push messages back to them.
Registers a persistent listener for messages sent from a UI script via ipcRenderer.send(). If multiple listeners are registered on the same channel, all of them are called in registration order.
No removeListener
There is no ipcMain.off() or removeListener(). Listeners are cleaned up automatically when the script that registered them is unloaded or refreshed.
PARAMETERS
ipcMain.on("ui-ready", (event, payload) => {
console.log("UI is ready:", JSON.stringify(payload));
// event.from === "ui", event.to === "main"
});Registers a synchronous request handler for ipcRenderer.invoke() calls. Only one handler per channel is active at a time. Calling handle again on the same channel silently replaces the previous handler.
When to use handle vs on
Use handle + invoke when the UI needs a value back from the Main script immediately. Use on + send for fire-and-forget notifications where no return value is needed.
PARAMETERS
ipcMain.handle("get-config", (event, payload) => {
return { theme: "dark", version: 2 };
});Sends a message from the Main script to all UI listeners registered on the given channel via ipcRenderer.on(). If no listeners are registered, the call is a silent no-op.
PARAMETERS
ipcMain.send("main-ready", { ts: Date.now() });
ipcMain.send("refresh"); // payload is optionalipcRenderer
Available in UI scripts only. Used to send messages to the Main script and receive responses.
Scoped to the UI script
ipcRenderer is passed as a local parameter to your UI script and is not accessible from the global scope. This is intentional: it keeps the UI script sandboxed. Timers (setTimeout, setInterval) are also unavailable in UI scripts.
Registers a persistent listener for messages sent from the Main script via ipcMain.send(). Multiple listeners on the same channel are all called in registration order.
When a UI script is reloaded or refreshed, all its previously registered listeners are automatically removed before the script runs again, so there is no risk of duplicate listeners accumulating across reloads.
PARAMETERS
ipcRenderer.on("main-ready", (event, payload) => {
console.log("Main says:", JSON.stringify(payload));
ui.setElementProperties("status", { text: "Connected" });
});
ipcRenderer.on("main-pong", (event, payload) => {
console.log("pong received:", payload.ts);
});Sends a message from the UI script to all Main listeners registered on the given channel via ipcMain.on(). If no listeners are registered, the call is a silent no-op.
PARAMETERS
ipcRenderer.send("ui-ready", { ts: Date.now() });
ipcRenderer.send("ui-ping", { msg: "hello" });Calls a Main script handler and returns its return value synchronously. The call runs inline on the current call stack with no async overhead. A matching ipcMain.handle() must be registered before invoke is called, otherwise a ReferenceError is thrown.
Throws if no handler is registered
If ipcMain.handle(channel, ...) has not been called for the given channel before invoke is called, a ReferenceError is thrown: No ipcMain handler for channel: <channel>. Register handlers in your Main script before the UI script starts executing.
PARAMETERS
any The value returned by the matching ipcMain.handle() handler.ipcMain.handle("get-config", (event, payload) => {
return { theme: "dark", version: 1 };
});Limitations
These features do not exist in the current implementation:
| Missing feature | Note |
|---|---|
ipcMain.off() / removeListener() | No public API to unregister a specific listener. Listeners are cleaned up on script unload/refresh. |
ipcMain.once() / ipcRenderer.once() | No single-fire listener variant. Implement it manually with a flag if needed. |
ipcMain.emit() | Channels are always directional (main to ui, or ui to main). There is no way to broadcast within the same layer. |
Async invoke | ipcRenderer.invoke() is synchronous. If the handler returns a Promise, the caller receives the Promise object, not the resolved value. |
Full Example
A complete widget using all four IPC methods together.
import { widgetWindow } from 'novadesk';
// Receive fire-and-forget notifications from the UI
ipcMain.on("ui-ready", (event, payload) => {
console.log("[main] UI ready:", JSON.stringify(payload));
});
ipcMain.on("ui-ping", (event, payload) => {
console.log("[main] ping received, sending pong");
ipcMain.send("main-pong", { ts: Date.now(), echo: payload });
});
// Handle synchronous data requests from the UI
ipcMain.handle("get-config", (event, payload) => {
return { theme: "dark", version: 1 };
});
const win = new widgetWindow({
id: "demo",
width: 400,
height: 400,
script: "ui/script.ui.js",
backgroundColor: "rgb(10,10,10)"
});
// Push initial state to the UI
ipcMain.send("main-ready", { ts: Date.now() });