Repository navigation
v0.2.0
@tiberjs/runner:0.2.0 is a clean redesign of the runtime around one lifetime primitive: Job.
A Job is now both an execution and the owner of its actual descendants. Supervision, grouped execution, cancellation, deadlines, and immutable context are composed around that ownership model without hidden scopes or lifecycle managers.
Important
This is a breaking release. It intentionally removes the 0.1 compatibility surface rather than retaining aliases or adapters.
Highlights
Job-owned structured concurrency
Job<T> replaces the split between executions, tasks, task groups, and manually managed runtime state.
import { Job, execute, fork } from "@tiberjs/runner";
const job = new Job(() => 42).start();
console.log(await job); // 42
const result = await execute(async () => {
const first = fork(() => "first");
const second = fork(() => "second");
return [await first, await second];
});
A Job:
- is cold and single-use
- captures ownership and context when started, not when constructed
- settles only after its body and all actual descendants finish
- owns cancellation for its complete subtree
- preserves the original success value, cancellation reason, or failure
- supports await using through
AsyncDisposable
await job and job.join() unwrap the result. job.result() observes success or failure as a value without throwing execution errors.
Explicit ownership
Jobs form a real ownership tree.
const child = new Job(handler).start({ parent });
Inside an execution, fork() and new Job(...).start() create children of the current Job. Pass { parent: undefined } to explicitly start an independent root.
There are no synthetic owners between a Job and its descendants.
Explicit supervision
Supervisor manages an ordinary Job supplied by the application. It does not create a hidden root, startup phase, or shutdown scope.
import { addAbortListener } from "node:events";
import { Job, Supervisor, signal } from "@tiberjs/runner";
const application = new Job(async () => {
const stopped = Promise.withResolvers<void>();
using registration = addAbortListener(signal(), stopped.resolve);
await stopped.promise;
});
await using supervisor = new Supervisor(application, {
failure: "isolate",
});
supervisor.start({ parent: undefined });
const value = await supervisor.run(() => 42);
Supervisor submissions become direct children of the managed Job.
failure: "isolate"keeps an independent child failure from cancelling the owner or its siblings.failure: "fail-fast"propagates child failures to the owner.flush()joins current children without closing the owner.close()delegates cancellation and completion to the managed Job.
Initialization order, readiness, logging, retries, and error reporting remain application responsibilities.
Declarative TaskGroup
TaskGroup is now an immutable declaration rather than a lifetime owner.
const group = new TaskGroup([
new TaskGroup([new Job(() => "A"), new Job(() => "B")]),
new Job(() => "C"),
]);
const result = await supervisor.run(group);
// [["A", "B"], "C"]
Nested declarations preserve their result shape without creating intermediate Jobs.
Each group selects either:
failure: "fail-fast"— cancel sibling leaves when a genuine failure escapesfailure: "isolate"— contain that failure at the group boundary
The complete declaration is validated before any member starts.
Context is separate from ownership
Immutable context frames describe an execution environment; they do not create lifetime nodes.
import { contextKey, execute, provide, use, withContext } from "@tiberjs/runner";
const Tenant = contextKey<string>("tenant");
await execute({ values: [provide(Tenant, "outer")] }, async () => {
await withContext([provide(Tenant, "inner")], async () => {
console.log(use(Tenant)); // "inner"
});
console.log(use(Tenant)); // "outer"
});
New context APIs include:
withContext()hasContext()requireContext()MissingContextError
Job seeds can supply context values, an external signal, an absolute deadline, and an opaque attachment.
Stronger cancellation and failure semantics
Cancellation and deadlines now belong to the Job that owns the affected subtree.
- parent cancellation reaches every actual descendant
- external abort signals remain linked until the Job drains
- deadlines cover the complete subtree, including asynchronous finalization
- cancellation does not hide an independent execution or cleanup failure
- original error identity and cause are preserved
- independent failures are represented with AggregateError
Catching an ordinary child Job failure does not erase its ownership effect. Use execute(), timeout(), Supervisor isolation, or an isolated TaskGroup when a failure must be contained.
Breaking changes
Node.js 24 is required
The minimum supported Node.js version is now 24.
Removed lifecycle and infrastructure APIs
Runner now contains only transport-independent execution ownership, supervision, grouping, cancellation, deadlines, and context.
The following responsibilities have been removed from the package:
- dependency injection and hierarchical resource scopes
- application startup and shutdown orchestration
- event delivery
- tracing
- implicit cleanup registration
Removed APIs include:
ApplicationLifecycleAppStarted,AppClosing,AppClosed- Scope and its DI/resource APIs
inject,scoped,currentScope,onStart,onDisposeEventBus,eventKeysetTracer,spandeferbeginscheduleDeadline
These concerns can now be composed independently at the application boundary instead of being coupled to every execution.
Use native try/finally, using, and await using for lexical resource ownership.
fork() returns Job
fork() now creates and returns a real child Job<T>. The previous Task<T> type and observation-transfer semantics have been removed.
TaskGroup no longer owns runtime state
The old execution-owned TaskGroup and forkGroup() APIs have been removed.
Create cold Jobs, place them in a declarativeTaskGroup, and submit the group through a running Supervisor.
RuntimeState changed
RuntimeState is now:
interface RuntimeState {
readonly job: Job<unknown>;
readonly context: ExecutionContext;
}
Task, scope, and attachment fields no longer exist at the top level. Attachments are available through:
currentState().context.attachment;
currentAttachment<T>();
Do not manually recreate the old begin() lifecycle. Use an explicit Job for executions whose lifetime must outlive the initial call chain.
Why this release
The 0.1 runtime combined execution ownership, context, dependency injection, resources, events, tracing, and application lifecycle in one package.
0.2 establishes a smaller boundary:
Jobowns execution lifetime.Supervisorapplies admission and failure policy.TaskGroupdeclares grouped work.Contextcarries immutable execution values.- Applications and optional packages own infrastructure concerns.
This removes duplicated lifecycle state and makes ownership, cancellation, failure propagation, and shutdown behavior directly observable from the Job tree.
Full changelog: v0.1.1...v0.2.0 (v0.1.1...v0.2.0)