A tiny, human-readable flow engine inspired by FFmpeg filtergraphs and Processing modes.
No nodes. No operators. No DSL gymnastics.
- Named pipes are event channels
- Edges describe pipelines
- Arrays mean parallel
- Parallel stages auto-join
- Everything else is series
- Worker threads are optional
NOTE: The worker pool is more suited for CPU-bound pure-JS transforms where you want to avoid blocking the main thread — e.g., heavy string processing, JSON transformations, or math-intensive filters across thousands of packets.
function myTransform(options = {}) {
return (send, packet) => {
send({ ...packet, value: packet.value * 2 });
};
}- Outer function = configuration
- Inner function = execution
- Async supported
An edge is:
[
inputPipe,
stage1,
stage2,
...stageN,
outputPipe
]['post', normalize, verify, 'updated']['post', [cover, audio, post], 'updated']['post', [cover, audio, post], verify, backup, 'updated']Semantics:
(post)
├─► cover
├─► audio
└─► post
│
[auto-join]
│
verify → backup → (updated)
No explicit barrier required.
function socket(channel) {
return send => {
setTimeout(() => {
send({ payload: { type: 'new-post' }, topic: channel });
}, 100);
};
}Usage:
[socket('post'), 'post']import { flow } from './index.js';
const blog = flow(
[
[socket('post'), 'post'],
['post', [cover, audio, post], verify, backup, 'updated'],
['updated', pagerizer, 'done']
],
{
context: { user: 'alice' },
workers: 8 // optional
}
);
blog.start();- Disabled when
workers === undefined - Enabled with fixed pool size
- Best for CPU-heavy transforms
flow(graph, { workers: 4 });Always explicit.
blog.dispose();- Removes all listeners
- Terminates worker threads
- Tears down entire graph
- Arrays always imply parallelism
- Parallel stages always auto-join
- Join happens before the next stage
- Output pipes only receive joined packets
- Graphs are static and readable
If you can draw the graph on a whiteboard, the API is correct.
Think FFmpeg filtergraphs, not Node-RED nodes. Think dataflow, not operators.
MIT