Docs / Packages / @zudojs/runtime
v1.3.1

@zudojs/runtime

Application runtime orchestrator for the Zudojs framework. Manages the complete lifecycle from creation through shutdown with deterministic dependency ordering, rollback, signal handling, readiness tracking, and health checks.

RUNTIME LIFECYCLE STARTUP SHUTDOWN ORCHESTRATION

OVERVIEW

A real application is made of several pieces: a database connection, a queue, an HTTP server. They have to come up in the right order — the server should not accept traffic before the database is connected — and go down in the opposite order, so nothing is torn out from under something still using it.

A runtime is the thing that does that for you. You hand it your pieces, it works out the order from the dependencies you declared, starts them one by one, and when the process is asked to shut down it stops them in reverse.

Each piece you hand it is a module: a plain object with an id, a name, an optional list of module ids it depends on, and up to four hook functions the runtime calls at the right moments.

When you need it

  • Your process owns resources that must be opened before use and closed on exit.
  • One piece of your app must be up before another one starts.
  • You deploy somewhere that sends SIGTERM and expects a clean exit (Kubernetes, Docker, systemd).
  • You need a readiness endpoint that only says "yes" once your dependencies really answer.

When you don't

  • A script that runs top to bottom and exits. There is no lifecycle to manage.
  • A library. Libraries should not start processes; leave that to the application that uses them.
  • You already use createApplication() from @zudojs/core, which wires a runtime for you.

How this fits with core and lifecycle

@zudojs/core owns the Module contract and the application object. It defines what a module is; this package defines when each of its hooks is called. Reach for core when you are writing modules, and for this package when you are booting a process yourself.

@zudojs/lifecycle is the same idea one level down: it orders and starts arbitrary components, not Zudo modules, and knows nothing about runtime status, readiness or health. Use it to sequence a handful of objects inside a larger program; use this package to run the whole process.

INSTALLATION

Install the package together with the services it orchestrates. All of them ship at the same version.

$ npm install @zudojs/runtime @zudojs/core @zudojs/logger @zudojs/container @zudojs/events
Source of truth: These docs follow the framework source. If an export shown here is missing from the version you installed, update to the latest @zudojs release.

The package also pulls in @zudojs/errors, @zudojs/constants and @zudojs/config as regular dependencies, so you do not install those yourself unless you import from them directly. Node 24 or newer is required.

QUICK START

This is a complete program. It defines one module, builds a runtime around it, starts it and stops it.

import type { Module } from "@zudojs/core"; import { createLogger } from "@zudojs/logger"; import { createContainer } from "@zudojs/container"; import { createEventBus } from "@zudojs/events"; import { createRuntime } from "@zudojs/runtime"; const database: Module = { id: "database", name: "Database", async onInitialize() { console.log("database connected"); }, async onShutdown() { console.log("database closed"); }, }; const runtime = createRuntime( { modules: new Map([["database", database]]), logger: createLogger({ name: "my-app" }), container: createContainer(), eventBus: createEventBus(), }, { environment: "development", applicationName: "my-app", }, ); await runtime.start(); console.log(runtime.state, runtime.ready); // "running" true await runtime.stop(); console.log(runtime.state); // "stopped"

What you should see, in this order (interleaved with the runtime's own log lines):

database connected running true database closed stopped
Tip: The first argument is the services the runtime works with; the second is how the runtime itself should behave. environment and applicationName are the only required options.

RUNTIME STATES

A lifecycle state is one word describing where the runtime is right now: not started yet, coming up, up, going down, down, or broken. Read it from runtime.state.

type RuntimeState = | "created" // built, nothing has run yet | "initializing" // onInitialize hooks are running | "initialized" // every module has initialized | "starting" // onReady hooks are running | "running" // everything is up | "stopping" // modules are being torn down | "stopped" // everything is down | "failed"; // something went wrong on the way up or down

The runtime only moves between states along fixed paths. Ask for an illegal move and you get a RuntimeStateError instead of a half-started process.

FromCan move to
createdinitializing, stopped, failed
initializinginitialized, failed
initializedstarting, failed
startingrunning, failed
runningstopping, failed
stoppingstopped, failed
stoppednothing — this is the end
failedstopping, stopped
Changed in v1.3.0: start() passes through every state in order: created → initializing → initialized → starting → running. onInitialize hooks see initializing and onReady hooks see starting, and the runtime publishes runtime.initialized and runtime.starting between the two phases. RUNTIME_STATE_TRANSITIONS no longer allows initializing → running or initialized → running. In 1.2.1 the two middle states were declared but never entered, and onReady ran while the state still read initializing.
In plain words: failed is not the end. A failed start only rolls back the modules that actually started, so you can still call stop() to release everything else. Since v1.3.0 the state helpers agree: TERMINAL_STATES is ["stopped"] and isTerminalState("failed") is false.

The same rules are available as functions, if you want to check a state without touching a runtime.

import { canTransition, isTerminalState, isRunning, hasFailed, } from "@zudojs/runtime"; console.log(canTransition("created", "initializing")); // true console.log(canTransition("stopped", "running")); // false console.log(isTerminalState("stopped")); // true console.log(isTerminalState("failed")); // false — stop() still works from "failed" console.log(isRunning("running")); // true console.log(hasFailed("failed")); // true

For more than the bare state, read runtime.status. It is recomputed every time you read it, so it never goes stale.

interface RuntimeStatus { readonly state: RuntimeState; readonly ready: boolean; readonly running: boolean; readonly startedAt?: Date; readonly stoppedAt?: Date; readonly failedAt?: Date; readonly error?: RuntimeError; /** Modules that failed to stop or destroy during the last shutdown. */ readonly shutdownFailures?: readonly RuntimeShutdownFailure[]; }
Watch out: state === "stopped" with a non-empty shutdownFailures means the runtime finished shutting down but some modules did not release their resources. Log that list; do not treat it as a clean exit.

MODULES AND ORDER

A module declares what it needs through dependencies — a list of other module ids. The runtime sorts modules so that a module's dependencies are always brought up first, and torn down last.

Each module can implement any of four hooks. The runtime calls them with a ModuleContext, and skips any hook a module does not define.

HookWhen it runsOrder
onInitializeBringing the module up: open connections, read config.Dependencies first
onReadyAfter every module has initialized: start accepting work. runtime.state is starting here (it was still initializing before v1.3.0).Dependencies first
onShutdownShutting down: stop accepting work, drain.Reverse
onDestroyLast call: release everything the module still holds.Reverse

This example has three modules in a chain. Watch the printed order.

import type { Module } from "@zudojs/core"; import { createLogger } from "@zudojs/logger"; import { createContainer } from "@zudojs/container"; import { createEventBus } from "@zudojs/events"; import { createRuntime } from "@zudojs/runtime"; function noisyModule(id: string, dependencies: string[]): Module { return { id, name: id, dependencies, async onInitialize() { console.log(`up: ${id}`); }, async onShutdown() { console.log(`down: ${id}`); }, }; } const modules = new Map([ ["api", noisyModule("api", ["cache"])], ["cache", noisyModule("cache", ["database"])], ["database", noisyModule("database", [])], ]); const runtime = createRuntime( { modules, logger: createLogger({ name: "ordering" }), container: createContainer(), eventBus: createEventBus(), }, { environment: "development", applicationName: "ordering" }, ); await runtime.start(); await runtime.stop();

Even though the map lists api first, this prints:

up: database up: cache up: api down: api down: cache down: database

You can compute that order yourself, without a runtime. The dependency functions take a map of module id to its dependency ids.

import { resolveDependencies } from "@zudojs/runtime"; const graph = new Map([ ["database", []], ["cache", ["database"]], ["api", ["cache"]], ]); console.log(resolveDependencies(graph).order); // [ 'database', 'cache', 'api' ] // Modules at the same depth have no dependency on each other, // so they could be started together. console.log(resolveDependencies(graph).parallelGroups); // [ [ 'database' ], [ 'cache' ], [ 'api' ] ]
Danger: Naming a dependency that is not in the map throws RuntimeDependencyError, and a dependency cycle throws RuntimeCircularDependencyError. Both surface at startup, not later at runtime.

STARTUP AND SHUTDOWN

start() initializes every module in order, then readies them. If any module throws, the runtime rolls back: modules that already started are stopped and destroyed in reverse, and the state becomes failed.

start() does not re-throw the error your module threw. It rejects with a RuntimeStartError whose cause is your original error, whose phase is "initialize" (an onInitialize threw) or "start" (an onReady threw), and whose failedModuleId names the module. So error instanceof MyDbError is always false in the catch block; test error.cause instead. Since v1.3.0 two subclasses narrow it down, and instanceof RuntimeStartError still matches both:

  • RuntimeInitializationError (phase: "initialize") when an onInitialize hook fails, or when the configuration manager fails to load (then with no failedModuleId).
  • RuntimeRollbackError when startup fails and the rollback that follows also fails. phase, failedModuleId and cause describe the startup failure, originalError holds the error start() would otherwise have thrown, rollbackError what failed during rollback (an AggregateError when several modules failed), and the message names both, for example Module "api" failed during initialization. Rollback also failed: close failed. Before v1.3.0 a rollback failure was only logged.

A startup that outlives startupTimeout rejects with RuntimeTimeoutError instead, which has no cause.

stop() goes the other way, and is safe to call more than once — stopping an already-stopped runtime does nothing.

Handling a failed start, and getting at the original error:

import { RuntimeStartError, RuntimeTimeoutError } from "@zudojs/runtime"; try { await runtime.start(); } catch (error) { if (error instanceof RuntimeStartError) { // error.cause is the error your module threw. console.error( `module ${error.failedModuleId} failed in ${error.phase}:`, error.cause, ); } else if (error instanceof RuntimeTimeoutError) { console.error(`${error.operation} timed out after ${error.timeoutMs}ms`); } else { console.error("startup failed:", error); } // Release whatever startup did not roll back, then exit non-zero. await runtime.stop(); await container.dispose(); // unless you passed disposeContainerOnStop: true process.exit(1); }

With a module whose onInitialize throws new Error("connect ECONNREFUSED"), this prints module database failed in initialize: Error: connect ECONNREFUSED.

Who disposes the container

The container is passed in, so by default the caller owns it: stop() calls every module's onShutdown and onDestroy but leaves the container alone, and any SINGLETON it created (a pool, a file handle, a client with open sockets) is still open after runtime.stop() resolves. Since v1.3.0 you can hand ownership to the runtime with disposeContainerOnStop: true: stop() then disposes the container after every module has shut down and been destroyed, including a signal-driven stop and stop() on a runtime that never started. A disposal failure is recorded in status.shutdownFailures under the id "(container)" (exported as CONTAINER_SHUTDOWN_ID) and does not fail the stop.

const runtime = createRuntime( { modules, logger, container, eventBus }, { environment: "production", applicationName: "my-api", disposeContainerOnStop: true }, ); await runtime.start(); await runtime.stop(); // modules shut down, then each singleton's dispose() runs console.log(container.isDisposed()); // true

Without the option, dispose the container yourself after the runtime has stopped (modules may still resolve services while they shut down, and a disposed container throws on every use): await runtime.stop(); await container.dispose();.

The fatal-error path (uncaughtException, unhandledRejection) calls process.exit(1) as soon as the stop settles, so an asynchronous dispose() you start yourself from a runtime.stopped handler may not finish there. disposeContainerOnStop: true avoids that, because the disposal is part of stop() itself.

Two options bound how long either direction may take. A module whose hook never settles no longer hangs the process forever.

OptionDefaultWhat it bounds
startupTimeout60000The whole startup sequence. On expiry, startup fails with a timeout error and rolls back; no further module hook starts, and a module whose hook finishes late is shut down and destroyed.
shutdownTimeout30000The whole shutdown sequence. On expiry stop() throws RuntimeStopError and the state becomes failed.
In plain words: pick a shutdown timeout slightly shorter than your platform's grace period. Kubernetes sends SIGTERM and then SIGKILL 30 seconds later by default, so a 30-second shutdown timeout leaves no room.

READINESS AND HEALTH

Liveness asks: is this process still worth keeping alive? If the answer is no, the right fix is to restart it.

Readiness asks something narrower: should this process be sent traffic right now? A process can be perfectly alive and still not ready — it is starting up, or its database went away for a minute. Restarting it would not help; you just stop sending it requests until it recovers.

A readiness check is a named function returning a boolean (or a promise of one). Register as many as you have dependencies worth checking.

Registering a check and re-running it:

let databaseConnected = false; runtime.registerReadinessCheck("database", () => databaseConnected); await runtime.runReadinessChecks(); console.log(runtime.ready); // false console.log(runtime.health.state); // "degraded" databaseConnected = true; await runtime.runReadinessChecks(); console.log(runtime.ready); // true console.log(runtime.health.state); // "healthy" console.log(runtime.readiness.checks.get("database")?.durationMs); // e.g. 0
Watch out: registering a check does not run it. A new check starts out failing, so registering one on a running runtime makes it not-ready until runReadinessChecks() passes it. That is deliberate — a check nobody has evaluated is not evidence of anything.

runtime.health is derived, never stored. While the runtime is running it is healthy if every check passes and degraded if any fails; every other lifecycle state maps straight onto a health state.

Lifecycle statehealth.state
created, stoppedunknown
initializing, initialized, startingstarting
runninghealthy or degraded, from the checks
stoppingstopping
failedunhealthy

Wiring the two ideas to two probe endpoints, as a fragment inside your HTTP handler:

// Liveness: restart me only if I am broken beyond recovery. const liveStatus = runtime.health.state === "unhealthy" ? 503 : 200; // Readiness: send me traffic only when every check passes. await runtime.runReadinessChecks(); const readyStatus = runtime.ready ? 200 : 503;

A check that throws counts as failing, and its message is recorded on the check. A check that does not settle within 5 seconds is recorded as failed too, so one hanging probe cannot wedge your readiness endpoint.

SIGNALS

When your platform wants your process gone, it sends a signal — a small message from the operating system. The runtime listens for these and calls stop() for you, so you rarely write signal code yourself.

Signal or eventWhat the runtime does
SIGTERMGraceful shutdown. The process stays alive until every onShutdown has finished, then exits on its own: code 0 when the shutdown succeeded, code 1 when it failed. A signal during startup waits for startup to settle, then stops. A second SIGTERM during shutdown exits immediately.
SIGINTSame as SIGTERM. This is Ctrl-C.
uncaughtExceptionLogs the error, shuts down, then exits with code 1.
unhandledRejectionSame as an uncaught exception.

Handlers are attached before modules start, so a signal arriving mid-boot is still honoured. Turn them off — in tests, or when your host owns process signals — with two options:

const runtime = createRuntime(dependencies, { environment: "test", applicationName: "my-app", handleSignals: false, handleFatalErrors: false, });
Watch out: do not add your own process.on("SIGTERM", ...) that also calls stop() while handleSignals is on. You get two shutdowns racing; the second signal is then read as "exit now".

How a signal-driven shutdown ends

With handleSignals on, the signal handler holds the event loop open for the whole graceful shutdown, so onShutdown runs to the end even under plain node, after your server has closed and nothing else keeps the process alive. The runtime never calls process.exit() here. It sets process.exitCode and lets the process exit on its own:

What happened during shutdownFinal stateExit codeEvents and log
Every hook finishedstopped0 (left alone)runtime.stopped
An onShutdown threwstopped1runtime.module.failed, then runtime.stopped; logs Shutdown handler failed. with a RuntimeSignalError
The shutdown outlived shutdownTimeoutfailed1runtime.failed (phase: "stop") once; logs Shutdown handler failed. with a RuntimeSignalError

Tested under plain node with a module whose onShutdown clears the last interval and then waits on an unref'd 300 ms timer, and shutdownTimeout: 500:

clean shutdown → database closed, runtime.stopped, exit code 0, state "stopped" onShutdown throws → exit code 1, state "stopped", runtime.failed not published onShutdown never ends → runtime.failed stop (once), exit code 1, state "failed"

An orchestrator such as Kubernetes or systemd sees the non-zero exit without any code of yours. You no longer need to set process.exitCode from a runtime.failed handler; subscribe to runtime.failed and runtime.module.failed only to report the failure.

Changed in v1.3.1: in v1.3.0 a SIGTERM/SIGINT under plain node could end the process at once with code 0, with the runtime still running or stopping and onShutdown unfinished, whenever nothing else kept the event loop alive (tsx hid this by keeping the loop alive). A failed signal-driven shutdown was only logged, as Shutdown failed., and the exit code stayed 0, so this page advised setting process.exitCode = 1 in a runtime.failed handler; that workaround can go. A failed stop also published runtime.failed twice, and a module failure during startup did too; each now publishes it once.

EVENTS

The runtime announces what it is doing on the event bus you passed in. Subscribe if you want to log boot progress, export metrics, or react to a health change without polling.

Listening for health changes:

import { createEventBus } from "@zudojs/events"; import type { RuntimeHealthEventPayload } from "@zudojs/runtime"; const eventBus = createEventBus(); eventBus.on("runtime.health.changed", (event) => { const payload = event.payload as RuntimeHealthEventPayload; console.log(`health: ${payload.previousState} -> ${payload.currentState}`); }); // Pass this same eventBus in the runtime's dependencies. // A clean boot prints: health: unknown -> starting, then starting -> healthy

Eighteen event types are emitted, in four families:

  • Runtimeruntime.initializing, .initialized, .starting, .running, .stopping, .stopped, .failed. .initialized and .starting are published since v1.3.0, and since v1.3.1 the RuntimeEventType union includes them too. There is no runtime.created event.
  • Moduleruntime.module.initializing, .initialized, .starting, .started, .stopping, .stopped, .failed
  • Shutdownruntime.shutdown.drain, runtime.shutdown.complete
  • Derived stateruntime.health.changed, runtime.readiness.changed

Every payload carries runtimeId, timestamp and state. Module events add moduleId and moduleName; failure events add error and phase. Set emitEvents: false to silence all of them.

RUNTIME CONTEXT

The context is one object holding everything about the running application: who it is, how it is doing, and the shared services. Read it from runtime.context. Like status, it is rebuilt on every read, so the values are always current.

interface RuntimeContext { readonly runtimeId: RuntimeId; readonly environment: Environment; readonly applicationName: string; readonly applicationVersion: string; readonly metadata: Readonly<Record<string, unknown>>; readonly state: RuntimeState; readonly status: RuntimeStatus; readonly startedAt?: Date; readonly logger: Logger; readonly container: Container; readonly eventBus: EventBus; readonly health: RuntimeHealth; readonly ready: boolean; }

Anything you put in metadata at creation shows up here — handy for a region, a build number, or a pod name.

const runtime = createRuntime(dependencies, { environment: "production", applicationName: "my-api", applicationVersion: "1.4.0", metadata: { region: "eu-west-1" }, }); console.log(runtime.context.metadata.region); // "eu-west-1" console.log(runtime.context.runtimeId); // "rt_9f2c..." (generated)

Modules get a different object, a ModuleContext from @zudojs/core, giving them a logger, the container, and configuration. Configuration is backed by a manager you supply; without one, modules see an empty configuration rather than a crash.

import { createConfigurationManager } from "@zudojs/core"; const runtime = createRuntime( { modules, logger, container, eventBus, configuration: createConfigurationManager(), }, { environment: "production", applicationName: "my-api" }, );
Note: context.application is only available if you pass an application in the same dependencies object. Reading it without one throws a clear error instead of handing back an empty stub.

MULTIPLE RUNTIMES

One process sometimes runs more than one thing — an API and a background worker, say. RuntimeRegistry keeps them together so you can start and stop them as a unit. You register each one under an id of your choosing.

import { RuntimeRegistry } from "@zudojs/runtime"; const registry = new RuntimeRegistry(); registry.register("api", apiRuntime); registry.register("worker", workerRuntime); await registry.startAll(); console.log(registry.isAllReady()); // true console.log(registry.getStatus()); // { api: { state: 'running', ready: true }, // worker: { state: 'running', ready: true } } await registry.stopAll();

Runtimes start in registration order and stop in reverse. If one fails to start, the ones already started are stopped again before the error reaches you, so you never keep a half-booted process.

Watch out: unregister(id) only drops the reference and returns whether there was one — it does not stop the runtime. Use removeAndStop(id) when you want both.

TESTING

The test helpers live behind their own entry point, @zudojs/runtime/testing, so a test-only module never ends up in your production bundle. They are not exported from the package root.

withTestRuntime builds a runtime with signals off, starts it, runs your callback, and stops it — even if the callback throws.

import { describe, it, expect } from "vitest"; import { withTestRuntime, createMockModule, } from "@zudojs/runtime/testing"; describe("my app", () => { it("starts and stops cleanly", async () => { const users = createMockModule("users"); await withTestRuntime(async (runtime) => { expect(runtime.state).toBe("running"); expect(runtime.ready).toBe(true); }, [users]); expect(users.calls.onInitialize).toBe(1); expect(users.callOrder).toEqual([ "onInitialize", "onReady", "onShutdown", "onDestroy", ]); }); });

When you need the runtime object itself, build one with createTestRuntime and drive it by hand. It creates its own container and, since v1.3.0, disposes it on stop() (disposeContainerOnStop: true); pass disposeContainerOnStop: false to keep it.

import { createTestRuntime, createMockModule } from "@zudojs/runtime/testing"; const runtime = createTestRuntime([createMockModule("users")], { startupTimeout: 1_000, }); await runtime.start(); await runtime.stop();
Note: createMockModule records its own calls in calls and callOrder. They are plain counters, not vi.fn() spies, so do not call toHaveBeenCalled() on them.

API REFERENCE

The Runtime object

MemberWhat it doesNotes
start()Brings every module up in dependency order.Rolls back and throws RuntimeStartError (original on cause; RuntimeInitializationError or RuntimeRollbackError where they apply) on failure. Calling it twice is a no-op.
stop()Tears every module down in reverse order.Works from failed; safe to call repeatedly. Disposes the container only with disposeContainerOnStop: true.
stateThe current lifecycle state.One RuntimeState string.
statusState plus timestamps, error and shutdown failures.Recomputed on every read.
contextIdentity, services and live state in one object.Recomputed on every read.
healthDerived health, with per-check results.Always unknown when trackHealth is off.
readyWhether every readiness check passes.Boolean.
readinessReadiness state plus a map of check results.Each result carries ready, message, durationMs.
registerReadinessCheck(name, check)Adds a named check.Starts out failing until first run.
removeReadinessCheck(name)Removes a check.Returns whether one existed.
runReadinessChecks()Re-evaluates every check.Each check is bounded by a 5-second timeout.

Options

OptionDefaultWhat it does
environmentrequired"development" | "test" | "staging" | "production".
applicationNamerequiredName used in logs and events.
applicationVersion"0.1.0"Version reported on the context.
runtimeIdgeneratedIdentifier for this runtime instance.
handleSignalstrueShut down on SIGTERM and SIGINT.
handleFatalErrorstrueShut down and exit 1 on an uncaught error.
exitOnFatalErrortrueExit 1 after a fatal-error shutdown.
fatalExitTimeout10000Exit anyway if that shutdown hangs.
forceExitOnSecondSignaltrueA second SIGTERM/SIGINT exits immediately.
startupTimeout60000Milliseconds allowed for startup.
shutdownTimeout30000Milliseconds allowed for shutdown.
emitEventstruePublish lifecycle events on the bus.
trackReadinesstrueMark ready automatically once all checks pass.
trackHealthtrueDerive health and emit health events.
readinessCheckTimeout5000Milliseconds one readiness check may take before it counts as failed.
parallelInitializationfalseInitialize modules at the same dependency depth together instead of one by one.
disposeContainerOnStopfalseDispose the container after every module has shut down. New in v1.3.0; createTestRuntime sets it to true.
metadata{}Free-form values surfaced on the context.

Functions

NameWhat it doesNotes
createRuntime(deps, options)Builds a runtime.The normal entry point.
resolveDependencies(map)Returns order and parallelGroups.Throws on a missing dependency or a cycle.
buildDependencyGraph(map)Returns nodes, order and any cycles.Does not throw on a cycle; it reports one.
validateDependencies(map)Throws if a module names a dependency that is not in the map.Returns nothing; resolveDependencies calls it for you.
computeRuntimeHealth(state, readiness)Derives health from a state and check results.What runtime.health uses.
createRuntimeOptions(options)Applies defaults, then validates.resolveRuntimeOptions and validateRuntimeOptions do each half.
createRuntimeContext(deps)Builds a context object.The runtime does this for you.
createRuntimeIdentity(id, env, name, version)Adds hostname and process id to the identity.Useful in log metadata.
createRuntimeId()Generates an rt_… identifier.createCorrelationId and createRequestId match it.
createStatus(state)Builds a bare status from a state.No timestamps.
canTransition(from, to)Whether a state move is legal.assertTransition throws instead.
isTerminalState / isRunning / hasFailed / canStart / canStopOne-line state questions.All take a RuntimeState.
executeStartup / rollbackStartup / executeShutdownThe sequences the runtime runs internally.Only needed if you build your own runtime.
createRuntimeEventPayload and friendsBuild event payloads.Module, failure, health and readiness variants exist.
publishRuntimeEvent(bus, logger, event)Publishes an event without letting a subscriber break the caller.Used by the runtime.

Classes

NameWhat it doesNotes
DefaultRuntimeThe runtime implementation.Prefer createRuntime().
RuntimeRegistryHolds several runtimes in one process.register(id, runtime), startAll, stopAll, removeAndStop.
ReadinessTrackerStores and evaluates readiness checks.Usable on its own: registerCheck, runChecks, updateCheck, removeCheck, getState.
LifecycleManagerCalls module hooks in order.initialize, start, stop, destroy, rollback.
SignalHandlerAttaches and removes process handlers.The runtime owns one already.

Errors

NameThrown whenNotes
RuntimeStateErrorAn illegal state move is attempted.E.g. starting a stopped runtime.
RuntimeStartErrorA module hook, or the configuration manager, fails during startup.Carries phase ("initialize" or "start") and failedModuleId; the error your module threw is on cause.
RuntimeStopErrorShutdown fails.Carries phase.
RuntimeInitializationErrorAn onInitialize hook, or the configuration manager, fails during startup.Extends RuntimeStartError with phase: "initialize"; accepts a failedModuleId. Thrown since v1.3.0.
RuntimeTimeoutErrorStartup, shutdown or a check runs too long.Carries operation and timeoutMs.
RuntimeRollbackErrorStartup fails and the rollback that follows also fails.Extends RuntimeStartError. originalError is the startup error, rollbackError what failed during rollback (an AggregateError for several). Thrown since v1.3.0.
RuntimeCircularDependencyErrorModules depend on each other in a loop.Carries the cycle.
RuntimeDependencyErrorA module depends on an unregistered id.Carries moduleId and dependencyId.
RuntimeSignalErrorLogged by SignalHandler when the shutdown it triggered rejects; never thrown, since a signal handler has no caller.Carries signal and, since v1.3.0, an optional cause. See the note under Signals: a runtime from createRuntime catches its own stop failure first and logs Shutdown failed. instead.
toRuntimeError(error, phase)Wraps anything thrown into a RuntimeError.Passes an existing one through unchanged.

Constants and types

NameWhat it isNotes
DEFAULT_RUNTIME_OPTIONSThe default option values.Frozen object.
RUNTIME_STATE_TRANSITIONSThe legal moves per state.Backs canTransition.
TERMINAL_STATES / STARTABLE_STATES / STOPPABLE_STATESState groupings.Frozen arrays. TERMINAL_STATES is ["stopped"] since v1.3.0.
CONTAINER_SHUTDOWN_ID"(container)"The shutdownFailures id under which a failed container disposal is recorded.
Runtime, RuntimeDependenciesThe runtime interface and its inputs.Type-only exports.
RuntimeOptions, ResolvedRuntimeOptionsOptions before and after defaults.Type-only.
RuntimeState, RuntimeStatus, RuntimeHealth, RuntimeHealthStateState and health shapes.Type-only.
ReadinessCheckFn, ReadinessCheck, ReadinessTrackerStateReadiness shapes.Type-only.
LifecyclePhase, LifecycleResult, LifecycleFailurePer-phase results from the lifecycle manager.Type-only.
RuntimeEventType, RuntimeModuleEventType, RuntimeEventMap and payload typesEvent names and payload shapes.Type-only. Since v1.3.1 RuntimeEventType is keyof RuntimeEventMap (all 18 names, including runtime.initialized and runtime.starting), and RuntimeModuleEventType is its runtime.module.* subset.

COMMON MISTAKES

  • Importing test helpers from the package root. import { withTestRuntime } from "@zudojs/runtime" fails — nothing by that name is exported there. Import from @zudojs/runtime/testing instead.
  • Registering a runtime without an id. registry.register(apiRuntime) does not type-check. The signature is register(id, runtime), and registering the same id twice throws.
  • Expecting a new readiness check to pass. It is failing until runReadinessChecks() runs it, so runtime.ready flips to false the moment you register one. Run the checks before reading readiness.
  • Treating failed as the end. Startup rollback shuts down started modules and destroys every module whose onInitialize ran (including one that threw); a module that finishes after a startup timeout is torn down when it settles, and stop() waits for that. Call stop() on a failed runtime to release it fully.
  • Caching runtime.status in a variable and reading it later. Each read builds a fresh snapshot; the one you held onto is frozen in the past. Read runtime.status at the moment you need it.
  • Ignoring status.shutdownFailures. The state reaches stopped either way. Only that list tells you a module failed to release its resources.
  • Checking the startup error's type directly. start() wraps a module's failure in RuntimeStartError, so error instanceof YourError is false. Look at error.cause.
  • Assuming stop() closes container services. By default it does not call container.dispose(). Pass disposeContainerOnStop: true, or call it yourself after the runtime has stopped.
  • Checking state === "initializing" inside onReady. Since v1.3.0 onReady runs in starting. Test for the state you actually mean, or subscribe to runtime.starting / runtime.running.

COMPLETE EXPORT INDEX

Every name @zudojs/runtime exports from its package root at v1.2.1 — 90 in total, generated from the package’s own entry point rather than written by hand. The sections above explain the ones you reach for most; this is the exhaustive list, so nothing shipped is undocumented. Names not covered above are typically internal helpers and supporting types.

Show all 90 exports
Classes (14)
DefaultRuntime LifecycleManager ReadinessTracker RuntimeCircularDependencyError RuntimeDependencyError RuntimeInitializationError RuntimeRegistry RuntimeRollbackError RuntimeSignalError RuntimeStartError RuntimeStateError RuntimeStopError RuntimeTimeoutError SignalHandler
Functions (31)
assertTransition buildDependencyGraph canStart canStop canTransition computeRuntimeHealth createCorrelationId createFailureEventPayload createHealthEventPayload createModuleEventPayload createReadinessEventPayload createRequestId createRuntime createRuntimeContext createRuntimeEventPayload createRuntimeId createRuntimeOptions createStatus executeShutdown executeStartup hasFailed isRunning isTerminalState publishRuntimeEvent resolveDependencies resolveRuntimeOptions rollbackStartup toRuntimeError validateDependencies validateRuntimeOptions withRuntimeContextState
Interfaces (30)
CircularDependencyInfo DependencyGraph DependencyNode DependencyResolutionResult LifecycleContext LifecycleFailure LifecycleManagerOptions LifecycleResult ManagedModule ModuleContextServices ReadinessCheck ReadinessOptions ReadinessTrackerState ResolvedRuntimeOptions Runtime RuntimeContext RuntimeContextDependencies RuntimeContextState RuntimeDependencies RuntimeEventMap RuntimeEventPayload RuntimeFailureEventPayload RuntimeHealth RuntimeHealthCheck RuntimeHealthEventPayload RuntimeModuleEventPayload RuntimeOptions RuntimeReadinessEventPayload RuntimeShutdownFailure RuntimeStatus
Type aliases (10)
LifecyclePhase ModuleEventListener ReadinessCheckFn ReadinessState RuntimeEventType RuntimeFailureState RuntimeHealthState RuntimeId RuntimeModuleEventType RuntimeState
Constants (5)
DEFAULT_RUNTIME_OPTIONS RUNTIME_STATE_TRANSITIONS STARTABLE_STATES STOPPABLE_STATES TERMINAL_STATES