Pardon our dust! All this is still a work in progress.

Work

A small reactive task graph for typed async computations

@hello-terrain/work

npm version
1.80
run
tasks
executed / cached
events
s:0 f:0 c:0 e:0
seed
1337

Overview

@hello-terrain/work is a small library for building reactive computation graphs. You define inputs (param), computations (task), and run them in an engine (graph).

It's designed to run in a hot loop (such as your render update loop), with minimal overhead. That's because it will bake the graph topology on the first call, and memoize (cache) unchanged dependencies.

Dependencies are marked dirty when you change them by calling myParam.set(newValue) on the param handler.

The graph won't be executed until you explicitly call .run(), but it will automatically sort out dirty dependencies and mark downstream tasks for re-computation.

You can set tasks to a "lane" (a simple string tag) and optionally enable per-lane concurrency limits by passing laneConcurrency to graph.run(). This is useful when you want to cap access to constrained resources (e.g. GPU work, rate-limited APIs, small DB pools).

Key features:

  • Typed dependency graphs (params + tasks)
  • Memoized recomputation when inputs change
  • Optional lane-based concurrency via laneConcurrency
  • Cancellation (AbortSignal)
  • Observability via a lightweight event stream

Key Concepts

ConceptDescription
ParamA reactive value that can be read, updated, and subscribed to
TaskA computation node that depends on params or other tasks
GraphAn execution engine that runs tasks with dependency resolution and caching

Installation

# npm
npm install @hello-terrain/work

# pnpm
pnpm add @hello-terrain/work

# yarn
yarn add @hello-terrain/work

Quick start

import { graph, param, task } from "@hello-terrain/work";

const value = param(2);

const calcSquare = task((get, work) => {
  const currentValue = get(value);
  return work(() => currentValue * currentValue);
});

const calcGraph = graph();
calcGraph.add(calcSquare);

await calcGraph.run();
calcGraph.get(calcSquare); // 4

value.set(4);

await calcGraph.run();
calcGraph.get(calcSquare); // 16

Features

Automatic Dependency Tracking

Dependencies are discovered automatically when tasks call get() (before work()). No manual wiring required.

const total = task((get, work) => {
  const aVal = get(a); // this could be a param
  const bVal = get(b); // ...or a task!
  const cVal = get(c);
  return work(() => aVal + bVal + cVal);
});

Memoization

Tasks cache their results by default. They only recompute when their dependencies change.

const expensive = task((get, work) => {
  const input = get(someParam);
  return work(() => heavyComputation(input));
}).cache("memo"); // Default behavior

const alwaysFresh = task((get, work) => {
  const input = get(someParam);
  return work(() => input);
}).cache("none"); // Always recompute

Cancellation Support

All tasks receive an abort signal for cooperative cancellation.

const fetchTask = task(async (get, work, ctx) => {
  return work(() => {
    const response = await fetch(url, { signal: ctx.signal });
    response.json()
  });
});

// Cancel a running graph
const controller = new AbortController();
const promise = g.run({ signal: controller.signal });
controller.abort();

Event System

Subscribe to graph events for monitoring, logging, or debugging.

// Subscribe to a group of events
const unsubTasks = g.on("task:*", (event) => console.log(event.type, event.taskId));

// Subscribe to a single event type
const unsubCacheHits = g.on("task:cacheHit", (event) => console.log("cache hit", event.taskId));

unsubTasks();
unsubCacheHits();

Next Steps

  • Param - Reactive parameter primitives
  • Task - Task definition and configuration
  • Graph - Execution engine and run options