Skip to content

Latest commit

 

History

History
226 lines (163 loc) · 8.1 KB

ipc.md

File metadata and controls

226 lines (163 loc) · 8.1 KB
execa logo

📞 Inter-process communication

Exchanging messages

When the ipc option is true, the current process and subprocess can exchange messages. This only works if the subprocess is a Node.js file.

The ipc option defaults to true when using execaNode() or the node option.

The current process sends messages with subprocess.sendMessage(message) and receives them with subprocess.getOneMessage().

The subprocess uses sendMessage(message) and getOneMessage(). Those are the same methods, but imported directly from the 'execa' module.

// parent.js
import {execaNode} from 'execa';

const subprocess = execaNode`child.js`;
await subprocess.sendMessage('Hello from parent');
const message = await subprocess.getOneMessage();
console.log(message); // 'Hello from child'
await subprocess;
// child.js
import {getOneMessage, sendMessage} from 'execa';

const message = await getOneMessage(); // 'Hello from parent'
const newMessage = message.replace('parent', 'child'); // 'Hello from child'
await sendMessage(newMessage);

Listening to messages

The methods described above read a single message. On the other hand, subprocess.getEachMessage() and getEachMessage() return an async iterable. This should be preferred when listening to multiple messages.

subprocess.getEachMessage() waits for the subprocess to end (even when using break or return). It throws if the subprocess fails. This means you do not need to await the subprocess' promise.

// parent.js
import {execaNode} from 'execa';

const subprocess = execaNode`child.js`;
await subprocess.sendMessage(0);

// This loop ends when the subprocess exits.
// It throws if the subprocess fails.
for await (const message of subprocess.getEachMessage()) {
	console.log(message); // 1, 3, 5, 7, 9
	await subprocess.sendMessage(message + 1);
}
// child.js
import {sendMessage, getEachMessage} from 'execa';

// The subprocess exits when hitting `break`
for await (const message of getEachMessage()) {
	if (message === 10) {
		break;
	}

	console.log(message); // 0, 2, 4, 6, 8
	await sendMessage(message + 1);
}

Filter messages

import {getOneMessage} from 'execa';

const startMessage = await getOneMessage({
	filter: message => message.type === 'start',
});
import {getEachMessage} from 'execa';

for await (const message of getEachMessage()) {
	if (message.type === 'start') {
		// ...
	}
}

Ensure messages are received

When a message is sent by one process, the other process must receive it using getOneMessage(), getEachMessage(), or automatically with result.ipcOutput. If not, that message is silently discarded.

If the strict: true option is passed to subprocess.sendMessage(message) or sendMessage(message), an error is thrown instead. This helps identifying subtle race conditions like the following example.

// main.js
import {execaNode} from 'execa';

const subprocess = execaNode`build.js`;
// This `build` message is received
await subprocess.sendMessage('build', {strict: true});
// This `lint` message is not received, so it throws
await subprocess.sendMessage('lint', {strict: true});
await subprocess;
// build.js
import {getOneMessage} from 'execa';

// Receives the 'build' message
const task = await getOneMessage();
// The `lint` message is sent while `runTask()` is ongoing
// Therefore the `lint` message is discarded
await runTask(task);

// Does not receive the `lint` message
// Without `strict`, this would wait forever
const secondTask = await getOneMessage();
await runTask(secondTask);

Retrieve all messages

The result.ipcOutput array contains all the messages sent by the subprocess. In many situations, this is simpler than using subprocess.getOneMessage() and subprocess.getEachMessage().

// main.js
import {execaNode} from 'execa';

const {ipcOutput} = await execaNode`build.js`;
console.log(ipcOutput[0]); // {kind: 'start', timestamp: date}
console.log(ipcOutput[1]); // {kind: 'stop', timestamp: date}
// build.js
import {sendMessage} from 'execa';

await sendMessage({kind: 'start', timestamp: new Date()});
await runBuild();
await sendMessage({kind: 'stop', timestamp: new Date()});

Send an initial message

The ipcInput option sends a message to the Node.js subprocess when it starts.

// main.js
import {execaNode} from 'execa';

const ipcInput = [
	{task: 'lint', ignore: /test\.js/},
	{task: 'copy', files: new Set(['main.js', 'index.js']),
}];
await execaNode({ipcInput})`build.js`;
// build.js
import {getOneMessage} from 'execa';

const ipcInput = await getOneMessage();

Message type

By default, messages are serialized using structuredClone(). This supports most types including objects, arrays, Error, Date, RegExp, Map, Set, bigint, Uint8Array, and circular references. This throws when passing functions, symbols or promises (including inside an object or array).

To limit messages to JSON instead, the serialization option can be set to 'json'.

import {execaNode} from 'execa';

await execaNode({serialization: 'json'})`child.js`;

Messages order

The messages are always received in the same order they were sent. Even when sent all at once.

import {sendMessage} from 'execa';

await Promise.all([
	sendMessage('first'),
	sendMessage('second'),
	sendMessage('third'),
]);

Keeping the subprocess alive

By default, the subprocess is kept alive as long as getOneMessage() or getEachMessage() is waiting. This is recommended if you're sure the current process will send a message, as this prevents the subprocess from exiting too early.

However, if you don't know whether a message will be sent, this can leave the subprocess hanging forever. In that case, the reference: false option can be set.

import {getEachMessage} from 'execa';

// {type: 'gracefulExit'} is sometimes received, but not always
for await (const message of getEachMessage({reference: false})) {
	if (message.type === 'gracefulExit') {
		gracefulExit();
	}
}

Debugging

When the verbose option is 'full', the IPC messages sent by the subprocess to the current process are printed on the console.

Also, when the subprocess failed, error.ipcOutput contains all the messages sent by the subprocess. Those are also shown at the end of the error message.


Next: 🐛 Debugging
Previous: ⏳️ Streams
Top: Table of contents