Core Concepts
Understand how Inval models computation as a dependency graph.
Dependency Graphs
Inval models all computation as a Directed Acyclic Graph (DAG). Each node is either an input (leaf) or a computed value (internal node).
The graph has these properties:
- Directed — edges go from dependencies to dependents
- Acyclic — no cycles allowed; detected at construction time
- Lazy — computed nodes only evaluate when accessed
- Incremental — only dirty nodes recompute
Think of it like a spreadsheet: cells reference other cells, and changing one cell updates only the cells that depend on it.
Input Nodes
Input nodes are leaf nodes in the graph. They hold values set by external code.
import { input } from '@blu3ph4ntom/inval'
const width = input(800)
// Read the value
width.get() // 800
// Set a new value — marks dependents dirty
width.set(600)
// Force invalidation (rarely needed)
width.invalidate()
// Inputs are never dirty
width.isDirty() // false
// Debug info
width.inspect() // { id, name, value, type: 'input' }
// Disconnect from graph
width.dispose() Key Properties
get()— returns current value (always fresh)set(value)— updates value and marks dependents dirtyinvalidate()— forces invalidation of dependentsisDirty()— always returns false for inputsdispose()— removes from dependency graph
Computed Nodes
Computed nodes declare their dependencies and compute lazily. They cache their result and only recompute when a dependency changes.
import { input, node } from '@blu3ph4ntom/inval'
const width = input(800)
const height = input(600)
const area = node({
dependsOn: { w: width, h: height },
compute: ({ w, h }) => w * h
})
// First access: computes
area.get() // 480000
// Second access: cached
area.get() // 480000 (no recompute)
// After dependency change: dirty
width.set(1000)
area.isDirty() // true
// Next access: recomputes
area.get() // 600000
// Force recompute even if not dirty
area.invalidate()
// Cleanup
area.dispose() Key Properties
get()— returns cached value or recomputes if dirtyisDirty()— true if any dependency changedinvalidate()— forces recompute on next get()dispose()— removes from dependency graph
Tip
The dependsOn object maps names to nodes. The compute function receives an object with the same keys, resolved to their current values.
Caching & Dirty State
Every computed node maintains a dirty flag. When an input changes, it walks the graph and marks all downstream nodes dirty. But they don't recompute immediately — they wait until get() is called.
const a = input(10)
const b = input(20)
const sum = node({
dependsOn: { a, b },
compute: ({ a, b }) => {
console.log('computing...')
return a + b
}
})
sum.get() // "computing..." → 30
sum.get() // 30 (cached, no log)
sum.get() // 30 (cached, no log)
a.set(15)
// No computation yet — lazy
sum.get() // "computing..." → 35 (recomputed) This lazy evaluation means you pay zero cost for unused computations. Only nodes you actually read get recomputed.
Batch Updates
When you need to change multiple inputs at once, use batch() to ensure atomic updates.
import { input, node, batch } from '@blu3ph4ntom/inval'
const a = input(1)
const b = input(2)
const sum = node({
dependsOn: { a, b },
compute: ({ a, b }) => a + b
})
// Without batch: sum recomputes twice
a.set(10) // sum marked dirty
b.set(20) // sum marked dirty again
sum.get() // recomputes
// With batch: sum recomputes once
const changed = batch(() => {
a.set(100)
b.set(200)
})
// changed = Set containing sum
sum.get() // recomputes once Warning
Always use batch() when setting multiple inputs. Without it, each set() triggers a separate invalidation walk.
Lifecycle
Nodes have a simple lifecycle:
Create
Call input() or node(). Edges are registered. Cycles detected.
Access
Call get(). Computed nodes evaluate if dirty, return cached otherwise.
Update
Call set() on inputs. Downstream nodes marked dirty. No recomputation yet.
Dispose
Call dispose() to remove from graph and free references.