topo-strict v1.0.0
topo-strict
Strict topological sorting, with API inspired by topo.
Basic Usage
The basic API is fairly similar to topo. You can create an instance of
Problem and add items to it, then solve using the Problem#solve method:
const { Problem } = require('topo-strict');
const morning = new Problem();
morning.add('Nap', { after: [ 'breakfast', 'prep' ] });
morning.add([
'Make toast',
'Pour juice',
], { before: 'breakfast', group: 'prep' });
morning.add('Eat breakfast', { group: 'breakfast' });
console.log(morning.solve());
// [ 'Make toast', 'Pour juice', 'Eat breakfast', 'Nap' ]The Rules
True to the name, topo-strict enforces several rules that are not enforced by
topo, and leverages Nani to produce
detailed errors whenever something goes wrong. The rules are as follows:
Strings added to a Problem-- henceforth known as ids-- must be unique, and must not be empty.
Strings provided to the
groupoption-- henceforth known as group keys-- must also be non-empty and may not share a value with any ids in the Problem.Strings provided to the
beforeandafteroptions-- henceforth known as constraint keys-- need not reference existing ids or group keys at the time they are added, but must do so by the the timesolveis called.
Violating any of these rules-- or providing non-string values to any of these
options-- will cause a ValidationError that contains KeyErrors identifying
all bad keys-- that is, ids, group keys, or constraint keys-- making it easy to
tell exactly what went wrong.
Advantages vs. topo
The strict enforcement of the rules above makes
topo-strictmore appropriate for situations where you really want to make sure nothing unexpected ever happens-- where failing fast with detailed errors is preferable-- such as dependency ordering that occurs when a server starts up.topoonly allows referencing of group keys throughbeforeandafterconstraints. Any ungrouped items cannot be sorted topologically. This means that in order to make a truly extensible dependency order, you have to specify a group with everything you add, even if you don't intend to add anything else to the group.The final order of ids from
topo-strictis completely independent of insertion order. Instead, a single canonical solution is found by prioritizing ids alphabetically. This is critical if you don't want the order of youraddcalls-- essentially an implementation detail-- to influence the result.topo-strictsupports several additional signatures to theaddmethod, described below.topo-strictalso comes with some features for easily viewing the Problem and the graph that will be used to solve it, also described below.topo-strictdoes not attempt to solve the problem until you callsolve, whereastoposolves it after every single call toadd. This is unlikely to make a significant difference performance-wise unless you have a huge number ofaddcalls, so it's hardly worth noting. I noted it anyway, though. :)
Disadvantages vs topo
topo-strictdoes not yet support merging, astopodoes. The rules and validation approach make this feature quite a bit more complicated, so I've put off implementing it until I personally need it for something. I'm happy to accept contributions from anybody who might want it sooner.The rules of
topo-strictare obviously not always appropriate, and in situations where you'd rather be loose and simple you should probably usetopo.topo-stricthas a much larger footprint thantopo, not least of which because it depends on the whole oflodash, sotopowill usually be more appropriate for use in browsers.Because
topo-strictdoes not attempt to solve the Problem until you callsolve, it won't detect cycles at the moment you create them, the waytopodoes.
More Stuff
A few more features of topo-strict will be discussed here. You can read about
anything else in the api docs.
Alternate add Signatures
When you're adding stuff to a Problem, you can use the signatures demonstrated in Basic Usage above, or you can do these:
// Options-only form
problem.add({
ids: [ 'foo', 'bar', 'baz' ],
group: 'qux',
});
// Shorthand form with multiple ids.
problem.add('foo', 'bar', 'baz', { group: 'qux' });
// Multiple arrays of ids.
problem.add([ 'foo', 'bar' ], [ 'baz', 'qux' ]);
// Go crazy with it if you want...
problem.add('foo', [ 'bar', 'baz' ], {
ids: [ 'qux' ],
group: 'yay',
before: [ 'omg', 'wow' ],
after: 'wtf',
});Debugging and Visualization Features
If you want to see a summary of the the whole problem, you can use the
Problem#toString method:
console.log(morning.toString());
/*
ids
---
Eat breakfast
Make toast
before: breakfast
Nap
after: breakfast
after: prep
Pour juice
before: breakfast
groups
------
breakfast
Eat breakfast
prep
Make toast
Pour juice
*/The Problem class also exposes the #toGraph method, which returns an instance
of Graph which also implements the #toString and #solve methods. This
allows you to preview the directed graph that's used to solve the problem before
solving it:
const morningGraph = morning.toGraph();
console.log(morningGraph.toString());
/*
nodes
-----
Eat breakfast
Make toast
Nap
Pour juice
edges
-----
from: Eat breakfast, to: Nap
from: Make toast, to: Eat breakfast
from: Make toast, to: Nap
from: Pour juice, to: Eat breakfast
from: Pour juice, to: Nap
*/
console.log(morningGraph.solve());
// [ 'Make toast', 'Pour juice', 'Eat breakfast', 'Nap' ]Like Problem#solve, Problem#toGraph with throw a ValidationError if any
constraint keys reference ids or group keys do not exist in the problem, and
like Graph#solve will throw a CycleError if a cycle is detected in the
graph.
Both the Problem and Graph classes also expose #toObject methods, which are
the basis for the #toString methods. This saves you the trouble of parsing the
above strings, if you're looking to implement your own visualization:
const { inspect } = require('util');
console.log(util.inspect(morning.toObject(), { depth: null }));
/*
{ ids:
[ { key: 'Eat breakfast', constraints: [] },
{ key: 'Make toast',
constraints: [ { type: 'before', key: 'breakfast' } ] },
{ key: 'Nap',
constraints:
[ { type: 'after', key: 'breakfast' },
{ type: 'after', key: 'prep' } ] },
{ key: 'Pour juice',
constraints: [ { type: 'before', key: 'breakfast' } ] } ],
groups:
[ { key: 'breakfast', ids: [ 'Eat breakfast' ] },
{ key: 'prep', ids: [ 'Make toast', 'Pour juice' ] } ] }
*/
console.log(util.inspect(morningGraph.toObject(), { depth: null }));
/*
{ nodes: [ 'Eat breakfast', 'Make toast', 'Nap', 'Pour juice' ],
edges:
[ { from: 'Eat breakfast', to: 'Nap' },
{ from: 'Make toast', to: 'Eat breakfast' },
{ from: 'Make toast', to: 'Nap' },
{ from: 'Pour juice', to: 'Eat breakfast' },
{ from: 'Pour juice', to: 'Nap' } ] }
*/