/
completion-listener.ts
90 lines (83 loc) · 3.44 KB
/
completion-listener.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import * as Rx from 'rxjs';
import { bufferCount, switchMap, take } from 'rxjs/operators';
import { CloseEvent, Command } from './command';
/**
* Defines which command(s) in a list must exit successfully (with an exit code of `0`):
*
* - `first`: only the first specified command;
* - `last`: only the last specified command;
* - `all`: all commands.
* - `command-{name|index}`: only the commands with the specified names or index.
* - `!command-{name|index}`: all commands but the ones with the specified names or index.
*/
export type SuccessCondition = 'first' | 'last' | 'all' | `command-${string|number}` | `!command-${string|number}`;
/**
* Provides logic to determine whether lists of commands ran successfully.
*/
export class CompletionListener {
private readonly successCondition: SuccessCondition;
private readonly scheduler?: Rx.SchedulerLike;
constructor({ successCondition = 'all', scheduler }: {
/**
* How this instance will define that a list of commands ran successfully.
* Defaults to `all`.
*
* @see {SuccessCondition}
*/
successCondition?: SuccessCondition,
/**
* For testing only.
*/
scheduler?: Rx.SchedulerLike,
}) {
this.successCondition = successCondition;
this.scheduler = scheduler;
}
private isSuccess(events: CloseEvent[]) {
if (this.successCondition === 'first') {
return events[0].exitCode === 0;
} else if (this.successCondition === 'last') {
return events[events.length - 1].exitCode === 0;
} else if (!/^!?command-.+$/.test(this.successCondition)) {
// If not a `command-` syntax, then it's an 'all' condition or it's treated as such.
return events.every(({ exitCode }) => exitCode === 0);
}
// Check `command-` syntax condition.
// Note that a command's `name` is not necessarily unique,
// in which case all of them must meet the success condition.
const [, nameOrIndex] = this.successCondition.split('-');
const targetCommandsEvents = events.filter(({ command, index }) => (
command.name === nameOrIndex
|| index === Number(nameOrIndex)
));
if (this.successCondition.startsWith('!')) {
// All commands except the specified ones must exit succesfully
return events.every((event) => (
targetCommandsEvents.includes(event)
|| event.exitCode === 0
));
}
// Only the specified commands must exit succesfully
return targetCommandsEvents.length > 0
&& targetCommandsEvents.every(event => event.exitCode === 0);
}
/**
* Given a list of commands, wait for all of them to exit and then evaluate their exit codes.
*
* @returns A Promise that resolves if the success condition is met, or rejects otherwise.
*/
listen(commands: Command[]): Promise<CloseEvent[]> {
const closeStreams = commands.map(command => command.close);
return Rx.merge(...closeStreams)
.pipe(
bufferCount(closeStreams.length),
switchMap(exitInfos =>
this.isSuccess(exitInfos)
? Rx.of(exitInfos, this.scheduler)
: Rx.throwError(exitInfos, this.scheduler),
),
take(1),
)
.toPromise();
}
};