I am a totally new user to stack overflow and this is my first ever question.
I have been researching ways to make Electron applications more secure and came across the following discussion about contextBridge and preload.js. With contextIsolation = true, is it possible to use ipcRenderer?
I have copied the code of interest below. My questions are with regards to when the Main process sends a message back to the Renderer process.
The Main process sends a message back to the Renderer via win.webContents.send("fromMain", responseObj);
In the Renderer process there is no active listener (no ipcRenderer.on because that is in the preload.js). We only have window.api.receive() but this is not a listener itself?
So, how does window.api.receive() know when to run?
EDIT/RESOLVED: Please see my own answer posted below, thanks.
main.js
ipcMain.on("toMain", (event, args) => {
  fs.readFile("path/to/file", (error, data) => {
    // Do something with file contents
    // Send result back to renderer process
    win.webContents.send("fromMain", responseObj);
  });
});
preload.js
const {
    contextBridge,
    ipcRenderer
} = require("electron");
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "api", {
        send: (channel, data) => {
            // whitelist channels
            let validChannels = ["toMain"];
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, data);
            }
        },
        receive: (channel, func) => {
            let validChannels = ["fromMain"];
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender` 
                ipcRenderer.on(channel, (event, ...args) => func(...args));
            }
        }
    }
);
renderer.js
window.api.receive("fromMain", (data) => {
            console.log(`Received ${data} from main process`);
        });
window.api.send("toMain", "some data");