A task runner executes a graph of named, interdependent jobs in the right order: each task runs only after the tasks it depends on have finished, and tasks that do not depend on each other run at the same time. It is the engine inside build tools like Gulp, Just, and npm scripts — you declare what depends on what, and the runner figures out the order and the parallelism. You will build a small one, exposed as a factory taskRunnerDepGraph().
const runner = taskRunnerDepGraph();
runner.task(name, deps, fn); // register a task
runner.task(name, fn); // shorthand when the task has no dependencies
// name: string unique task id
// deps: string[] names of tasks that must finish first (default [])
// fn: (results) => any receives { [depName]: result }; may be sync or async
runner.run(target); // Promise — resolves with target's result once its subgraph has run
const runner = taskRunnerDepGraph();
runner.task('clean', () => 'cleaned');
runner.task('compile', ['clean'], () => 'compiled');
runner.task('lint', ['clean'], () => 'linted');
runner.task('bundle', ['compile', 'lint'], (results) => [results.compile, results.lint]);
await runner.run('bundle'); // ['compiled', 'linted']
// clean runs first; compile and lint then run in parallel; bundle runs last.
// A dependency's result is passed to the task that needs it.
runner.task('config', () => ({ port: 3000 }));
runner.task('url', ['config'], (results) => `http://localhost:${results.config.port}`);
await runner.run('url'); // 'http://localhost:3000'
// A cycle is rejected, not run forever.
runner.task('a', ['b'], () => 1);
runner.task('b', ['a'], () => 2);
await runner.run('a'); // rejects: Cycle detected: a -> b -> a
fn runs only after every task in its deps has resolved. Tasks that do not depend on one another run concurrently, not one after the other.fn receives one object mapping each dependency name to that dependency's resolved result, for example { compile: '...', lint: '...' }. A leaf task with no dependencies receives {}.run — if two tasks both depend on a third, that third runs a single time and both dependents share its result (a diamond runs the shared task once).a -> b -> a rejects naming the cycle instead of overflowing the stack; and if a task's fn rejects, run rejects and the tasks that depend on it never start.fn may be sync or async — return a value or a promise; the runner awaits either before handing the result to dependents.task and run are the whole surface.