@molecule/api-process-lifecycle

Core interface · process-lifecycle · API (Node) · v1.2.1 · Apache-2.0

Readiness and liveness checks with per-check timeouts and criticality, a drain state, in-flight request tracking, and ordered shutdown hooks for an API process.

npm install @molecule/api-process-lifecycle

npm · Source on GitHub

How it works

@molecule/api-process-lifecycle is the process-lifecycle core interface on the API (Node) side: the API your app calls, with no vendor inside.

Choose the implementation by bonding one of its 1 provider: @molecule/api-process-lifecycle-express.

import {
  getProvider,
  registerReadinessCheck,
  registerShutdownHook,
  isReady,
} from '@molecule/api-process-lifecycle'
import { provider } from '@molecule/api-process-lifecycle-express'
import { setProvider } from '@molecule/api-process-lifecycle'

// Wire the provider once at startup
setProvider(provider)

// Readiness gates a load balancer; hooks close what the process owns
registerReadinessCheck({ name: 'database', check: async () => db.ping() })
registerShutdownHook({ name: 'database-pool', run: () => db.pool.end() })

const report = await isReady()
// report.ready === false while draining

Providers (1): @molecule/api-process-lifecycle-express

Works with: @molecule/api-bond

Reference

Process lifecycle core interface for molecule.dev.

Defines the contract a process uses to report readiness and liveness, drain before shutdown, and run ordered teardown hooks. The interface carries no implementation: @molecule/api-process-lifecycle-express provides the Express/Node binding that mounts health endpoints and handles SIGTERM/SIGINT.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import {
  getProvider,
  registerReadinessCheck,
  registerShutdownHook,
  isReady,
} from '@molecule/api-process-lifecycle'
import { provider } from '@molecule/api-process-lifecycle-express'
import { setProvider } from '@molecule/api-process-lifecycle'

// Wire the provider once at startup
setProvider(provider)

// Readiness gates a load balancer; hooks close what the process owns
registerReadinessCheck({ name: 'database', check: async () => db.ping() })
registerShutdownHook({ name: 'database-pool', run: () => db.pool.end() })

const report = await isReady()
// report.ready === false while draining

Type

core

Installation

npm install @molecule/api-process-lifecycle @molecule/api-bond

API

Interfaces

LifecycleCheckResult

The detailed result of a check function.

interface LifecycleCheckResult {
  /** Whether the check passed. */
  ok: boolean
  /** Optional human-readable explanation, safe to log or return in a health response. */
  detail?: string
}

LifecycleCheck

A named readiness or liveness check.

Checks are async-friendly and individually bounded: a check that does not answer within LifecycleCheck.timeoutMs is reported as failed (and timedOut: true) instead of holding the health endpoint open. A check that throws is a failed check, never a crashed process.

interface LifecycleCheck {
  /** Unique name shown in the readiness/liveness report (e.g. `'database'`). */
  name: string
  /** The check itself. Return/resolve `true`/`undefined` to pass, `false` to fail. */
  check: () => LifecycleCheckOutcome | Promise<LifecycleCheckOutcome>
  /** Per-check timeout in milliseconds. Defaults to the provider's configured value (2000 ms). */
  timeoutMs?: number
  /**
   * Whether this check gates the overall verdict. A failing critical check
   * makes readiness (or liveness) false; a failing non-critical check is
   * reported but does not remove the process from rotation. Defaults to `true`.
   */
  critical?: boolean
  /** Optional human-readable purpose of the check, for logs. */
  description?: string
}

LifecycleCheckReport

The result of running one check.

interface LifecycleCheckReport {
  /** The check's name. */
  name: string
  /** Whether the check passed (a timeout or throw is a failure). */
  ok: boolean
  /** Whether this check gates the overall verdict. */
  critical: boolean
  /** How long the check took, in milliseconds. */
  durationMs: number
  /** The check's own detail, or the thrown error's message. */
  detail?: string
  /** `true` when the check did not answer inside its timeout. */
  timedOut?: boolean
}

WarmupStep

A named step of the boot warm-up.

A load balancer takes the NEXT instance down as soon as this one's check passes, so readiness must not pass while the process is still loading its module graph or preloading caches. Warm-up steps are the work a boot does AFTER the port is bound but BEFORE the instance can serve at full speed; readiness stays closed until every step has completed or timed out.

A step that throws or exceeds its own timeout is reported and logged, and the warm-up still settles: a broken preload must not hold an instance out of rotation for the whole deploy window.

interface WarmupStep {
  /** Unique name shown in the warm-up report (e.g. `'chat-handler-module-graph'`). */
  name: string
  /** The work to observe. Resolve when the work has settled. */
  run: () => void | Promise<void>
  /**
   * How long this step may take before it is reported as timed out. Defaults
   * to the provider's configured value (45000 ms).
   */
  timeoutMs?: number
}

WarmupStepReport

The result of running one warm-up step.

interface WarmupStepReport {
  /** The step's name. */
  name: string
  /** `true` when the step completed without throwing or timing out. */
  ok: boolean
  /** How long the step took, in milliseconds. */
  durationMs: number
  /** The thrown error's message, when the step failed. */
  error?: string
  /** `true` when the step did not finish inside its timeout. */
  timedOut?: boolean
}

WarmupReport

The result of a warm-up pass.

The pass always settles — a failed or hung step is bounded and reported, not propagated — because readiness must not stay closed forever after a broken preload.

interface WarmupReport {
  /** `true` when every step completed without throwing or timing out. */
  ok: boolean
  /** Total wall-clock time of the pass, in milliseconds. */
  durationMs: number
  /** Per-step results, in execution order. */
  steps: WarmupStepReport[]
}

DrainHook

A step run as a shutdown starts, BEFORE the load-balancer flip window and before in-flight requests are drained.

This is where an application checkpoints work it could otherwise lose to a force-kill during the flip window (for example, persisting a running chat turn). Drain hooks are bounded and never stop the shutdown: a hook that throws is reported, and the flip window still runs — skipping it would let a load balancer route to an instance whose readiness is already 503.

interface DrainHook {
  /** Unique name shown in the drain report and logs (e.g. `'chat-turn-checkpoint'`). */
  name: string
  /**
   * The work to run.
   *
   * @param reason - Why the shutdown started (usually a signal name).
   */
  run: (reason?: string) => void | Promise<void>
  /** Execution order, lowest first. Hooks with the same order run in registration order. */
  order?: number
  /** Per-hook timeout in milliseconds. Defaults to the provider's configured hook timeout. */
  timeoutMs?: number
}

DrainHookReport

The result of running one drain hook.

interface DrainHookReport {
  /** The hook's name. */
  name: string
  /** `true` when the hook completed without throwing or timing out. */
  ok: boolean
  /** How long the hook took, in milliseconds. */
  durationMs: number
  /** The thrown error's message, when the hook failed. */
  error?: string
  /** `true` when the hook did not finish inside its timeout. */
  timedOut?: boolean
}

ReadinessReport

The answer to "may a load balancer send this process new traffic?".

interface ReadinessReport {
  /**
   * `true` only when the process is ready, not draining, no critical check
   * failed, the boot warm-up has settled, and the configured number of
   * consecutive passing probes has been observed.
   */
  ready: boolean
  /** The current lifecycle phase. */
  state: ProcessLifecycleState
  /** `true` while draining or stopped. */
  draining: boolean
  /** `true` once every boot warm-up step has completed or timed out. */
  warm: boolean
  /** Number of requests currently being served. */
  inFlightRequests: number
  /** The per-check results, in registration order. Empty while draining (checks are not run). */
  checks: LifecycleCheckReport[]
}

LivenessReport

The answer to "is this process alive?".

Liveness deliberately ignores drain state: a draining process is still alive and restarting it would abort the requests the drain is trying to finish.

interface LivenessReport {
  /** `true` while the process has not stopped and no critical liveness check failed. */
  live: boolean
  /** The current lifecycle phase. */
  state: ProcessLifecycleState
  /** Milliseconds since the provider was created. */
  uptimeMs: number
  /** The per-check results, in registration order. */
  checks: LifecycleCheckReport[]
}

ShutdownHook

A teardown step run during shutdown (close a pool, flush a queue, release a lock). Hooks run sequentially so each resource is fully released before the next one starts.

interface ShutdownHook {
  /** Unique name shown in the shutdown report and logs (e.g. `'database-pool'`). */
  name: string
  /** The teardown work. A throw or timeout is reported and does not stop later hooks. */
  run: () => void | Promise<void>
  /**
   * Execution order, lowest first. Hooks with the same order run in
   * registration order. Defaults to `0`.
   */
  order?: number
  /** Per-hook timeout in milliseconds. Defaults to the provider's configured value (10000 ms). */
  timeoutMs?: number
}

ShutdownHookReport

The result of running one shutdown hook.

interface ShutdownHookReport {
  /** The hook's name. */
  name: string
  /** `true` when the hook completed without throwing or timing out. */
  ok: boolean
  /** How long the hook took, in milliseconds. */
  durationMs: number
  /** The thrown error's message, when the hook failed. */
  error?: string
  /** `true` when the hook did not finish inside its timeout. */
  timedOut?: boolean
}

ShutdownReport

The result of a shutdown pass.

interface ShutdownReport {
  /** Always `'stopped'` once the pass finishes. */
  state: ProcessLifecycleState
  /** The reason passed to `startDraining()` / `runShutdownHooks()` (usually a signal name). */
  reason?: string
  /** Total wall-clock time of the hook pass, in milliseconds. */
  durationMs: number
  /** Requests still in flight when the hook pass started. */
  inFlightAtStart: number
  /** Per-hook results, in execution order. */
  hooks: ShutdownHookReport[]
}

ProcessLifecycleProvider

The contract every process-lifecycle provider implements.

Application code reaches the bonded provider through the accessors in @molecule/api-process-lifecycle (setProvider, getProvider, registerReadinessCheck, isReady, …) rather than importing a provider.

interface ProcessLifecycleProvider {
  /** The current lifecycle phase. */
  state(): ProcessLifecycleState
  /** Move from `starting` to `ready` once the boot sequence has finished. */
  markReady(): void
  /** `true` while draining or stopped. */
  isDraining(): boolean
  /** Begin draining: readiness reports not-ready from this call onward. Idempotent. */
  startDraining(reason?: string): void
  /**
   * Record that a request started. The returned function releases the
   * request; it is idempotent so a `finish`/`close` pair cannot double-count.
   */
  beginRequest(): () => void
  /** Number of requests currently being served. */
  inFlightRequests(): number
  /**
   * Resolve `true` when the in-flight count reaches zero, or `false` when
   * `timeoutMs` elapses first.
   */
  waitForRequestsToDrain(timeoutMs: number): Promise<boolean>
  /**
   * Register a named readiness check.
   *
   * @returns A function that removes the check.
   */
  registerReadinessCheck(check: LifecycleCheck): () => void
  /**
   * Register a named liveness check.
   *
   * @returns A function that removes the check.
   */
  registerLivenessCheck(check: LifecycleCheck): () => void
  /**
   * Register an ordered shutdown hook.
   *
   * @returns A function that removes the hook.
   */
  registerShutdownHook(hook: ShutdownHook): () => void
  /**
   * Register a named boot warm-up step.
   *
   * Steps registered before `ProcessLifecycleProvider.markReady` run as
   * one pass when it is called; readiness stays closed until every step has
   * completed or timed out.
   *
   * @returns A function that removes the step.
   */
  registerWarmupStep(step: WarmupStep): () => void
  /**
   * Run every registered warm-up step once.
   *
   * Idempotent: a second call joins the in-flight pass instead of running the
   * work again. Never rejects — a failing or hanging step is reported.
   *
   * @returns The per-step warm-up report.
   */
  runWarmup(): Promise<WarmupReport>
  /**
   * Whether the boot warm-up has settled (every step completed or timed out).
   *
   * @returns `true` once the warm-up pass has finished.
   */
  isWarm(): boolean
  /**
   * Register a drain hook, run at the start of a shutdown before the
   * load-balancer flip window.
   *
   * @returns A function that removes the hook.
   */
  registerDrainHook(hook: DrainHook): () => void
  /**
   * Run every drain hook in order, then resolve. Never rejects.
   *
   * The graceful-shutdown sequence calls this BEFORE its flip window and runs
   * the window even when a hook fails.
   *
   * @param reason - Why the shutdown started (usually a signal name).
   * @returns The per-hook drain report.
   */
  runDrainHooks(reason?: string): Promise<DrainHookReport[]>
  /** Run every readiness check and report whether traffic may be routed here. */
  isReady(): Promise<ReadinessReport>
  /** Run every liveness check and report whether the process is alive. */
  isLive(): Promise<LivenessReport>
  /** Run every shutdown hook in order, then mark the process stopped. Idempotent. */
  runShutdownHooks(reason?: string): Promise<ShutdownReport>
}

Types

ProcessLifecycleState

The lifecycle phase of an API process.

  • starting — the process exists but has not finished its boot sequence; a load balancer must not route to it yet.
  • ready — the process is serving traffic.
  • draining — a shutdown was requested. Readiness reports not-ready so a load balancer stops sending new traffic, while in-flight requests finish.
  • stopped — every shutdown hook has run; the process is about to exit.
type ProcessLifecycleState = 'starting' | 'ready' | 'draining' | 'stopped'

LifecycleCheckOutcome

What a check function may return. true/undefined mean the check passed; false means it failed; an object carries a pass/fail verdict plus an optional human-readable detail for logs and the readiness response body.

type LifecycleCheckOutcome = boolean | void | LifecycleCheckResult

Functions

setProvider(provider)

Registers a process-lifecycle provider as the active singleton. Called by a bond package during application startup.

Any readiness check, liveness check or shutdown hook registered BEFORE a provider existed is applied now (and re-applied if a provider is replaced).

function setProvider(provider: ProcessLifecycleProvider): void
  • provider — The provider implementation to bond.

getProvider()

Retrieves the bonded process-lifecycle provider, throwing if none is wired.

function getProvider(): ProcessLifecycleProvider

Returns: The bonded process-lifecycle provider.

hasProvider()

Checks whether a process-lifecycle provider is currently bonded.

function hasProvider(): boolean

Returns: true if a provider is bonded.

resetRegistrationsForTests()

Drop every queued registration. For tests that exercise several apps in one process; application code never calls it.

function resetRegistrationsForTests(): void

registerReadinessCheck(check)

Registers a named readiness check.

When no provider is bonded yet — the normal case for a bond package registering a check while its module is imported — the check is queued and applied as soon as setProvider() runs.

function registerReadinessCheck(check: LifecycleCheck): () => void
  • check — The check to register.

Returns: A function that removes the check.

registerLivenessCheck(check)

Registers a named liveness check. Queued until a provider is bonded, like registerReadinessCheck.

function registerLivenessCheck(check: LifecycleCheck): () => void
  • check — The check to register.

Returns: A function that removes the check.

registerShutdownHook(hook)

Registers an ordered shutdown hook. Queued until a provider is bonded, like registerReadinessCheck.

function registerShutdownHook(hook: ShutdownHook): () => void
  • hook — The hook to register.

Returns: A function that removes the hook.

isReady()

Runs every readiness check on the bonded provider.

function isReady(): Promise<ReadinessReport>

Returns: The readiness report a load balancer decides from.

isLive()

Runs every liveness check on the bonded provider.

function isLive(): Promise<LivenessReport>

Returns: The liveness report.

isDraining()

Reports whether the bonded provider is draining or stopped.

function isDraining(): boolean

Returns: true while draining or stopped.

startDraining(reason)

Asks the bonded provider to begin draining.

function startDraining(reason?: string): void
  • reason — Optional reason (usually a signal name) recorded by the provider.

beginRequest()

Records a request as in flight and returns its idempotent release function.

function beginRequest(): () => void

Returns: A function that releases the request when it finishes.

inFlightRequests()

Reports how many requests the bonded provider is currently serving.

function inFlightRequests(): number

Returns: The in-flight request count.

runShutdownHooks(reason)

Runs every shutdown hook on the bonded provider.

function runShutdownHooks(reason?: string): Promise<ShutdownReport>
  • reason — Optional reason (usually a signal name) recorded in the report.

Returns: The per-hook shutdown report.

registerWarmupStep(step)

Registers a named boot warm-up step. Queued until a provider is bonded, like registerReadinessCheck.

function registerWarmupStep(step: WarmupStep): () => void
  • step — The warm-up step to register.

Returns: A function that removes the step.

runWarmup()

Runs every registered boot warm-up step on the bonded provider.

function runWarmup(): Promise<WarmupReport>

Returns: The per-step warm-up report.

isWarm()

Reports whether the boot warm-up has settled on the bonded provider.

function isWarm(): boolean

Returns: true once the warm-up pass has finished.

registerDrainHook(hook)

Registers a drain hook. Queued until a provider is bonded, like registerReadinessCheck.

function registerDrainHook(hook: DrainHook): () => void
  • hook — The drain hook to register.

Returns: A function that removes the hook.

runDrainHooks(reason)

Runs every drain hook on the bonded provider.

function runDrainHooks(reason?: string): Promise<DrainHookReport[]>
  • reason — Optional reason (usually a signal name) recorded in the report.

Returns: The per-hook drain report.

Available Providers

ProviderPackage
Express@molecule/api-process-lifecycle-express

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-bond ^1.0.1

Runtime Dependencies

  • @molecule/api-bond

  • Liveness and readiness answer different questions. Liveness asks "is this process alive?" and stays true while draining (restarting a draining process would abort the requests the drain is finishing). Readiness asks "may traffic be routed here?" and turns false at the first startDraining() call.

  • Checks are individually bounded. A check that hangs is reported as failed at its timeout instead of holding the health endpoint open. A check that throws is a failed check, never an unhandled rejection.

  • Critical vs non-critical. Only critical failures change the verdict; non-critical results are visible in the report so a degraded dependency can be observed without taking the whole process out of rotation.

  • Read getProvider() after bonds are wired. Like every bond-backed accessor, these throw when no provider is bonded, so do not call them at module top level.