Graph — Dependent Async Tasks
2.88 kB minified + gzipped
A customer page reads one customer record and feeds two panels from it. Each panel fetches the record for itself, so the page reads it twice and the slower request decides when the page settles. When the record lookup fails, the second request is still running for a page that will never render.
Core Behavior
Section titled “Core Behavior”Graph<E, Out, Input> is a validated plan that runs a target node and the nodes it depends on as
asynchronous tasks.
- A node starts after every declared dependency succeeds. Ready nodes run concurrently, limited by
concurrency, and a shared node runs once per run. - The first node error settles the graph with that error and aborts the signal passed to active nodes.
- A caller abort settles the graph with
Aborted, even when active tasks keep working. - Node ids and cycles are checked when the graph is built, so a bad plan fails before any work starts.
Reach for Graph when several asynchronous computations share inputs and only some depend on
others. A single lazy operation needs only Task. The type model
explains how a graph definition carries its input, and the choosing guide puts
the execution shapes side by side.
Example: Build Metadata
Section titled “Example: Build Metadata”A build report needs both the byte count and line count of a manifest. Both measurements depend on reading the same file, so the file should be read once before either measurement starts.
A dependent node waits for every declared dependency. A shared dependency is reused by both branches:
flowchart TD
Ready["Root nodes ready"] --> Run["Run a node with input and dependency outputs"]
Run --> Outcome{"Node outcome"}
Outcome -->|Ok| Store["Store the output"]
Store --> Target{"Target node?"}
Target -->|Yes| Settle["Settle the graph with Ok"]
Target -->|No| Queue["Queue dependents whose last dependency completed"]
Queue --> Run
Outcome -->|Err| Fail["Settle the graph with Err and abort active nodes"]
Preparing the Report
Section titled “Preparing the Report”This example reads the project’s manifest, calculates two measurements, and combines them. Run it from the project root:
manifest runs once, and both measurement nodes start after it succeeds. report then waits for
both measurements.
Publishing the Report in a Transaction
Section titled “Publishing the Report in a Transaction”The report is computed in memory. Writing it out and recording the build are two steps that must not leave a partial result behind when the second one fails, which is where a transaction takes over:
If the build log step fails, the transaction runs the report step’s undo and deletes the file before
returning the failure. See Transaction for the full step surface.
Core Operations
Section titled “Core Operations”Defining Nodes
Section titled “Defining Nodes”| Signature | What it does | Behavior or return type |
|---|---|---|
Graph.nodeFactory<Input>() |
Creates a node constructor with a shared input contract. | Infers dependency outputs and errors. |
Graph.node(id, run) |
Defines a node without dependencies. | The callback receives ctx. |
Graph.node(id, deps, run) |
Defines a node with named dependencies. | The callback receives ctx and deps. |
Graph.make(target, options?) |
Collects the target and its reachable dependencies. | Throws on duplicate ids or a cycle. |
Running Graphs
Section titled “Running Graphs”| Signature | What it does | Return type |
|---|---|---|
Graph.run(input, options?)(graph) |
Runs the graph with a per-run input. | Data-last; a deferred Result<E | Aborted, Out>. |
Graph.run()(graph) |
Runs a graph whose nodes need no input. | Same outcome, with E | Aborted. |
A node reads the run input from ctx.input and dependency outputs from deps:
({ ctx, deps }) => task. A node with no input needs only the dependencies, or no argument at all.
The graph definition keeps its input type; Graph.run checks the value against that type instead of
widening the graph to accept it. A record assembled at runtime keeps the contract when it is typed
Record<string, Graph.AnyNode<ReportInput>>. A record typed as plain Graph.AnyNode erases the
input contract, and Graph.make rejects it.
concurrency limits how many nodes run at once and defaults to unbounded. A nonpositive or
non-integer value clamps to 1. Apply Task.Result.retry(policy) or
Task.Result.timeout({ duration, onTimeout }) to the task a node returns when it needs retries or a
deadline; the signal supplied to that task includes graph cancellation.
Failure Behavior and Edge Cases
Section titled “Failure Behavior and Edge Cases”A node error settles the graph with that error. Dependents that have not started are skipped, and active nodes receive an aborted signal but must honour it to stop. When the target succeeds, any sibling still running is signalled to stop as well.
A caller signal settles the graph with Aborted immediately, carrying
signal.reason, even if node tasks ignore the signal. An already-aborted signal settles before any
node starts. A caller abort also aborts the signal passed to each node.
Graph.node throws a TypeError for an empty id or a missing run callback. Graph.make throws for
duplicate ids and for cycles among reachable nodes. Node callbacks and the tasks they return must
not throw or reject; a rejected node task leaves the run pending because the scheduler never
observes a settlement. Wrap fallible work in Task.Result.tryCatch.
A graph with no input is run as Graph.run()(graph). When that graph also needs run options, pass
undefined in the input position: Graph.run(undefined, { signal })(graph).