Skip to content

endpoint() + lifecycle

@nwire/endpoint is the process-lifecycle layer: graceful shutdown, SIGTERM drain via http-terminator, K8s liveness/readiness probes. The entry-verb shape is endpoint(...).use(adopter).mount(app).run().

Signature

ts
import { endpoint } from "@nwire/endpoint";

function endpoint(name: string, options?: EndpointConfig): EndpointBuilder;

interface EndpointConfig {
  readonly banner?: boolean;
  readonly probes?: {
    readonly enabled?: boolean;
    readonly port?: number;        // default 9400
  };
  readonly shutdown?: {
    readonly drainTimeout?: number;   // default 30_000 (ms)
    readonly hardTimeout?: number;    // default 5_000 (ms)
    readonly drainDelay?: number;     // wait between /ready→503 and drain
    readonly onShutdown?: () => Promise<void>;
  };
  readonly exitOnShutdown?: boolean;  // tests pass false
}

interface EndpointBuilder {
  use(adopter: Adapter): EndpointBuilder;       // install a transport adopter
  mount(app: App | ComposedApp): EndpointBuilder;
  run(): Promise<RunningEndpoint>;
}

Minimal

ts
import { createApp } from "@nwire/app";
import { endpoint } from "@nwire/endpoint";
import { get } from "@nwire/wires/http";
import { httpKoa } from "@nwire/koa";

const app = createApp({ appName: "hello" });
app.wire(get("/hello"), async () => ({ hello: "world" }));

await endpoint("hello", { probes: { enabled: true } })
  .use(httpKoa({ port: 3000 }))
  .mount(app)
  .run();

With forge

ts
import { createApp, defineRegistry } from "@nwire/app";
import { forgePlugins } from "@nwire/forge";
import { endpoint } from "@nwire/endpoint";
import { httpKoa } from "@nwire/koa";

const app = createApp({
  appName: "orders",
  registry: defineRegistry({ handlers: [placeOrder] }),
  plugins: [...forgePlugins()],
});

await endpoint("orders-api", { probes: { enabled: true } })
  .use(httpKoa({ port: 3000 }))
  .mount(app)
  .run();

mount(app) plumbs the App's container into the adopter dispatch so every wire's handler resolves bindings from the App that owns the wire. appCompose(a, b) returns a ComposedApp you mount the same way.

Multi-adopter

Stack adopters on the same endpoint:

ts
import { httpKoa } from "@nwire/koa";
import { queueInMemory } from "@nwire/queue";
import { mcpAdapter } from "@nwire/mcp";

await endpoint("svc", { probes: { enabled: true } })
  .use(httpKoa({ port: 3000 }))
  .use(queueInMemory())
  .use(mcpAdapter())
  .mount(app)
  .run();

Each adopter consumes wires whose binding.$adapter matches its kind.

Boot output

text
[http-koa] listening on 0.0.0.0:3000
[lifecycle] /ready → 200; serving

K8s probes

endpoint({ probes: { enabled: true, port: 9400 } }) opens a separate operational HTTP server on port 9400 with:

  • GET /live — liveness. 200 once the process is up.
  • GET /ready — readiness. 200 once all addCheck() contributors pass.
yaml
livenessProbe:
  httpGet: { path: /live, port: 9400 }
  initialDelaySeconds: 10
readinessProbe:
  httpGet: { path: /ready, port: 9400 }
  initialDelaySeconds: 3
  periodSeconds: 5

addCheck

Adopters and plugins register readiness contributors via the AdapterBootContext:

ts
// inside an Adapter's boot:
ctx.addCheck({
  name: "redis",
  check: async () => { await redis.ping(); },
  timeout: 2_000,
});

Each check's check() returns void on success or throws on failure. The aggregate /ready returns 200 only when every check resolves.

Shutdown sequence

  1. running.shutdown(reason) or SIGTERM/SIGINT received
  2. /ready flips to 503
  3. drainDelay wait (gives the LB time to redirect)
  4. Each adopter's shutdown() runs (HTTP drains in-flight, queue stops pulling new jobs, etc.)
  5. App plugins' dispose() fires in reverse boot order
  6. onShutdown() hook (if configured)
  7. Probes server closes
  8. Process exits (unless exitOnShutdown: false)

drainTimeout bounds step 4; hardTimeout is the SIGKILL backstop.

Test seam

ts
const running = await endpoint("test", {
  exitOnShutdown: false,
  banner: false,
  probes: { enabled: false },
})
  .use(httpKoa({ port: 0 }))
  .mount(app)
  .run();

// ... test assertions ...

await running.shutdown("test");

exitOnShutdown: false keeps the test process alive after the shutdown sequence completes.

See also

MIT licensed.