Skip to content

v0.2.0

Choose a tag to compare

@miinhho miinhho released this 11 Sep 17:51
· 12 commits to main since this release
96569b2

@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 escapes
  • failure: "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:

  • ApplicationLifecycle
  • AppStarted, AppClosing, AppClosed
  • Scope and its DI/resource APIs
  • inject, scoped, currentScope, onStart, onDispose
  • EventBus, eventKey
  • setTracer, span
  • defer
  • begin
  • scheduleDeadline

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:

  • Job owns execution lifetime.
  • Supervisor applies admission and failure policy.
  • TaskGroup declares grouped work.
  • Context carries 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)