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.

inputs.ts TYPESCRIPT
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 dirty
  • invalidate() — forces invalidation of dependents
  • isDirty() — always returns false for inputs
  • dispose() — 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.

computed.ts TYPESCRIPT
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 dirty
  • isDirty() — true if any dependency changed
  • invalidate() — 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.

caching.ts TYPESCRIPT
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.

batch.ts TYPESCRIPT
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:

1

Create

Call input() or node(). Edges are registered. Cycles detected.

2

Access

Call get(). Computed nodes evaluate if dirty, return cached otherwise.

3

Update

Call set() on inputs. Downstream nodes marked dirty. No recomputation yet.

4

Dispose

Call dispose() to remove from graph and free references.