@zudojs/lifecycle
Application and component lifecycle orchestration — state machine, dependency ordering, graceful shutdown, signal handling, retry with backoff, rollback, and execution plans.
INSTALLATION
WHAT IT DOES
@zudojs/lifecycle is the lifecycle orchestration engine for Zudojs. It provides:
- → A LifecycleStateMachine that validates and tracks state transitions (IDLE → INITIALIZING → INITIALIZED → STARTING → STARTED → READY → STOPPING → STOPPED → DISPOSED)
- → Component lifecycle hooks — initialize, start, ready, stop, dispose — that components implement as needed
- → Dependency ordering via topological sort and a directed acyclic graph
- → Graceful shutdown with reverse-ordered teardown and configurable timeouts
- → Signal handling — automatic SIGINT/SIGTERM interception to trigger shutdown
- → A LifecycleEventEmitter that emits 16 typed events for observability integration
- → Retry with backoff — configurable exponential or fixed retry for component operations
- → Concurrency control — parallel execution of independent components at the same priority, with configurable limits
- → Rollback support — components that fail during startup can be disposed in reverse order
- → Execution plans — build ordered startup/shutdown stages from the dependency graph
WHERE IT SITS
The lifecycle manager sits between the application layer and the runtime. Modules register themselves as components. The manager resolves their dependency order, executes their hooks during startup and shutdown, and integrates with the runtime for process-level coordination.
DEPENDENCIES
| Package | Version | Purpose |
|---|---|---|
| @zudojs/errors | 1.2.0 | Error hierarchy (LifecycleError, LifecycleStateError, LifecycleTimeoutError, etc.) |
| @zudojs/constants | 1.1.1 | LifecycleState, LifecyclePhase enums, valid transitions, and default values |
workspace:*, always — including on main. They are never hand-pinned to an exact version. At publish time pnpm rewrites each workspace:* to the exact version of that package in the same release, so a published tarball carries real ranges. Releases go out through publish-all.sh, which runs pnpm -r publish — it rewrites the ranges and publishes in dependency order. Plain npm publish does not understand the workspace: protocol and would ship a literal workspace:* to the registry.
LIFECYCLE STATES
The LifecycleState enum defines every state a component or application can be in. The state machine enforces valid transitions between them.
Enum: LifecycleState
Valid Transitions
| From | To (allowed) |
|---|---|
| IDLE | INITIALIZING, DISPOSED |
| INITIALIZING | INITIALIZED, FAILED |
| INITIALIZED | STARTING, STOPPING, DISPOSED |
| STARTING | STARTED, FAILED |
| STARTED | READY, STOPPING, FAILED |
| READY | STOPPING, FAILED |
| STOPPING | STOPPED, FAILED |
| STOPPED | DISPOSED |
| FAILED | STOPPING, DISPOSED |
| DISPOSED | — (terminal) |
Startup Path
Shutdown Path
LIFECYCLE PHASES
Phases are the discrete hooks that components can implement. The manager executes these hooks in dependency order during startup and shutdown.
Enum: LifecyclePhase
Ordered Phase Arrays
Helper Functions
| Function | Returns |
|---|---|
| getPhaseHookName(phase) | String hook name for a phase (e.g. "initialize") |
| getComponentMethod(phase) | Method name to call on a component (e.g. "start") |
COMPONENT INTERFACE
Components implement LifecycleComponent to participate in the lifecycle. All hook methods are optional — implement only what you need.
Interface: LifecycleComponent
Interface: LifecycleRegistrationOptions
timeout: Infinity means no bound; NaN or a negative value throws RangeError at registration; a timed-out hook is not retried.
priority (default 0): orders components that share a dependency level, and since 1.2.0 it is a barrier, not a hint. Every component at one priority finishes the phase before the next priority begins, so register(metrics, { priority: 100 }) genuinely starts before register(server, { priority: 0 }). Previously the whole level was launched concurrently up to concurrency (default 10) and the sorted order was observable only at concurrency: 1 — whichever hook happened to finish first won. Components sharing a priority still run together, up to concurrency, so the default configuration (everything at priority 0) is unchanged. Shutdown mirrors startup within a level: the lowest priority stops first, the highest last.
Priority only orders components that are already in the same stage. A component with a dependsOn that puts it alone in its own stage gains nothing from a high priority — dependencies decide the stage, priority decides the order inside it.
Interface: LifecycleRetryOptions
Interface: LifecycleRegistration
Example: Implementing a Component
LIFECYCLE CONTEXT
Every component hook receives a LifecycleContext with cancellation support, phase info, and metadata.
Interface: LifecycleContext
Factory Function
Example: Using Context
LIFECYCLE MANAGER
The LifecycleManager is the central orchestrator. It registers components, resolves dependency order, executes hooks, handles signals, and manages shutdown.
Interface: LifecycleManagerOptions
shutdownTimeout: Infinity means no deadline. handleSignals: handlers are installed by start() (not the constructor) and removed after shutdown; a second signal during shutdown exits with code 1.
Since 1.2.0, shutdown() no longer disposes a component whose stop() is still running. A stop() hook that blows its own component timeout is abandoned rather than cancelled; shutdown used to wait for such hooks only before the stop phase, so one abandoned during it had dispose() run on top of it while shutdown() resolved and reported the application DISPOSED. Each shutdown phase now waits for abandoned hooks to settle before the next begins, still bounded by shutdownTimeout, so await shutdown(); process.exit(0) can no longer cut a drain short.
Class: LifecycleManager
| Member | Type / Signature | Description |
|---|---|---|
| register() | register(component, options?): void | Register a component with optional config |
| start() | start(): Promise<void> | Execute startup phases (idempotent) |
| shutdown() | shutdown(): Promise<void> | Execute shutdown phases (idempotent) |
| state | LifecycleState (getter) | Current application state |
| events | LifecycleEventEmitter (getter) | Event emitter for observability |
| registry | LifecycleRegistry (getter) | The component registry |
| getStatus() | getStatus(): ReadonlyMap<string, { state, results }> | Per-component state and execution results |
| dispose() | dispose(): void | Clean up signal handlers and event listeners |
Factory Function
LIFECYCLE REGISTRY
The LifecycleRegistry manages component registration, validates dependencies, and builds the dependency graph.
Class: LifecycleRegistry
| Method | Signature | Description |
|---|---|---|
| register() | register(component, options?): void | Register a component. Throws if frozen or duplicate. |
| validate() | validate(): void | Validate all dependencies exist and graph is acyclic. |
| freeze() | freeze(): void | Validate and lock — no more registrations allowed. |
| get() | get(id): LifecycleRegistration | undefined | Look up a registration by ID. |
| getAll() | getAll(): readonly LifecycleRegistration[] | All registrations. |
| getIds() | getIds(): readonly string[] | All registration IDs. |
| graph | DependencyGraph (getter) | The dependency graph. |
| size | number (getter) | Number of registered components. |
Example: Registration
STATE MACHINE
The LifecycleStateMachine tracks and validates state transitions for a single entity (component or application).
Class: LifecycleStateMachine
| Member | Type / Signature | Description |
|---|---|---|
| state | LifecycleState (getter) | Current state |
| isTerminal | boolean (getter) | True if STOPPED or DISPOSED |
| isRunning | boolean (getter) | True if STARTED or READY |
| transition() | transition(to: LifecycleState): void | Validate and apply transition. Throws on invalid. |
| canTransition() | canTransition(to): boolean | Check if transition is valid. |
| forceState() | forceState(state): void | Set state without validation (recovery/init only). |
Example
DEPENDENCY GRAPH
The DependencyGraph is a directed acyclic graph that tracks component dependencies, detects cycles, and enables topological ordering.
Class: DependencyGraph
| Method | Signature | Description |
|---|---|---|
| addNode() | addNode(id: string): void | Add a node to the graph. |
| addEdge() | addEdge(from, to): void | Add a directed edge: from depends on to. |
| getNodes() | getNodes(): readonly string[] | All nodes. |
| getDependencies() | getDependencies(id): readonly string[] | Nodes that id depends on. |
| getDependents() | getDependents(id): readonly string[] | Nodes that depend on id. |
| validate() | validate(): void | Throw LifecycleDependencyError if cycle detected. |
Topological Sort Functions
Within a stage, components are sorted by priority — highest first for topologicalSort, lowest first for reverseTopologicalSort. Since 1.2.0 reverseTopologicalSort reverses each stage’s contents as well as the stage list, so a shutdown is the exact mirror of the startup order; it used to reverse only the stage list, leaving every stage in descending-priority order. The executor treats each run of equal priority as a barrier, so a stage is fully parallel only where its components share a priority.
EXECUTION PLANS
Execution plans translate the dependency graph into ordered stages for a specific lifecycle phase.
Interface: ExecutionPlan
Interface: ExecutionStage
Factory Function
Example
Components inside a stage are listed in execution order: priority descending for startup phases, ascending for shutdown phases. The executor runs each run of equal priority as one batch and waits for it before starting the next, so ["server", "cache"] runs concurrently only if both were registered at the same priority.
EXECUTOR
The LifecycleExecutor runs component hooks with timeout, retry, and concurrency support.
Class: LifecycleExecutor
| Method | Signature | Description |
|---|---|---|
| execute() | execute(registration, phase, context): Promise<ExecutionResult> | Run a single component hook with retry and timeout. |
| executeStage() | executeStage(registrations, phase, context, concurrency): Promise<ExecutionResult[]> | Run a stage one priority group at a time, each group limited by concurrency. The next group starts only once the previous has settled. |
Interface: ExecutionResult
EVENTS
The LifecycleEventEmitter emits typed events at each lifecycle phase, enabling observability integration without depending on @zudojs/events.
Class: LifecycleEventEmitter
| Method | Signature | Description |
|---|---|---|
| on() | on(type, listener): () => void | Subscribe to an event type. Returns unsubscribe function. |
| emit() | emit(type, data): void | Emit an event with timestamp. |
| clear() | clear(): void | Remove all listeners. |
Enum: LifecycleEventType (16 event types)
Event Payloads
Example: Observability Integration
SIGNAL HANDLING
Automatic process signal interception for graceful shutdown. The lifecycle manager installs these by default.
Function: installSignalHandlers
Interface: SignalHandlerOptions
Constant: DEFAULT_SHUTDOWN_SIGNALS
Example: Custom Signal Handling
ASYNC UTILITIES
Helper functions for timeout, abort, and concurrency control used internally by the executor.
Function: withTimeout
Wraps an async function with a timeout. Throws LifecycleTimeoutError if exceeded.
Function: withAbort
Wraps an async function with abort signal support. Rejects when the signal is aborted.
Function: withConcurrency
Execute async operations on an array with a maximum concurrency limit.
ERROR HIERARCHY
All error types are defined in @zudojs/errors and re-exported by this package.
| Error | When Thrown |
|---|---|
| LifecycleError | Base error for all lifecycle-related issues. Since 1.2.0 it also covers registry and abort failures — registering after freeze(), a duplicate id, an unregistered dependsOn target and a cancelled withAbort — which used to throw a bare Error. The messages are unchanged, but they now carry an ErrorCode and answer instanceof LifecycleError. |
| LifecycleStateError | Invalid state transition attempted |
| LifecycleTimeoutError | Component operation exceeded timeout |
| LifecycleDependencyError | Circular dependency or missing dependency detected |
| LifecycleComponentError | A component hook threw during execution |
FULL INTEGRATION EXAMPLE
Complete working example: register components with dependencies, handle signals, observe events, and manage the full lifecycle.
COMPLETE EXPORT INDEX
Every name @zudojs/lifecycle exports from its package root at v1.2.0 — 36 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 36 exports
DependencyGraph LifecycleEventEmitter LifecycleExecutor LifecycleManager LifecycleRegistry LifecycleStateMachinebuildExecutionPlan createLifecycleContext createLifecycleManager getComponentMethod getPhaseHookName installSignalHandlers reverseTopologicalSort topologicalSort withAbort withConcurrency withTimeoutExecutionPlan ExecutionResult ExecutionStage LifecycleApplicationEvent LifecycleComponent LifecycleComponentEvent LifecycleContext LifecycleEvent LifecycleManagerOptions LifecycleRegistration LifecycleRegistrationOptions LifecycleRetryOptions SignalHandlerOptionsLifecycleEventListener LifecycleEventType TopologicalStageDEFAULT_SHUTDOWN_SIGNALS SHUTDOWN_PHASES STARTUP_PHASES