Minimal async task with reactive state
npm install @gantryland/taskMinimal async task with reactive state.
A Task instance is the identity: share the instance to share async state across modules, services, and UI.
- Small API (run, subscribe, cancel, reset)
- AbortSignal-aware execution
- Latest-request-wins behavior built in
- No runtime dependencies; works in browser and Node.js
``bash`
npm install @gantryland/task
`typescript
import { Task } from "@gantryland/task";
type User = { id: string; name: string };
const userTask = new Task
fetch(/api/users/${id}, { signal }).then((r) => r.json())
);
const unsubscribe = userTask.subscribe(({ data, error, isLoading, isStale }) => {
if (isStale || isLoading) return renderLoading();
if (error) return renderError(error);
renderUser(data);
});
await userTask.run("42");
unsubscribe();
`
- You need shared async state with deterministic transitions.
- You want cancellation and "latest run wins" semantics by default.
- You want a small primitive and compose caching/retry/scheduling separately.
- You only need a one-off Promise with no shared state.
- You need multi-emission streams (use observable tools).
- You want a full data-fetching framework with built-in policies.
`typescript`
type TaskState
data: T | undefined;
error: Error | undefined;
isLoading: boolean;
isStale: boolean;
};
- data: last successful valueerror
- : last failure (AbortError is not stored)isLoading
- : true while a run is activeisStale
- : true before first execution
Transition shape:
`text`
stale -> run() -> loading -> success | error
`typescript`
new Task
| Member | Purpose | Return |
| --- | --- | --- |
| getState() | Read current snapshot | TaskState |subscribe(listener)
| | Observe state changes (immediate first emit) | () => void |run(...args)
| | Execute the current TaskFn | Promise |cancel()
| | Abort in-flight run and clear loading | void |reset()
| | Restore initial stale state | void |
- Latest request wins; older completions are ignored.
- run() clears error, sets isLoading: true, and flips isStale: false.run()
- resolves undefined on error, abort, or superseded execution.data
- Failures keep previous and normalize non-Error throws.
- Listener errors are isolated (they do not break task state updates).
`typescript
import { Task, type TaskFn } from "@gantryland/task";
type Project = { id: string; name: string };
const fetchProject: TaskFn
fetch(/api/projects/${id}, { signal }).then((r) => r.json());
const projectTask = new Task(fetchProject);
const result = await projectTask.run("p_123");
if (result !== undefined) {
console.log(result.name);
}
`
`typescript
import { Task } from "@gantryland/task";
const task = new Task(async () => {
throw new Error("Request failed");
});
await task.run();
const { data, error } = task.getState();
if (error) {
report(error.message);
} else {
render(data);
}
`
`typescript
import { Task } from "@gantryland/task";
const task = new Task(async () => {
const response = await fetch("/api/profile");
if (!response.ok) throw new Error("Request failed");
return response.json() as Promise<{ id: string; name: string }>;
});
await task.run();
`
- @gantryland/task-react - Minimal React hooks for Task interop
- @gantryland/task-combinators - TaskFn composition and control-flow operators
- @gantryland/task-cache - Cache combinators and in-memory store
`bash``
npx vitest packages/task/test