@gingur/auto-graph
v0.3.1
Published
Promise-first dependency graph runner using fluent generics for type chaining
Maintainers
Readme
auto-graph
A promise-first dependency graph runner with fluent generics for type-safe task chaining. Build complex workflows with automatic parallelization and intelligent dependency resolution.
Features
- 🚀 Automatic Parallelization - Independent tasks run concurrently
- 🔗 Smart Dependency Resolution - Tasks execute as soon as their dependencies are ready
- 🎯 Fluent Generics - Type information flows through the chain for full type safety
- 💾 Incremental Computation - Cache results to skip expensive recomputation
- 🔄 Immutable API - Each
.add()returns a new graph instance - ⚡ Promise-First - Native async/await support throughout
Installation
npm install @gingur/auto-graphQuick Start
import { AutoGraph } from '@gingur/auto-graph';
const graph = new AutoGraph()
.add('fetchUser', async () => {
const response = await fetch('/api/user');
return response.json();
})
.add('fetchPosts', async () => {
const response = await fetch('/api/posts');
return response.json();
})
.add('enrichPosts', ['fetchUser', 'fetchPosts'], ({ fetchUser, fetchPosts }) => {
return fetchPosts.map((post) => ({
...post,
author: fetchUser.name,
}));
});
const results = await graph.run();
console.log(results.enrichPosts);API
new AutoGraph()
Creates a new empty graph.
.add(name, fn) or .add(name, deps, fn)
Add a task to the graph. Returns a new graph instance with the task added.
// No dependencies
.add('task1', () => 42)
// With dependencies
.add('task2', ['task1'], ({ task1 }) => task1 * 2).run(cache?)
Execute the graph and return all task results.
const results = await graph.run();
// With cache to skip expensive tasks
const results = await graph.run({ expensiveTask: cachedValue });Fluent Generics & Type Safety
The library uses fluent generics to maintain type information as you chain .add() calls. Each task's return type is automatically inferred and available to dependent tasks:
const graph = new AutoGraph()
.add('num', () => 42) // Returns number
.add('str', () => 'hello') // Returns string
.add('combined', ['num', 'str'], ({ num, str }) => {
// num is typed as number, str is typed as string
return `${str}: ${num}`;
});⚠️ Gotcha: Breaking the Chain
You must chain .add() calls directly to maintain type information. Breaking the chain loses type safety:
// ❌ BAD - Type information is lost
let graph = new AutoGraph();
graph = graph.add('a', () => 1);
graph = graph.add('b', ['a'], ({ a }) => a + 1); // 'a' is not typed correctly!
// ✅ GOOD - Types flow through the chain
const graph = new AutoGraph().add('a', () => 1).add('b', ['a'], ({ a }) => a + 1); // 'a' is correctly typed as number⚠️ Gotcha: Storing Intermediate Graphs
Storing intermediate graphs in variables can cause type issues:
// ❌ BAD - baseGraph doesn't know about tasks added later
const baseGraph = new AutoGraph().add('a', () => 1);
const extendedGraph = baseGraph.add('b', ['a'], ({ a }) => a + 1);
// This won't work as expected:
const broken = baseGraph.add('c', ['b'], ({ b }) => b + 1); // Error: 'b' doesn't exist on baseGraph!
// ✅ GOOD - Chain from the extended graph
const working = extendedGraph.add('c', ['b'], ({ b }) => b + 1); // Works correctlyExamples
Parallel Execution
Independent tasks run in parallel automatically:
const graph = new AutoGraph()
.add('fetchUsers', async () => await db.users.findMany())
.add('fetchProducts', async () => await db.products.findMany())
.add('combine', ['fetchUsers', 'fetchProducts'], ({ fetchUsers, fetchProducts }) => ({
users: fetchUsers,
products: fetchProducts,
}));
await graph.run(); // fetchUsers and fetchProducts run in parallelBuild Pipeline
const pipeline = new AutoGraph()
.add('lint', async () => {
await exec('eslint .');
return { errors: 0 };
})
.add('typeCheck', async () => {
await exec('tsc --noEmit');
return { errors: 0 };
})
.add('compile', ['lint', 'typeCheck'], async () => {
await exec('tsc');
return { output: 'dist/' };
})
.add('test', ['compile'], async () => {
await exec('npm test');
return { passed: true };
})
.add('bundle', ['compile'], async () => {
await exec('webpack');
return { file: 'dist/bundle.js' };
});
await pipeline.run(); // test and bundle run in parallel after compileIncremental Computation
Skip expensive recomputation by providing cached results:
const graph = new AutoGraph()
.add('expensiveTask', async () => {
await heavyProcessing();
return { result: 'data' };
})
.add('processResult', ['expensiveTask'], ({ expensiveTask }) => {
return expensiveTask.result.toUpperCase();
});
// First run
const firstRun = await graph.run();
// Second run - reuse expensive computation
const cache = { expensiveTask: firstRun.expensiveTask };
const secondRun = await graph.run(cache); // expensiveTask is not re-executedComplex Dependencies
const graph = new AutoGraph()
.add('a', () => 1)
.add('b', () => 2)
.add('c', () => 3)
.add('d', ['a'], ({ a }) => a * 2)
.add('e', ['b'], ({ b }) => b * 2)
.add('f', ['c'], ({ c }) => c * 2)
.add('g', ['d', 'e'], ({ d, e }) => d + e)
.add('h', ['e', 'f'], ({ e, f }) => e + f)
.add('result', ['g', 'h'], ({ g, h }) => g * h);
const results = await graph.run();
console.log(results.result); // 60Error Handling
Build-time validation catches common mistakes:
// Duplicate task names
new AutoGraph().add('task1', () => 1).add('task1', () => 2); // Throws: Task 'task1' already exists
// Missing dependencies
new AutoGraph().add('task1', () => 1).add('task2', ['missing'], () => 2); // Throws: depends on missing task 'missing'Runtime errors propagate normally:
const graph = new AutoGraph().add('task1', () => {
throw new Error('Failed');
});
try {
await graph.run();
} catch (err) {
console.error('Task failed:', err);
}Performance
- Parallel Execution: Independent tasks run concurrently
- Eager Execution: Tasks start immediately when dependencies are ready
- No Polling: Promise-based coordination, no busy-waiting
Requirements
- Node.js >= 22
Inspiration
This project was inspired by async.auto from the Async.js library. Thanks to all contributors for pioneering these concepts and helping improve developer experience.
License
MIT
Author
Troy Rhinehart
