Composition

Patterns for declared reactive graphs.

Use these public patterns to shape inputs, reductions, joins, lifecycle, and effects before jumping to package-specific syntax.

walkthrough

Tiny Graph

Summary

Start with explicit graph topology: a few named inputs, a derived value, and an effect boundary where the outside world observes the result.

Build the Shape First

Model the system as declared nodes and edges before choosing a language package. A source owns input facts, a derived node reduces those facts into a smaller value, and an effect is the boundary where output leaves the graph.

This is the public mental model behind the package APIs: topology is inspectable, data moves through messages, and ordinary application code should not reach around the graph to trigger work.

  • Name the inputs you expect to change.
  • Create derived nodes for reductions and transformations.
  • Keep effects at the edge of the graph, not hidden in the middle of computation.

Where Code Lives

Use this shared guide to understand the composition shape, then follow the TypeScript, Python, or Rust package docs for exact factory names, examples, generated API reference, and install commands.

  • Shared site: composition concepts and cross-language guarantees.
  • Language repos: runnable examples, API details, demos, and release notes.

concept

Activation and First Run

Summary

A composed node should not compute from imaginary inputs: first execution waits until its declared dependencies have produced real data, unless the author explicitly opts into partial behavior.

Why It Matters

Reactive systems often fail by running too early and smuggling placeholder values through the graph. GraphReFly treats absence as absence. A multi-input derived node waits for real input from every declared dependency before its first normal computation.

  • Cold nodes announce subscription with START.
  • Cached data can be pushed to a new subscriber.
  • Missing dependency data stays missing; it is not replaced with a fake value.

Composition Rule of Thumb

Prefer declaring the actual dependencies and letting first-run gating protect the initial wave. If a pattern truly needs partial data, make that choice explicit and handle the missing input in the node body or language-level helper.

  • Do not add companion nodes solely to pretend a value exists.
  • Do not use null or an empty object as a hidden bootstrap signal.
  • Use language-local examples for the exact partial-input API.

pattern

No Placeholder State

Summary

Represent unknown input as not-yet-emitted, not as a fake domain value. This keeps reductions honest and makes inspection explainable.

Use Absence Honestly

A graph can distinguish no data from data whose value is null. That distinction is important for joins, forms, retained views, and any composition where a missing input should delay or suppress output instead of being treated as a real business value.

  • No emission means the dependency has not produced DATA yet.
  • Null can be a real domain value when the domain allows it.
  • Invalidation clears a prior value instead of replacing it with a made-up placeholder.

Avoid

Avoid eager bootstrap payloads that only exist to make a downstream node run. They hide the true lifecycle of the graph and force every later composition to remember which values are real.

  • No default object as a secret lifecycle marker.
  • No magic string such as "loading" unless it is genuine domain data.
  • No package-level API mirror in shared docs; link to language-local helpers.

concept

Glitch-Free Joins

Summary

When one input fans out and rejoins, the join should see the settled wave once, not a half-old and half-new mix.

The Join Contract

A diamond shape is common: one upstream fact feeds two derived branches, then a downstream node joins them. GraphReFly treats one emission array as one wave, propagates dirtiness before values, and lets the join compute after the changed dependencies settle.

  • The downstream join recomputes once for the wave.
  • Intermediate dirty state is observable as protocol behavior, not a value-level API burden.
  • Batching can coalesce work without changing the declared topology.

How to Compose With It

Design joins as reductions over declared dependencies. Do not manually trigger the join from each branch or read another branch's cached value inside a reactive function.

  • Declare both branch outputs as dependencies.
  • Let the graph coordinate the wave.
  • Use package-local operators for convenience, but keep the topology explainable.

pattern

Terminal Reductions

Summary

Some reductions intentionally wait for completion. Compose them as terminal-aware graph nodes instead of polling or peeking at upstream caches.

When to Use It

Use terminal reductions for patterns such as collect-then-summarize, last value, final report, bounded import, or any stream where the output is only meaningful after the input has settled.

  • Completion is part of the graph lifecycle.
  • Errors are data for error-handling patterns or protocol terminal events, depending on the package helper.
  • Resource cleanup belongs at activation/deactivation boundaries.

Keep the Boundary Clear

A terminal-aware composition should still be declared and inspectable. It should not run timers, polling loops, hidden subscriptions, or direct cache reads inside the reduction to discover whether upstream work is done.

  • Use declared dependencies for input.
  • Use package-local terminal operators or examples for exact API details.
  • Keep external resource cleanup in the language package's source/effect boundary.