diff --git a/docs/api.md b/docs/api.md index 29986f87b8..382c470c2a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -103,8 +103,9 @@ This requires the [`ipc`](#optionsipc) option to be `true`. The [type](ipc.md#me [More info.](ipc.md#exchanging-messages) -### getOneMessage() +### getOneMessage(getOneMessageOptions?) +_getOneMessageOptions_: [`GetOneMessageOptions`](#getonemessageoptions)\ _Returns_: [`Promise`](ipc.md#message-type) Receive a single `message` from the parent process. @@ -113,6 +114,18 @@ This requires the [`ipc`](#optionsipc) option to be `true`. The [type](ipc.md#me [More info.](ipc.md#exchanging-messages) +#### getOneMessageOptions + +_Type_: `object` + +#### getOneMessageOptions.filter + +_Type_: [`(Message) => boolean`](ipc.md#message-type) + +Ignore any `message` that returns `false`. + +[More info.](ipc.md#filter-messages) + ### getEachMessage() _Returns_: [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols) @@ -259,8 +272,9 @@ This requires the [`ipc`](#optionsipc) option to be `true`. The [type](ipc.md#me [More info.](ipc.md#exchanging-messages) -### subprocess.getOneMessage() +### subprocess.getOneMessage(getOneMessageOptions?) +_getOneMessageOptions_: [`GetOneMessageOptions`](#getonemessageoptions)\ _Returns_: [`Promise`](ipc.md#message-type) Receive a single `message` from the subprocess. @@ -900,7 +914,7 @@ By default, this applies to both `stdout` and `stderr`, but [different values ca _Type:_ `boolean`\ _Default:_ `true` if either the [`node`](#optionsnode) option or the [`ipcInput`](#optionsipcinput) is set, `false` otherwise -Enables exchanging messages with the subprocess using [`subprocess.sendMessage(message)`](#subprocesssendmessagemessage), [`subprocess.getOneMessage()`](#subprocessgetonemessage) and [`subprocess.getEachMessage()`](#subprocessgeteachmessage). +Enables exchanging messages with the subprocess using [`subprocess.sendMessage(message)`](#subprocesssendmessagemessage), [`subprocess.getOneMessage()`](#subprocessgetonemessagegetonemessageoptions) and [`subprocess.getEachMessage()`](#subprocessgeteachmessage). The subprocess must be a Node.js file. diff --git a/docs/execution.md b/docs/execution.md index e2a38d8be4..dee6e8835a 100644 --- a/docs/execution.md +++ b/docs/execution.md @@ -126,7 +126,7 @@ Synchronous execution is generally discouraged as it holds the CPU and prevents - Signal termination: [`subprocess.kill()`](api.md#subprocesskillerror), [`subprocess.pid`](api.md#subprocesspid), [`cleanup`](api.md#optionscleanup) option, [`cancelSignal`](api.md#optionscancelsignal) option, [`forceKillAfterDelay`](api.md#optionsforcekillafterdelay) option. - Piping multiple subprocesses: [`subprocess.pipe()`](api.md#subprocesspipefile-arguments-options). - [`subprocess.iterable()`](lines.md#progressive-splitting). -- [IPC](ipc.md): [`sendMessage()`](api.md#sendmessagemessage), [`getOneMessage()`](api.md#getonemessage), [`getEachMessage()`](api.md#geteachmessage), [`result.ipcOutput`](output.md#any-output-type), [`ipc`](api.md#optionsipc) option, [`serialization`](api.md#optionsserialization) option, [`ipcInput`](input.md#any-input-type) option. +- [IPC](ipc.md): [`sendMessage()`](api.md#sendmessagemessage), [`getOneMessage()`](api.md#getonemessagegetonemessageoptions), [`getEachMessage()`](api.md#geteachmessage), [`result.ipcOutput`](output.md#any-output-type), [`ipc`](api.md#optionsipc) option, [`serialization`](api.md#optionsserialization) option, [`ipcInput`](input.md#any-input-type) option. - [`result.all`](api.md#resultall) is not interleaved. - [`detached`](api.md#optionsdetached) option. - The [`maxBuffer`](api.md#optionsmaxbuffer) option is always measured in bytes, not in characters, [lines](api.md#optionslines) nor [objects](transform.md#object-mode). Also, it ignores transforms and the [`encoding`](api.md#optionsencoding) option. diff --git a/docs/input.md b/docs/input.md index 2362d23902..ea50ab8dc0 100644 --- a/docs/input.md +++ b/docs/input.md @@ -92,7 +92,7 @@ await execa({stdin: 'inherit'})`npm run scaffold`; ## Any input type -If the subprocess [uses Node.js](node.md), [almost any type](ipc.md#message-type) can be passed to the subprocess using the [`ipcInput`](ipc.md#send-an-initial-message) option. The subprocess retrieves that input using [`getOneMessage()`](api.md#getonemessage). +If the subprocess [uses Node.js](node.md), [almost any type](ipc.md#message-type) can be passed to the subprocess using the [`ipcInput`](ipc.md#send-an-initial-message) option. The subprocess retrieves that input using [`getOneMessage()`](api.md#getonemessagegetonemessageoptions). ```js // main.js diff --git a/docs/ipc.md b/docs/ipc.md index 98495e92d6..1627f36a92 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -12,7 +12,7 @@ When the [`ipc`](api.md#optionsipc) option is `true`, the current process and su The `ipc` option defaults to `true` when using [`execaNode()`](node.md#run-nodejs-files) or the [`node`](node.md#run-nodejs-files) option. -The current process sends messages with [`subprocess.sendMessage(message)`](api.md#subprocesssendmessagemessage) and receives them with [`subprocess.getOneMessage()`](api.md#subprocessgetonemessage). The subprocess uses [`sendMessage(message)`](api.md#sendmessagemessage) and [`getOneMessage()`](api.md#getonemessage) instead. +The current process sends messages with [`subprocess.sendMessage(message)`](api.md#subprocesssendmessagemessage) and receives them with [`subprocess.getOneMessage()`](api.md#subprocessgetonemessagegetonemessageoptions). The subprocess uses [`sendMessage(message)`](api.md#sendmessagemessage) and [`getOneMessage()`](api.md#getonemessagegetonemessageoptions) instead. ```js // parent.js @@ -33,7 +33,7 @@ console.log(await getOneMessage()); // 'Hello from parent' ## Listening to messages -[`subprocess.getOneMessage()`](api.md#subprocessgetonemessage) and [`getOneMessage()`](api.md#getonemessage) read a single message. To listen to multiple messages in a row, [`subprocess.getEachMessage()`](api.md#subprocessgeteachmessage) and [`getEachMessage()`](api.md#geteachmessage) should be used instead. +[`subprocess.getOneMessage()`](api.md#subprocessgetonemessagegetonemessageoptions) and [`getOneMessage()`](api.md#getonemessagegetonemessageoptions) read a single message. To listen to multiple messages in a row, [`subprocess.getEachMessage()`](api.md#subprocessgeteachmessage) and [`getEachMessage()`](api.md#geteachmessage) should be used instead. [`subprocess.getEachMessage()`](api.md#subprocessgeteachmessage) waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess [fails](api.md#result). This means you do not need to `await` the subprocess' [promise](execution.md#result). @@ -67,9 +67,29 @@ for await (const message of getEachMessage()) { } ``` +## Filter messages + +```js +import {getOneMessage} from 'execa'; + +const startMessage = await getOneMessage({ + filter: message => message.type === 'start', +}); +``` + +```js +import {getEachMessage} from 'execa'; + +for await (const message of getEachMessage()) { + if (message.type === 'start') { + // ... + } +} +``` + ## Retrieve all messages -The [`result.ipcOutput`](api.md#resultipcoutput) array contains all the messages sent by the subprocess. In many situations, this is simpler than using [`subprocess.getOneMessage()`](api.md#subprocessgetonemessage) and [`subprocess.getEachMessage()`](api.md#subprocessgeteachmessage). +The [`result.ipcOutput`](api.md#resultipcoutput) array contains all the messages sent by the subprocess. In many situations, this is simpler than using [`subprocess.getOneMessage()`](api.md#subprocessgetonemessagegetonemessageoptions) and [`subprocess.getEachMessage()`](api.md#subprocessgeteachmessage). ```js // main.js @@ -118,6 +138,8 @@ By default, messages are serialized using [`structuredClone()`](https://develope To limit messages to JSON instead, the [`serialization`](api.md#optionsserialization) option can be set to `'json'`. ```js +import {execaNode} from 'execa'; + const subprocess = execaNode({serialization: 'json'})`child.js`; ``` diff --git a/lib/ipc/get-one.js b/lib/ipc/get-one.js index 0ba98c1e51..3d18bff77b 100644 --- a/lib/ipc/get-one.js +++ b/lib/ipc/get-one.js @@ -1,4 +1,4 @@ -import {once} from 'node:events'; +import {once, on} from 'node:events'; import { validateIpcOption, validateConnection, @@ -7,19 +7,24 @@ import { } from './validation.js'; // Like `[sub]process.once('message')` but promise-based -export const getOneMessage = ({anyProcess, isSubprocess, ipc}) => { +export const getOneMessage = ({anyProcess, isSubprocess, ipc}, {filter} = {}) => { const methodName = 'getOneMessage'; validateIpcOption(methodName, isSubprocess, ipc); validateConnection(methodName, isSubprocess, anyProcess.channel !== null); - return onceMessage(anyProcess, isSubprocess, methodName); + return onceMessage({ + anyProcess, + isSubprocess, + methodName, + filter, + }); }; -const onceMessage = async (anyProcess, isSubprocess, methodName) => { +const onceMessage = async ({anyProcess, isSubprocess, methodName, filter}) => { const controller = new AbortController(); try { - const [message] = await Promise.race([ - once(anyProcess, 'message', {signal: controller.signal}), + return await Promise.race([ + getMessage(anyProcess, filter, controller), throwOnDisconnect({ anyProcess, isSubprocess, @@ -27,7 +32,6 @@ const onceMessage = async (anyProcess, isSubprocess, methodName) => { controller, }), ]); - return message; } catch (error) { disconnect(anyProcess); throw error; @@ -36,6 +40,19 @@ const onceMessage = async (anyProcess, isSubprocess, methodName) => { } }; +const getMessage = async (anyProcess, filter, {signal}) => { + if (filter === undefined) { + const [message] = await once(anyProcess, 'message', {signal}); + return message; + } + + for await (const [message] of on(anyProcess, 'message', {signal})) { + if (filter(message)) { + return message; + } + } +}; + const throwOnDisconnect = async ({anyProcess, isSubprocess, methodName, controller: {signal}}) => { await once(anyProcess, 'disconnect', {signal}); throwOnEarlyDisconnect(methodName, isSubprocess); diff --git a/test-d/ipc/get-each.test-d.ts b/test-d/ipc/get-each.test-d.ts index 24802f1d65..f18975a4e9 100644 --- a/test-d/ipc/get-each.test-d.ts +++ b/test-d/ipc/get-each.test-d.ts @@ -6,12 +6,6 @@ import { type Options, } from '../../index.js'; -for await (const message of getEachMessage()) { - expectType(message); -} - -expectError(getEachMessage('')); - const subprocess = execa('test', {ipc: true}); for await (const message of subprocess.getEachMessage()) { @@ -22,7 +16,12 @@ for await (const message of execa('test', {ipc: true, serialization: 'json'}).ge expectType>(message); } +for await (const message of getEachMessage()) { + expectType(message); +} + expectError(subprocess.getEachMessage('')); +expectError(getEachMessage('')); execa('test', {ipcInput: ''}).getEachMessage(); execa('test', {ipcInput: '' as Message}).getEachMessage(); diff --git a/test-d/ipc/get-one.test-d.ts b/test-d/ipc/get-one.test-d.ts index fb0efc0573..afaf08c5d3 100644 --- a/test-d/ipc/get-one.test-d.ts +++ b/test-d/ipc/get-one.test-d.ts @@ -6,14 +6,14 @@ import { type Options, } from '../../index.js'; -expectType>(getOneMessage()); -expectError(await getOneMessage('')); - const subprocess = execa('test', {ipc: true}); -expectType>(await subprocess.getOneMessage()); -expectType>(await execa('test', {ipc: true, serialization: 'json'}).getOneMessage()); +expectType>>(subprocess.getOneMessage()); +const jsonSubprocess = execa('test', {ipc: true, serialization: 'json'}); +expectType>>(jsonSubprocess.getOneMessage()); +expectType>(getOneMessage()); expectError(await subprocess.getOneMessage('')); +expectError(await getOneMessage('')); await execa('test', {ipcInput: ''}).getOneMessage(); await execa('test', {ipcInput: '' as Message}).getOneMessage(); @@ -26,3 +26,30 @@ expectType(execa('test', {}).getOneMessage); expectType(execa('test', {ipc: false}).getOneMessage); expectType(execa('test', {ipcInput: undefined}).getOneMessage); expectType(execa('test', {ipc: false, ipcInput: ''}).getOneMessage); + +await subprocess.getOneMessage({filter: undefined} as const); +await subprocess.getOneMessage({filter: (message: Message<'advanced'>) => true} as const); +await jsonSubprocess.getOneMessage({filter: (message: Message<'json'>) => true} as const); +await jsonSubprocess.getOneMessage({filter: (message: Message<'advanced'>) => true} as const); +await subprocess.getOneMessage({filter: (message: Message<'advanced'> | bigint) => true} as const); +await subprocess.getOneMessage({filter: () => true} as const); +expectError(await subprocess.getOneMessage({filter: (message: Message<'advanced'>) => ''} as const)); +// eslint-disable-next-line @typescript-eslint/no-empty-function +expectError(await subprocess.getOneMessage({filter(message: Message<'advanced'>) {}} as const)); +expectError(await subprocess.getOneMessage({filter: (message: Message<'json'>) => true} as const)); +expectError(await subprocess.getOneMessage({filter: (message: '') => true} as const)); +expectError(await subprocess.getOneMessage({filter: true} as const)); +expectError(await subprocess.getOneMessage({unknownOption: true} as const)); + +await getOneMessage({filter: undefined} as const); +await getOneMessage({filter: (message: Message) => true} as const); +await getOneMessage({filter: (message: Message<'advanced'>) => true} as const); +await getOneMessage({filter: (message: Message | bigint) => true} as const); +await getOneMessage({filter: () => true} as const); +expectError(await getOneMessage({filter: (message: Message) => ''} as const)); +// eslint-disable-next-line @typescript-eslint/no-empty-function +expectError(await getOneMessage({filter(message: Message) {}} as const)); +expectError(await getOneMessage({filter: (message: Message<'json'>) => true} as const)); +expectError(await getOneMessage({filter: (message: '') => true} as const)); +expectError(await getOneMessage({filter: true} as const)); +expectError(await getOneMessage({unknownOption: true} as const)); diff --git a/test-d/ipc/send.test-d.ts b/test-d/ipc/send.test-d.ts index 6a4faeb7f1..001872b2d7 100644 --- a/test-d/ipc/send.test-d.ts +++ b/test-d/ipc/send.test-d.ts @@ -6,18 +6,19 @@ import { type Options, } from '../../index.js'; +const subprocess = execa('test', {ipc: true}); +expectType(await subprocess.sendMessage('')); expectType>(sendMessage('')); +expectError(await subprocess.sendMessage()); expectError(await sendMessage()); +expectError(await subprocess.sendMessage(undefined)); expectError(await sendMessage(undefined)); +expectError(await subprocess.sendMessage(0n)); expectError(await sendMessage(0n)); +expectError(await subprocess.sendMessage(Symbol('test'))); expectError(await sendMessage(Symbol('test'))); -const subprocess = execa('test', {ipc: true}); -expectType(await subprocess.sendMessage('')); - -expectError(await subprocess.sendMessage()); - await execa('test', {ipcInput: ''}).sendMessage(''); await execa('test', {ipcInput: '' as Message}).sendMessage(''); await execa('test', {} as Options).sendMessage?.(''); diff --git a/test/fixtures/ipc-echo-filter.js b/test/fixtures/ipc-echo-filter.js new file mode 100755 index 0000000000..cb6dc800f3 --- /dev/null +++ b/test/fixtures/ipc-echo-filter.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +import {sendMessage, getOneMessage} from '../../index.js'; +import {foobarArray} from '../helpers/input.js'; + +await sendMessage(await getOneMessage(({filter: message => message === foobarArray[1]}))); diff --git a/test/fixtures/ipc-echo-twice-filter.js b/test/fixtures/ipc-echo-twice-filter.js new file mode 100755 index 0000000000..f08adb67cf --- /dev/null +++ b/test/fixtures/ipc-echo-twice-filter.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import {sendMessage, getOneMessage} from '../../index.js'; +import {alwaysPass} from '../helpers/ipc.js'; + +const message = await getOneMessage({filter: alwaysPass}); +const secondMessagePromise = getOneMessage({filter: alwaysPass}); +await sendMessage(message); +await sendMessage(await secondMessagePromise); diff --git a/test/fixtures/ipc-process-error-filter.js b/test/fixtures/ipc-process-error-filter.js new file mode 100755 index 0000000000..743657ea7f --- /dev/null +++ b/test/fixtures/ipc-process-error-filter.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node +import process from 'node:process'; +import {getOneMessage} from '../../index.js'; +import {foobarString} from '../helpers/input.js'; +import {alwaysPass} from '../helpers/ipc.js'; + +const cause = new Error(foobarString); +await Promise.all([ + getOneMessage({filter: alwaysPass}), + process.emit('error', cause), +]); diff --git a/test/helpers/ipc.js b/test/helpers/ipc.js index 5acf2ac791..bb5efbc50c 100644 --- a/test/helpers/ipc.js +++ b/test/helpers/ipc.js @@ -7,3 +7,5 @@ export const iterateAllMessages = async subprocess => { return messages; }; + +export const alwaysPass = () => true; diff --git a/test/ipc/buffer-messages.js b/test/ipc/buffer-messages.js index 5e394f7d4c..4ef20c6053 100644 --- a/test/ipc/buffer-messages.js +++ b/test/ipc/buffer-messages.js @@ -56,3 +56,14 @@ test('Sets empty error.ipcOutput, sync', t => { const {ipcOutput} = t.throws(() => execaSync('fail.js')); t.deepEqual(ipcOutput, []); }); + +test('"error" event interrupts result.ipcOutput', async t => { + const subprocess = execa('ipc-echo-twice.js', {ipcInput: foobarString}); + t.is(await subprocess.getOneMessage(), foobarString); + + const cause = new Error(foobarString); + subprocess.emit('error', cause); + const error = await t.throwsAsync(subprocess); + t.is(error.cause, cause); + t.deepEqual(error.ipcOutput, [foobarString]); +}); diff --git a/test/ipc/get-one.js b/test/ipc/get-one.js index 1058b1abe8..a5f1f215a2 100644 --- a/test/ipc/get-one.js +++ b/test/ipc/get-one.js @@ -3,12 +3,13 @@ import {setTimeout} from 'node:timers/promises'; import test from 'ava'; import {execa} from '../../index.js'; import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; -import {foobarString} from '../helpers/input.js'; -import {iterateAllMessages} from '../helpers/ipc.js'; +import {foobarString, foobarArray} from '../helpers/input.js'; +import {iterateAllMessages, alwaysPass} from '../helpers/ipc.js'; setFixtureDirectory(); const getOneSubprocessMessage = subprocess => subprocess.getOneMessage(); +const getOneFilteredMessage = subprocess => subprocess.getOneMessage({filter: alwaysPass}); const testKeepAlive = async (t, buffer) => { const subprocess = execa('ipc-echo-twice.js', {ipc: true, buffer}); @@ -41,11 +42,12 @@ test('Buffers initial message to current process, buffer false', async t => { await subprocess; }); -test('Does not buffer initial message to current process, buffer true', async t => { +test.serial('Does not buffer initial message to current process, buffer true', async t => { const subprocess = execa('ipc-send-print.js', {ipc: true}); const [chunk] = await once(subprocess.stdout, 'data'); t.is(chunk.toString(), '.'); - t.is(await Promise.race([setTimeout(1e3), subprocess.getOneMessage()]), undefined); + await setTimeout(1e3); + t.is(await Promise.race([setTimeout(0), subprocess.getOneMessage()]), undefined); await subprocess.sendMessage('.'); const {ipcOutput} = await subprocess; t.deepEqual(ipcOutput, [foobarString]); @@ -53,6 +55,24 @@ test('Does not buffer initial message to current process, buffer true', async t const HIGH_CONCURRENCY_COUNT = 100; +test('subprocess.getOneMessage() can filter messages', async t => { + const subprocess = execa('ipc-send-twice.js', {ipc: true}); + const message = await subprocess.getOneMessage({filter: message => message === foobarArray[1]}); + t.is(message, foobarArray[1]); + + const {ipcOutput} = await subprocess; + t.deepEqual(ipcOutput, foobarArray); +}); + +test('exports.getOneMessage() can filter messages', async t => { + const subprocess = execa('ipc-echo-filter.js', {ipc: true}); + await subprocess.sendMessage(foobarArray[0]); + await subprocess.sendMessage(foobarArray[1]); + + const {ipcOutput} = await subprocess; + t.deepEqual(ipcOutput, [foobarArray[1]]); +}); + test.serial('Can retrieve initial IPC messages under heavy load, buffer false', async t => { await Promise.all( Array.from({length: HIGH_CONCURRENCY_COUNT}, async (_, index) => { @@ -72,26 +92,28 @@ test.serial('Can retrieve initial IPC messages under heavy load, buffer true', a ); }); -const testTwice = async (t, buffer) => { +const testTwice = async (t, buffer, filter) => { const subprocess = execa('ipc-send.js', {ipc: true, buffer}); t.deepEqual( - await Promise.all([subprocess.getOneMessage(), subprocess.getOneMessage()]), + await Promise.all([subprocess.getOneMessage({filter}), subprocess.getOneMessage({filter})]), [foobarString, foobarString], ); await subprocess; }; -test('subprocess.getOneMessage() can be called twice at the same time, buffer false', testTwice, false); -test('subprocess.getOneMessage() can be called twice at the same time, buffer true', testTwice, true); +test('subprocess.getOneMessage() can be called twice at the same time, buffer false', testTwice, false, undefined); +test('subprocess.getOneMessage() can be called twice at the same time, buffer true', testTwice, true, undefined); +test('subprocess.getOneMessage() can be called twice at the same time, buffer false, filter', testTwice, false, alwaysPass); +test('subprocess.getOneMessage() can be called twice at the same time, buffer true, filter', testTwice, true, alwaysPass); -const testCleanupListeners = async (t, buffer) => { +const testCleanupListeners = async (t, buffer, filter) => { const subprocess = execa('ipc-send.js', {ipc: true, buffer}); const bufferCount = buffer ? 1 : 0; t.is(subprocess.listenerCount('message'), bufferCount); t.is(subprocess.listenerCount('disconnect'), bufferCount); - const promise = subprocess.getOneMessage(); + const promise = subprocess.getOneMessage({filter}); t.is(subprocess.listenerCount('message'), bufferCount + 1); t.is(subprocess.listenerCount('disconnect'), bufferCount + 1); t.is(await promise, foobarString); @@ -105,22 +127,14 @@ const testCleanupListeners = async (t, buffer) => { t.is(subprocess.listenerCount('disconnect'), 0); }; -test('Cleans up subprocess.getOneMessage() listeners, buffer false', testCleanupListeners, false); -test('Cleans up subprocess.getOneMessage() listeners, buffer true', testCleanupListeners, true); - -test('"error" event interrupts result.ipcOutput', async t => { - const subprocess = execa('ipc-echo-twice.js', {ipcInput: foobarString}); - t.is(await subprocess.getOneMessage(), foobarString); +test('Cleans up subprocess.getOneMessage() listeners, buffer false', testCleanupListeners, false, undefined); +test('Cleans up subprocess.getOneMessage() listeners, buffer true', testCleanupListeners, true, undefined); +test('Cleans up subprocess.getOneMessage() listeners, buffer false, filter', testCleanupListeners, false, alwaysPass); +test('Cleans up subprocess.getOneMessage() listeners, buffer true, filter', testCleanupListeners, true, alwaysPass); - const cause = new Error(foobarString); - subprocess.emit('error', cause); - const error = await t.throwsAsync(subprocess); - t.is(error.cause, cause); - t.deepEqual(error.ipcOutput, [foobarString]); -}); - -const testParentDisconnect = async (t, buffer) => { - const subprocess = execa('ipc-echo-twice.js', {ipc: true, buffer}); +const testParentDisconnect = async (t, buffer, filter) => { + const fixtureName = filter ? 'ipc-echo-twice-filter.js' : 'ipc-echo-twice.js'; + const subprocess = execa(fixtureName, {ipc: true, buffer}); await subprocess.sendMessage(foobarString); t.is(await subprocess.getOneMessage(), foobarString); @@ -134,19 +148,23 @@ const testParentDisconnect = async (t, buffer) => { } }; -test('subprocess.disconnect() interrupts exports.getOneMessage(), buffer false', testParentDisconnect, false); -test('subprocess.disconnect() interrupts exports.getOneMessage(), buffer true', testParentDisconnect, true); +test('subprocess.disconnect() interrupts exports.getOneMessage(), buffer false', testParentDisconnect, false, false); +test('subprocess.disconnect() interrupts exports.getOneMessage(), buffer true', testParentDisconnect, true, false); +test('subprocess.disconnect() interrupts exports.getOneMessage(), buffer false, filter', testParentDisconnect, false, true); +test('subprocess.disconnect() interrupts exports.getOneMessage(), buffer true, filter', testParentDisconnect, true, true); -const testSubprocessDisconnect = async (t, buffer) => { +const testSubprocessDisconnect = async (t, buffer, filter) => { const subprocess = execa('empty.js', {ipc: true, buffer}); - await t.throwsAsync(subprocess.getOneMessage(), { + await t.throwsAsync(subprocess.getOneMessage({filter}), { message: /subprocess\.getOneMessage\(\) could not complete/, }); await subprocess; }; -test('Subprocess exit interrupts disconnect.getOneMessage(), buffer false', testSubprocessDisconnect, false); -test('Subprocess exit interrupts disconnect.getOneMessage(), buffer true', testSubprocessDisconnect, true); +test('Subprocess exit interrupts disconnect.getOneMessage(), buffer false', testSubprocessDisconnect, false, undefined); +test('Subprocess exit interrupts disconnect.getOneMessage(), buffer true', testSubprocessDisconnect, true, undefined); +test('Subprocess exit interrupts disconnect.getOneMessage(), buffer false, filter', testSubprocessDisconnect, false, alwaysPass); +test('Subprocess exit interrupts disconnect.getOneMessage(), buffer true, filter', testSubprocessDisconnect, true, alwaysPass); const testParentError = async (t, getMessages, useCause, buffer) => { const subprocess = execa('ipc-echo.js', {ipc: true, buffer}); @@ -170,6 +188,8 @@ const testParentError = async (t, getMessages, useCause, buffer) => { test('"error" event interrupts subprocess.getOneMessage(), buffer false', testParentError, getOneSubprocessMessage, false, false); test('"error" event interrupts subprocess.getOneMessage(), buffer true', testParentError, getOneSubprocessMessage, false, true); +test('"error" event interrupts subprocess.getOneMessage(), buffer false, filter', testParentError, getOneFilteredMessage, false, false); +test('"error" event interrupts subprocess.getOneMessage(), buffer true, filter', testParentError, getOneFilteredMessage, false, true); test('"error" event interrupts subprocess.getEachMessage(), buffer false', testParentError, iterateAllMessages, true, false); test('"error" event interrupts subprocess.getEachMessage(), buffer true', testParentError, iterateAllMessages, true, true); @@ -189,5 +209,7 @@ const testSubprocessError = async (t, fixtureName, buffer) => { test('"error" event interrupts exports.getOneMessage(), buffer false', testSubprocessError, 'ipc-process-error.js', false); test('"error" event interrupts exports.getOneMessage(), buffer true', testSubprocessError, 'ipc-process-error.js', true); +test('"error" event interrupts exports.getOneMessage(), buffer false, filter', testSubprocessError, 'ipc-process-error-filter.js', false); +test('"error" event interrupts exports.getOneMessage(), buffer true, filter', testSubprocessError, 'ipc-process-error-filter.js', true); test('"error" event interrupts exports.getEachMessage(), buffer false', testSubprocessError, 'ipc-iterate-error.js', false); test('"error" event interrupts exports.getEachMessage(), buffer true', testSubprocessError, 'ipc-iterate-error.js', true); diff --git a/types/ipc.d.ts b/types/ipc.d.ts index ad49c8a983..a7381bebda 100644 --- a/types/ipc.d.ts +++ b/types/ipc.d.ts @@ -26,6 +26,18 @@ export type Message< Serialization extends Options['serialization'] = Options['serialization'], > = Serialization extends 'json' ? JsonMessage : AdvancedMessage; +/** +Options to `getOneMessage()` and `subprocess.getOneMessage()` +*/ +type GetOneMessageOptions< + Serialization extends Options['serialization'], +> = { + /** + Ignore any `message` that returns `false`. + */ + readonly filter?: (message: Message) => boolean; +}; + // IPC methods in subprocess /** Send a `message` to the parent process. @@ -39,7 +51,7 @@ Receive a single `message` from the parent process. This requires the `ipc` option to be `true`. The type of `message` depends on the `serialization` option. */ -export function getOneMessage(): Promise; +export function getOneMessage(getOneMessageOptions?: GetOneMessageOptions): Promise; /** Iterate over each `message` from the parent process. @@ -66,7 +78,7 @@ export type IpcMethods< This requires the `ipc` option to be `true`. The type of `message` depends on the `serialization` option. */ - getOneMessage(): Promise>; + getOneMessage(getOneMessageOptions?: GetOneMessageOptions): Promise>; /** Iterate over each `message` from the subprocess.