topo-strict
v1.0.0
Published
Strict topological sorting
Downloads
5
Maintainers
Readme
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
group
option-- 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
before
andafter
options-- 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 timesolve
is 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-strict
more 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.topo
only allows referencing of group keys throughbefore
andafter
constraints. 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-strict
is 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 youradd
calls-- essentially an implementation detail-- to influence the result.topo-strict
supports several additional signatures to theadd
method, described below.topo-strict
also comes with some features for easily viewing the Problem and the graph that will be used to solve it, also described below.topo-strict
does not attempt to solve the problem until you callsolve
, whereastopo
solves it after every single call toadd
. This is unlikely to make a significant difference performance-wise unless you have a huge number ofadd
calls, so it's hardly worth noting. I noted it anyway, though. :)
Disadvantages vs topo
topo-strict
does not yet support merging, astopo
does. 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-strict
are obviously not always appropriate, and in situations where you'd rather be loose and simple you should probably usetopo
.topo-strict
has a much larger footprint thantopo
, not least of which because it depends on the whole oflodash
, sotopo
will usually be more appropriate for use in browsers.Because
topo-strict
does not attempt to solve the Problem until you callsolve
, it won't detect cycles at the moment you create them, the waytopo
does.
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' } ] }
*/