Promise-based IPC
ehmicky opened this issue · 2 comments
Current API (callbacks)
Currently, to use Node.js IPC, one must use callbacks.
// parent.js
const subprocess = execaNode('child.js');
subprocess.send(sentMessage, error => {
// ...
});
subprocess.once('message', oneMessage => {
// ...
});
subprocess.on('message', eachMessage => {
// ...
});
// child.js
import process from 'node:process';
process.send(sentMessage, error => {
// ...
});
process.once('message', oneMessage => {
// ...
});
process.on('message', eachMessage => {
// ...
});
New API (promises)
To make it work better with async
/await
, provide with better stack traces and avoid uncaught exceptions, we should expose that API using promises instead.
// parent.js
const subprocess = execaNode('child.js');
await subprocess.ipc.send(sentMessage);
const oneMessage = await subprocess.ipc.receive();
for await (const eachMessage of subprocess.ipc.iterable()) {
// ...
};
// child.js
import {ipc} from 'execa';
await ipc.send(sentMessage);
const oneMessage = await ipc.receive();
for await (const eachMessage of ipc.iterable()) {
// ...
};
Additional pros
I have just looked into Node.js/libuv source code for the IPC feature to check for edge cases. On top of just providing with a promise-based API instead of callbacks, this would also benefit the additional pros.
Graceful exit
When using process.on('message')
or subprocess.on('message')
, the process is kept alive. My guess is that most users probably solve this by calling either:
process.exit()
: this is bad because it interrupts any ongoing logic.process.disconnect()
: this is bad because it prevents callingprocess.on('message')
later. In other words, it is not possible to stop listening then restart later.
The proper way is to remove the message
event listener, which we would automatically do.
(Note: under the hood, Node.js listens for addListener
and removeListener
events on the process.)
Error handling
It's a little hard right now for users to both use the IPC callbacks API, while at the same time await
the Execa promise in case the subprocess failed. Using a promise-based API would ensure no subprocess failure is creating unhandled promise rejections.
Backpressure
subprocess.send()
returns false
when more than ~350KB has been buffered but not sent yet. This works like streams highWaterMark
, for backpressure. I.e. the best practice would be for users to stop sending data when false
is returned.
I am guessing most users might not do this. Our API would automatically provide with backpressure, since the buffer is always emptied when the subprocess.send()
callback is fired. So awaiting the .send()
promise would be enough to ensure the IPC channel is not under pressure.
Better validation
If the user forgot to use the ipc: true
option, we can provide with a nice error message. Right now, they might be wondering why process.send
crashes when it is undefined
.
Simplicity
With that API, users should never have to use process.disconnect()
, process.on('disconnect')
, process.connected
or process.channel
. This is automatically handled for them.
Backward compatibility
The callback-based API would still be available, but undocumented.
What do you think @sindresorhus?
That looks like a better API indeed 👍
I started working on this and I realized I kept forgetting to type ipc
. If I do, users might do as well. I am now using the following method names instead. If the new names don't work, please let me know, and I'll rename them.
// parent.js
const subprocess = execaNode('child.js');
await subprocess.sendMessage(sentMessage);
const oneMessage = await subprocess.getOneMessage();
for await (const eachMessage of subprocess.getEachMessage()) {
// ...
};
// child.js
import {sendMessage, getOneMessage, getEachMessage} from 'execa';
await sendMessage(sentMessage);
const oneMessage = await getOneMessage();
for await (const eachMessage of getEachMessage()) {
// ...
};