Skip to content

Commit c6e791a

Browse files
Support AbortController (#490)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 93ab929 commit c6e791a

File tree

5 files changed

+94
-7
lines changed

5 files changed

+94
-7
lines changed

index.d.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,34 @@ export interface CommonOptions<EncodingType> {
208208
*/
209209
readonly killSignal?: string | number;
210210

211+
/**
212+
You can abort the spawned process using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
213+
214+
When `AbortController.abort()` is called, [`.isCanceled`](https://github.com/sindresorhus/execa#iscanceled) becomes `false`.
215+
216+
*Requires Node.js 16 or later.*
217+
218+
@example
219+
```js
220+
import {execa} from 'execa';
221+
222+
const abortController = new AbortController();
223+
const subprocess = execa('node', [], {signal: abortController.signal});
224+
225+
setTimeout(() => {
226+
abortController.abort();
227+
}, 1000);
228+
229+
try {
230+
await subprocess;
231+
} catch (error) {
232+
console.log(subprocess.killed); // true
233+
console.log(error.isCanceled); // true
234+
}
235+
```
236+
*/
237+
readonly signal?: AbortSignal;
238+
211239
/**
212240
If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`.
213241
@@ -341,6 +369,8 @@ export interface ExecaReturnValue<StdoutErrorType = string>
341369

342370
/**
343371
Whether the process was canceled.
372+
373+
You can cancel the spawned process using the [`signal`](https://github.com/sindresorhus/execa#signal-1) option.
344374
*/
345375
isCanceled: boolean;
346376
}

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export function execa(file, args, options) {
127127
escapedCommand,
128128
parsed,
129129
timedOut,
130-
isCanceled: context.isCanceled,
130+
isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false),
131131
killed: spawned.killed,
132132
});
133133

index.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ execa('unicorns', {timeout: 1000});
142142
execa('unicorns', {maxBuffer: 1000});
143143
execa('unicorns', {killSignal: 'SIGTERM'});
144144
execa('unicorns', {killSignal: 9});
145+
execa('unicorns', {signal: new AbortController().signal});
145146
execa('unicorns', {windowsVerbatimArguments: true});
146147
execa('unicorns', {windowsHide: false});
147148
/* eslint-enable @typescript-eslint/no-floating-promises */

readme.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ try {
8383
```js
8484
import {execa} from 'execa';
8585

86-
const subprocess = execa('node');
86+
const abortController = new AbortController();
87+
const subprocess = execa('node', [], {signal: abortController.signal});
8788

8889
setTimeout(() => {
89-
subprocess.cancel();
90+
abortController.abort();
9091
}, 1000);
9192

9293
try {
@@ -171,10 +172,6 @@ Milliseconds to wait for the child process to terminate before sending `SIGKILL`
171172

172173
Can be disabled with `false`.
173174

174-
#### cancel()
175-
176-
Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This is preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`.
177-
178175
#### all
179176

180177
Type: `ReadableStream | undefined`
@@ -290,6 +287,8 @@ Type: `boolean`
290287

291288
Whether the process was canceled.
292289

290+
You can cancel the spawned process using the [`signal`](#signal-1) option.
291+
293292
#### killed
294293

295294
Type: `boolean`
@@ -546,6 +545,16 @@ Default: `SIGTERM`
546545

547546
Signal value to be used when the spawned process will be killed.
548547

548+
#### signal
549+
550+
Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
551+
552+
You can abort the spawned process using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
553+
554+
When `AbortController.abort()` is called, [`.isCanceled`](#iscanceled) becomes `false`.
555+
556+
*Requires Node.js 16 or later.*
557+
549558
#### windowsVerbatimArguments
550559

551560
Type: `boolean`\

test/kill.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,50 @@ test('calling cancel method on a process which has been killed does not make err
262262
const {isCanceled} = await t.throwsAsync(subprocess);
263263
t.false(isCanceled);
264264
});
265+
266+
if (globalThis.AbortController !== undefined) {
267+
test('calling abort throws an error with message "Command was canceled"', async t => {
268+
const abortController = new AbortController();
269+
const subprocess = execa('noop.js', [], {signal: abortController.signal});
270+
abortController.abort();
271+
await t.throwsAsync(subprocess, {message: /Command was canceled/});
272+
});
273+
274+
test('calling abort twice should show the same behaviour as calling it once', async t => {
275+
const abortController = new AbortController();
276+
const subprocess = execa('noop.js', [], {signal: abortController.signal});
277+
abortController.abort();
278+
abortController.abort();
279+
const {isCanceled} = await t.throwsAsync(subprocess);
280+
t.true(isCanceled);
281+
t.true(subprocess.killed);
282+
});
283+
284+
test('calling abort on a successfully completed process does not make result.isCanceled true', async t => {
285+
const abortController = new AbortController();
286+
const subprocess = execa('noop.js', [], {signal: abortController.signal});
287+
const {isCanceled} = await subprocess;
288+
abortController.abort();
289+
t.false(isCanceled);
290+
});
291+
292+
test('calling cancel after abort should show the same behaviour as only calling cancel', async t => {
293+
const abortController = new AbortController();
294+
const subprocess = execa('noop.js', [], {signal: abortController.signal});
295+
abortController.abort();
296+
subprocess.cancel();
297+
const {isCanceled} = await t.throwsAsync(subprocess);
298+
t.true(isCanceled);
299+
t.true(subprocess.killed);
300+
});
301+
302+
test('calling abort after cancel should show the same behaviour as only calling cancel', async t => {
303+
const abortController = new AbortController();
304+
const subprocess = execa('noop.js', [], {signal: abortController.signal});
305+
subprocess.cancel();
306+
abortController.abort();
307+
const {isCanceled} = await t.throwsAsync(subprocess);
308+
t.true(isCanceled);
309+
t.true(subprocess.killed);
310+
});
311+
}

0 commit comments

Comments
 (0)