Covector is a package that helps with versioning and publishing your packages. The process is typically built into two separate steps: a version step and a publish step. The version step tends towards functional purity, but the publish step is built around side effects.
The user configures the functions that need to run to publish each package. It can include multiple functions leading up to and following a publish step. Each of the packages are run serially (as we can expect some interdependencies exist if you are publishing from a monorepo), but we need to wait for each of these side effects to complete before proceeding.
This is where effection really begins to shine. It is a library built around managing side effects. It runs atop generators which are much more expressive than promises. In the ideal state, running covector from promises achieves the objectives, but it begins to have issues when things error or stop responding. We are running user configured commands so we have to expect that error states will occur.
When a command throws an error, we can exit covector. Typically if we have an error state in a publish command that exits the process, we can expect to not continue the publish sequence. This does not operate differently between promises and the generators in effection. Deep process trees and commands that stop responding operate much differently though.
When you are executing a child process via a promises, there is no insight or affect you can have on the command. You fire it off, and hope it comes back with information. What do you do if it hangs indefinitely? You can optionally kill off after a timeout, but how do you decide the length of timeout? The period can reasonably vary quite drastically depending on building and bundling steps that happen prior to publishing. The structure of promises does not lend itself well to streaming back the command output as it executes either. We can find ourselves in a situation where a command timeout is set for 60 minutes, but the process hangs and we don’t receive any output until it is flushed as the process is killed after 60 minutes.
Effection is built around the scope of a function. An operation is invoked within the scope of another. When a parent process shuts down, it can handle shutting down all of the children processes based on that scoping. The structure also makes it trivial (relative to promises) to stream the output of a command as it executes. This means we can receive immediate feedback on an error, and optionally manually cancel a process if we choose.
The following is an example using execa
which is promise based (using async/await here) and does not support streaming the output as it executes. See that we end up needing to add the timeout as an option on the command. We don’t receive any output until the process has completed (regardless of the state it exits in).
try {
const child = await execa.command(command, {
...options,
all: true,
timeout: 1200000,
});
const out = child.stdout;
if (log !== false) {
console.log(out);
}
return out;
} catch (e) {
throw new Error(e);
}
The following is an example of what invoking a command in an effection (v1) scope with generators. Note we don’t specify a timeout anywhere, and we are able to immediately stream the output to the console as it happens.
let child: Process = yield exec(command, options);
if (log !== false) {
yield spawn(
child.stdout
.subscribe()
.forEach(function* (datum) {
const out = stripAnsi(datum.toString().trim());
if (out !== "") console.log(out);
})
);
yield spawn(
child.stderr
.subscribe()
.forEach(function* (datum:)> {
const out = stripAnsi(datum.toString().trim());
if (out !== "") console.error(out);
})
);
}
const out = yield child.expect();
const stripped: string = stripAnsi(
Buffer.concat(out.tail).toString().trim()
);
return stripped;
How do we stop this process if it hangs if we don’t specify it on the command? This is the beauty of the effection scope. We can use the following function and invoke it anywhere in our scope tree, and it will stop and tear down everything cleanly. We can use this at the command level with a shorter timeout, and higher up in the process scope to stop the process after an “overall” time has been reached.
export const raceTime = function ({
t = 1200000,
msg = `timeout out waiting ${t / 1000}s for command`,
}: {
t?: number,
msg?: string,
} = {}) {
return spawn(function* () {
yield timeout(t);
throw new Error(msg);
});
};
This functionality provides a foundation that we can build from; a foundation where we can act on the output and worry less about wiring up our code and structuring it to manage cleanly exiting each of the processes. This architecture also opens up a path where covector can make executions that are strictly built around child_process
. Covector may see a future where the core is about building up the context about the state and publish sequencing of a code base, and then hand off the actual execution elsewhere. Adding this type of option would be increasingly complex with the expectations built into processes via promises.