0

So I'm pretty new to Electron, and my goal is to have a completely offline application that effectively queries and displays results from a loaded SQLite file (hence the questionable SQL practices here).

I am able to query my database and get expected returns. However, when I make consecutive calls like this, the second query is getting the same result as the previous call. Here is the bit from my renderer (React). The first query will usually as expected, but then the second res is identical to that of the first. However, sometimes the second query gets the expected result and the first is identical to that. Either way, they both end up with the same res value.

Any ideas on what exactly is happening and how I can fix this? I don't believe a synchronous approach would be possible with ipcMain.

Code in renderer (React)

// called from ipcMain when database file is set
ipcRenderer.on('set-db', (event, arg) => {
    // Get authors
    queryDB("SELECT * FROM users;")
        .then((res) => {
            try {
                var options = [];
                res.forEach((e) => {
                    options.push({value: e.id, label: `@${e.name}`});
                    if (e.display_name != e.name) {
                        options.push({value: e.id, label: `@${e.display_name}`});
                    }
                });
                setAuthorOptions(options);
                console.log("Set author options");
            }
            catch (exception) {}
        });

    // Get channels
    queryDB("SELECT * FROM channels;")
        .then((res) => {
            try {
                var options = [];
                res.forEach((e) => {
                    if (allowedChannelTypes.includes(e.type)) {
                        options.push({value: e.id, label: `# ${e.name}`});
                    }
                });
                setChannelOptions(options);
                console.log("Set channel options");
            }
            catch (exception) {}
        });

});

Here is the code in the main process

ipcMain.on('asynchronous-message', (event, sql) => {
    if (db) {
        db.all(sql, (error, rows) => {
            event.reply('asynchronous-reply', (error && error.message) || rows);
        });
    }
    return
});

And the renderer code

export default function queryDB(sql) {
    return new Promise((res) => {
        ipcRenderer.once('asynchronous-reply', (_, arg) => {
            res(arg);
        });

        ipcRenderer.send('asynchronous-message', sql);
    })
}
midnight-coding
  • 2,857
  • 2
  • 17
  • 27
firefly
  • 3
  • 2
  • Why not using IPC `invoke`/`handle`? – Arkellys May 10 '23 at 06:11
  • The event occurs in the main process, and when that happens I would like for something to be updated in the renderer process. As far as I can tell from documentation, only the Renderer can `invoke` and the main process must `handle`. – firefly May 10 '23 at 23:40

1 Answers1

0

The problem is that you are using ipcRenderer.once several times with the same channel name. As described in the docs, this method:

Adds a one time listener function for the event. This listener is invoked only the next time a message is sent to channel, after which it is removed.

So if you make a new call before the previous one got a reply, they will both receive the results of the first one which is answered.

I'm not familiar with SQLite, but from what I gathered, depending on the library you use db.all() will either work with a callback or be a promise. I see two ways to fix this:

With a promise

If you use the promise way, you can simply use invoke/handle. For example:

Renderer

export default function queryDB(sql) {
    return ipcRenderer.invoke('query-db', sql);
}

Main

ipcMain.handle('query-db', async (event, sql) => {
  if(!db) return;

  const res = await db.all(sql);
  return res;
});

With a callback

If you prefer to use a callback, you have to make sure that you use unique channel names with ipcRenderer.once, for example:

Renderer

export default function queryDB(sql) {
    return new Promise((res) => {
        const channelName = 'asynchronous-reply-' + Date.now();

        ipcRenderer.once(channelName, (_, arg) => {
            res(arg);
        });

        ipcRenderer.send('asynchronous-message', sql, channelName);
    })
}

Main

ipcMain.on('asynchronous-message', (event, sql, channelName) => {
    if (db) {
        db.all(sql, (error, rows) => {
            event.reply(channelName, (error && error.message) || rows);
        });
    }
    return
});

If you use this method, and since you don't always send a reply to the created channel, you also need to make sure you cleanup the unused listeners using ipcRenderer.removeListener or ipcRenderer.removeAllListeners.

Arkellys
  • 5,562
  • 2
  • 16
  • 40
  • I had to use a different module for sqlite that allowed using 'await', but the first suggestion ultimately worked, thank you! – firefly May 12 '23 at 02:41