Skip to content

Service Lifecycle

Rizky Zulkarnaen edited this page Sep 9, 2026 · 1 revision

Service Lifecycle

Source of truth: docs/api-reference.md § Service lifecycle · ADR: ADR-0020

service() attaches lifecycle behavior to one entry of defineApp({ services }):

import { defineApp, service } from "lugas";

defineApp({
  services: {
    db: service({
      name: "db",
      value: createDb(),
      init: async (db) => { await db.connect(); },
      dispose: async (db) => { await db.close(); },
    }),
  },
});

Startup: gated, ordered, rollback on failure

  • init runs at serve time in declaration order (the services object key order). No Lugas handler executes before every init has settled — early requests are held by a traffic gate.
  • A startup failure disposes already-initialized services in reverse and surfaces through server.lugasLifecycle.ready (rejection) plus a redacted 503 on held routes.
  • Plain (non-service()) values keep the live-reference behavior and are never initialized or disposed.

Shutdown: drain-ordered, idempotent

server.lugasLifecycle.shutdown() runs: stop accepting → drain in-flight requests and track()ed tasks under a deadline → reverse-order disposal.

const server = app.serve({
  port: 3000,
  shutdown: {
    drainDeadlineMs: 10_000,  // default
    signals: true,            // opt-in SIGINT/SIGTERM; Lugas never exits the process
  },
});

server.lugasLifecycle.track(backgroundJob()); // drain waits for tracked work

Outcomes are reported distinctly: connectionsClosed, trackedWorkCompleted, disposalCompleted, plus disposalFailures.

Deadline invariant: when the deadline expires with work remaining, the outcome is unsuccessful (cooperated: false, deadlineExpired: true); connections are force-closed but services possibly still in use are not disposed — continuing work observes an intact resource, never fabricated success. Detached (untracked) work is the application's responsibility.

Composition

  • SSE: an open stream is in-flight work — close writers on shutdown for prompt exits, or rely on deadline force-close (which provably runs stream cleanups). See Server-Sent Events.
  • Native route values (plain functions, Response, Bun.file, { dir }) bypass the framework pipeline and are not lifecycle-gated.
  • Signals are opt-in: importing Lugas installs no handlers and never exits the process.

Pinned by tests/lifecycle/; evidence in docs/reports/issues/M7-004.md.

Clone this wiki locally