@nwire/endpoint 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Gefter / 200apps Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @nwire/endpoint
2
+
3
+ > Process lifecycle — graceful shutdown, K8s probes, signal handling for any Node server.
4
+
5
+ ## What it is
6
+
7
+ Wraps any Node server (Express, Fastify, Koa, Nest, Hono, raw `http`, or a Nwire interface) with the boring lifecycle every long-running process needs: `http-terminator` for draining keep-alive sockets, `lightship` for readiness/liveness probes on a dedicated port, signal handling, and ordered shutdown. The outermost layer of every Nwire entrypoint.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @nwire/endpoint
13
+ ```
14
+
15
+ ## Standalone use
16
+
17
+ For developers who need real graceful shutdown around an Express/Fastify/Koa/Nest app without rewriting boot code. `endpoint().serve(target).run()` accepts any server-like target.
18
+
19
+ ```ts
20
+ import express from "express";
21
+ import { endpoint } from "@nwire/endpoint";
22
+
23
+ const app = express();
24
+ app.get("/hello", (_, res) => res.json({ ok: true }));
25
+
26
+ await endpoint("api", { port: 3000 }).serve(app).run();
27
+ // http-terminator drains keep-alive; /live + /ready on :9400; SIGTERM handled
28
+ ```
29
+
30
+ ## Within nwire-app
31
+
32
+ For developers using this package as part of the Nwire stack. `endpoint().use(app).mount(api).run()` boots a Nwire `App` (plugins, providers, lifecycle events) and mounts one or more Nwire interfaces in the same managed process.
33
+
34
+ ```ts
35
+ import { endpoint } from "@nwire/endpoint";
36
+ import { createApp } from "@nwire/forge";
37
+ import { http } from "@nwire/http";
38
+
39
+ const app = createApp("learnflow").module(stations);
40
+ const api = http("api").wire(stationRoutes);
41
+
42
+ await endpoint("api", { port: 3000 }).use(app).mount(api).run();
43
+ ```
44
+
45
+ ## API
46
+
47
+ - `endpoint(name, config).serve(target).run()` — canonical builder; accepts foreign frameworks, Nwire apps, and Nwire interfaces.
48
+ - `attachLifecycle({ server, shutdown, health })` — escape hatch when you already have a `Server`.
49
+ - `defineCheck(name, check)` — declarative readiness probe; aggregated into `/ready`.
50
+ - `RunningEndpoint` / `EndpointConfig` / `ShutdownConfig` / `HealthConfig` / `HealthCheck` — config types.
51
+
52
+ ## See also
53
+
54
+ - [Architecture sketch §05 — Foundation tier](../../architecture-sketch.html#packages)
55
+ - Built on [`http-terminator`](https://github.com/gajus/http-terminator) + [`lightship`](https://github.com/gajus/lightship)
56
+ - Sibling packages: [@nwire/container](../nwire-container), [@nwire/app](../nwire-app), [@nwire/http](../nwire-http)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Smoke tests for the endpoint builder. Exercises:
3
+ *
4
+ * - Type guards on the three Servable shapes (Nwire interface / app / foreign)
5
+ * - Builder accepts each shape via .serve()
6
+ * - .serve() with an unknown shape throws a clear error
7
+ *
8
+ * Full lifecycle integration (boot → listen → SIGTERM → drain → exit) is
9
+ * covered by the docker-compose integration suite, not this unit file.
10
+ * That suite lives at `examples/integration-tests/` and runs against
11
+ * real K8s probes + http-terminator timing.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=endpoint.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/endpoint.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Smoke tests for the endpoint builder. Exercises:
3
+ *
4
+ * - Type guards on the three Servable shapes (Nwire interface / app / foreign)
5
+ * - Builder accepts each shape via .serve()
6
+ * - .serve() with an unknown shape throws a clear error
7
+ *
8
+ * Full lifecycle integration (boot → listen → SIGTERM → drain → exit) is
9
+ * covered by the docker-compose integration suite, not this unit file.
10
+ * That suite lives at `examples/integration-tests/` and runs against
11
+ * real K8s probes + http-terminator timing.
12
+ */
13
+ import { describe, it, expect } from "vitest";
14
+ import { endpoint, isAppServable, isForeignServable, isNwireServable, } from "../endpoint.js";
15
+ describe("endpoint() — type guards", () => {
16
+ it("identifies a NwireServable by the $nwireServable marker", () => {
17
+ const ns = {
18
+ $nwireServable: true,
19
+ transport: "http",
20
+ attach: () => undefined,
21
+ };
22
+ expect(isNwireServable(ns)).toBe(true);
23
+ expect(isAppServable(ns)).toBe(false);
24
+ expect(isForeignServable(ns)).toBe(false);
25
+ });
26
+ it("identifies an AppServable by the $nwireApp marker", () => {
27
+ const app = {
28
+ $nwireApp: true,
29
+ container: {
30
+ resolve: () => undefined,
31
+ has: () => false,
32
+ register: () => undefined,
33
+ createScope: () => app.container,
34
+ },
35
+ boot: async () => undefined,
36
+ shutdown: async () => undefined,
37
+ };
38
+ expect(isAppServable(app)).toBe(true);
39
+ expect(isNwireServable(app)).toBe(false);
40
+ });
41
+ it("identifies a ForeignServable by .listen()", () => {
42
+ /**
43
+ * Anything with `.listen(port, host?, cb?): Server` is treated as a
44
+ * foreign framework. The endpoint will wrap the resulting Server
45
+ * with attachLifecycle().
46
+ */
47
+ const foreign = {
48
+ listen: () => ({}),
49
+ };
50
+ expect(isForeignServable(foreign)).toBe(true);
51
+ expect(isNwireServable(foreign)).toBe(false);
52
+ expect(isAppServable(foreign)).toBe(false);
53
+ });
54
+ it("rejects plain objects that satisfy none of the contracts", () => {
55
+ const garbage = { foo: 1, bar: () => undefined };
56
+ expect(isNwireServable(garbage)).toBe(false);
57
+ expect(isAppServable(garbage)).toBe(false);
58
+ expect(isForeignServable(garbage)).toBe(false);
59
+ });
60
+ });
61
+ describe("endpoint() — builder", () => {
62
+ it("accepts each Servable shape via .serve() in any order", () => {
63
+ /**
64
+ * The builder doesn't run anything until .run() is called, so we can
65
+ * verify ordering / acceptance without spinning up a server.
66
+ */
67
+ const app = {
68
+ $nwireApp: true,
69
+ container: {
70
+ resolve: () => undefined,
71
+ has: () => false,
72
+ register: () => undefined,
73
+ createScope: () => undefined,
74
+ },
75
+ boot: async () => undefined,
76
+ shutdown: async () => undefined,
77
+ };
78
+ const iface = {
79
+ $nwireServable: true,
80
+ transport: "queue",
81
+ attach: () => undefined,
82
+ };
83
+ expect(() => endpoint("api").serve(app).serve(iface)).not.toThrow();
84
+ expect(() => endpoint("api").serve(iface).serve(app)).not.toThrow();
85
+ });
86
+ it("throws a clear error when .serve() gets an unknown shape", () => {
87
+ /**
88
+ * Catching the bad-input case at .serve() time (not .run() time) means
89
+ * the developer sees the error at the call site, not buried in a
90
+ * runtime stack trace from inside the boot sequence.
91
+ */
92
+ expect(() => endpoint("api").serve({ random: 42 })).toThrow(/not a Nwire app, Nwire interface, or framework app/);
93
+ });
94
+ });
95
+ //# sourceMappingURL=endpoint.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.test.js","sourceRoot":"","sources":["../../src/__tests__/endpoint.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@nwire/endpoint` — production process lifecycle for Node servers.
3
+ *
4
+ * The outermost layer of any Nwire app, and the only piece that talks to
5
+ * the operating system. Wraps any Node server (Express, Fastify, Koa,
6
+ * Nest, Hono, Nwire interfaces) with K8s-grade graceful shutdown,
7
+ * http-terminator drain, and lightship readiness/liveness probes.
8
+ *
9
+ * Two public APIs:
10
+ *
11
+ * - `endpoint(name, config).serve(target).run()` — high-level builder
12
+ * (the v1.0 canonical entry). Handles boot ordering, transport
13
+ * attachment, probes, and shutdown sequence in one chained call.
14
+ * - `attachLifecycle({ server, shutdown, health })` — low-level escape
15
+ * hatch. Use this when you already have a Node `Server` and just
16
+ * need graceful shutdown + probes wrapped around it.
17
+ *
18
+ * Both compose around the same battle-tested primitives
19
+ * (`http-terminator` + `lightship`). The high-level builder is what most
20
+ * projects reach for; the low-level form is for adapters and tests.
21
+ */
22
+ export { endpoint, EndpointBuilder, isAppServable, isForeignServable, isNwireServable, type AppServable, type EndpointConfig, type ForeignServable, type HostBindings, type HostFactory, type NwireServable, type RunningEndpoint, type Servable, } from "./endpoint.js";
23
+ export { attachLifecycle, defineCheck, type HealthCheck, type HealthConfig, type LifecycleManager, type ShutdownConfig, } from "./lifecycle.js";
24
+ //# sourceMappingURL=endpoint-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-index.d.ts","sourceRoot":"","sources":["../src/endpoint-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,QAAQ,EACR,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,QAAQ,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@nwire/endpoint` — production process lifecycle for Node servers.
3
+ *
4
+ * The outermost layer of any Nwire app, and the only piece that talks to
5
+ * the operating system. Wraps any Node server (Express, Fastify, Koa,
6
+ * Nest, Hono, Nwire interfaces) with K8s-grade graceful shutdown,
7
+ * http-terminator drain, and lightship readiness/liveness probes.
8
+ *
9
+ * Two public APIs:
10
+ *
11
+ * - `endpoint(name, config).serve(target).run()` — high-level builder
12
+ * (the v1.0 canonical entry). Handles boot ordering, transport
13
+ * attachment, probes, and shutdown sequence in one chained call.
14
+ * - `attachLifecycle({ server, shutdown, health })` — low-level escape
15
+ * hatch. Use this when you already have a Node `Server` and just
16
+ * need graceful shutdown + probes wrapped around it.
17
+ *
18
+ * Both compose around the same battle-tested primitives
19
+ * (`http-terminator` + `lightship`). The high-level builder is what most
20
+ * projects reach for; the low-level form is for adapters and tests.
21
+ */
22
+ export { endpoint, EndpointBuilder, isAppServable, isForeignServable, isNwireServable, } from "./endpoint.js";
23
+ export { attachLifecycle, defineCheck, } from "./lifecycle.js";
24
+ //# sourceMappingURL=endpoint-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-index.js","sourceRoot":"","sources":["../src/endpoint-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,QAAQ,EACR,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,eAAe,GAShB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,WAAW,GAKZ,MAAM,aAAa,CAAC"}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * `endpoint()` — the canonical Nwire entry point for running a process.
3
+ *
4
+ * An endpoint is the outermost layer of a Nwire app. It owns:
5
+ *
6
+ * - the port (when listening for HTTP-class transports)
7
+ * - the signal handling (SIGTERM / SIGINT)
8
+ * - the graceful drain (http-terminator)
9
+ * - the K8s probes (lightship — startup / liveness / readiness)
10
+ * - the boot order (apps first, transports second)
11
+ * - the shutdown order (transports first, apps reverse-of-boot)
12
+ *
13
+ * The endpoint is the only Nwire layer that talks to the operating
14
+ * system. Everything else — apps, modules, interfaces, resolvers — is
15
+ * pure data + functions until an endpoint wraps them in a process.
16
+ *
17
+ * ## What can be served
18
+ *
19
+ * `endpoint(name, config).serve(target)` accepts any of:
20
+ *
21
+ * - A Nwire interface compiled to a Node server adapter (the common case)
22
+ * - An existing Express / Fastify / Koa / Nest app (the interop case)
23
+ * - A `@nwire/app` instance (boot order + container DI)
24
+ *
25
+ * Multiple `.serve()` calls compose: serve the app first to boot its
26
+ * container, then serve one or more interfaces that use that container.
27
+ *
28
+ * ## Why this exists
29
+ *
30
+ * Every framework eventually needs a process layer — something that knows
31
+ * about SIGTERM, drains in-flight requests, exposes `/healthz`, and binds
32
+ * to a port. Most frameworks bolt this on as a deployment afterthought.
33
+ * `@nwire/endpoint` makes it the first-class entry point so you don't
34
+ * write the same K8s-readiness code on every project. It works alone —
35
+ * install only this package and wrap any Node server.
36
+ */
37
+ import type { Server } from "node:http";
38
+ import type { Container } from "@nwire/container";
39
+ import { type HealthConfig, type HealthCheck, type LifecycleManager, type ShutdownConfig } from "./lifecycle.js";
40
+ /**
41
+ * Anything that can be served by an endpoint. Three categories:
42
+ *
43
+ * 1. **NwireServable** — a Nwire interface (http/queue/etc) that knows how
44
+ * to produce a request handler and report what transport it needs.
45
+ * 2. **ForeignServable** — an existing framework app (Express / Fastify /
46
+ * Koa / Nest / etc) that the endpoint will wrap.
47
+ * 3. **AppServable** — a Container-with-lifecycle that gets booted first
48
+ * so transports can use its bindings.
49
+ *
50
+ * Detection is structural: the endpoint inspects the shape at runtime to
51
+ * decide how to serve it. No magic — see {@link isNwireServable},
52
+ * {@link isAppServable}, {@link isForeignServable}.
53
+ */
54
+ export type Servable = NwireServable | ForeignServable | AppServable;
55
+ /**
56
+ * A Nwire interface compiled for the endpoint. The interface exposes how
57
+ * it wants to be mounted: which transport, what handler shape, what
58
+ * health checks it contributes.
59
+ */
60
+ export interface NwireServable {
61
+ readonly $nwireServable: true;
62
+ /** Which transport this interface uses. Decides what listener spins up. */
63
+ readonly transport: "http" | "graphql" | "queue" | "cron" | "ws" | "mcp" | "custom";
64
+ /**
65
+ * Attach to a host that owns the transport. For HTTP-class transports
66
+ * the endpoint provides a Koa/raw http server and the interface mounts
67
+ * its router. For queue-class transports the endpoint spins up the
68
+ * consumer loop.
69
+ */
70
+ attach(host: HostBindings): void | Promise<void>;
71
+ /** Optional health checks contributed by the interface itself. */
72
+ readonly checks?: readonly HealthCheck[];
73
+ /** Run during shutdown — disconnect from broker, flush, etc. */
74
+ shutdown?(): void | Promise<void>;
75
+ }
76
+ /**
77
+ * A foreign framework app — typically `express()`, `Fastify()`, `Koa()`,
78
+ * or `NestFactory.create(AppModule)`. The endpoint will use the framework's
79
+ * own listen mechanics where possible and wrap the resulting `Server` with
80
+ * `attachLifecycle()`.
81
+ */
82
+ export interface ForeignServable {
83
+ /** Required marker: any object that produces an http.Server via .listen(port). */
84
+ listen(port: number, hostname?: string, callback?: () => void): Server;
85
+ }
86
+ /**
87
+ * A Nwire app — a Container with boot/shutdown lifecycle. The endpoint
88
+ * boots it before mounting transports so any handler `ctx.resolve(...)`
89
+ * works the moment the first request lands.
90
+ *
91
+ * `@nwire/app` produces this shape; you can build your own minimal one if
92
+ * you don't want the full plugin system.
93
+ */
94
+ export interface AppServable {
95
+ readonly $nwireApp: true;
96
+ readonly container: Container;
97
+ boot(): Promise<void>;
98
+ shutdown(): Promise<void>;
99
+ /**
100
+ * Optional — app name used in lifecycle event payloads. Defaults to
101
+ * the endpoint's `name` when missing.
102
+ */
103
+ readonly appName?: string;
104
+ /**
105
+ * Optional — dispatch a framework lifecycle event on the app's bus.
106
+ * The endpoint uses this to fire `nwire.wire.mounting` /
107
+ * `nwire.wire.mounted` / `nwire.wire.unmounted` / `nwire.app.ready`
108
+ * at the right points in the serve loop.
109
+ *
110
+ * The dispatcher is duck-typed by event NAME so endpoint stays
111
+ * independent of `@nwire/app`. Apps that don't expose this method
112
+ * still serve correctly — endpoint just skips dispatch silently.
113
+ *
114
+ * Returns `false` when a series-bail subscriber prevented; `true`
115
+ * otherwise. Endpoint honors `false` only for the `*-ing` events.
116
+ */
117
+ dispatchFrameworkEvent?(eventName: string, payload: unknown): Promise<boolean>;
118
+ }
119
+ /** What the endpoint hands to each NwireServable.attach() call. */
120
+ export interface HostBindings {
121
+ /**
122
+ * Container of the served app, or `undefined` if no app was served.
123
+ * When undefined, the interface should preserve its own `.provide()`
124
+ * container (the L2 standalone case).
125
+ */
126
+ readonly container: Container | undefined;
127
+ /**
128
+ * Register a HealthCheck on the endpoint's lifecycle. Interfaces that
129
+ * need readiness verification (queue connected, NATS subscribed) call
130
+ * this so the K8s probes reflect real state.
131
+ */
132
+ addCheck(check: HealthCheck): void;
133
+ }
134
+ export interface EndpointConfig {
135
+ /** Port for HTTP-class transports. Omit for queue-only / cron-only endpoints. */
136
+ readonly port?: number;
137
+ /** Bind address. Default `0.0.0.0`. */
138
+ readonly host?: string;
139
+ /** Shutdown timing budgets. */
140
+ readonly shutdown?: ShutdownConfig;
141
+ /** Health probe configuration (a dedicated port; defaults to 9_400). */
142
+ readonly probes?: HealthConfig;
143
+ /** Show the boot banner on stdout. Default `true`. */
144
+ readonly banner?: boolean;
145
+ /** Test seam — skip process.exit() during shutdown. Default unset. */
146
+ readonly exitOnShutdown?: boolean;
147
+ }
148
+ export interface RunningEndpoint {
149
+ readonly name: string;
150
+ readonly server?: Server;
151
+ readonly lifecycle: LifecycleManager;
152
+ /** Trigger shutdown manually. Idempotent. */
153
+ shutdown(reason?: string): Promise<void>;
154
+ }
155
+ /**
156
+ * Build an endpoint. The returned builder is chainable: `.serve()` adds
157
+ * apps + interfaces in declaration order; `.run()` boots and listens.
158
+ *
159
+ * ```ts
160
+ * await endpoint("api", { port: 3000 })
161
+ * .serve(app) // optional — boots container first
162
+ * .serve(httpApi) // mounts an HTTP interface
163
+ * .serve(workers) // mounts a queue worker
164
+ * .run()
165
+ * ```
166
+ *
167
+ * For a foreign-framework integration:
168
+ *
169
+ * ```ts
170
+ * await endpoint("api", { port: 3000 })
171
+ * .serve(existingExpressApp)
172
+ * .run()
173
+ * ```
174
+ */
175
+ export declare function endpoint(name: string, config?: EndpointConfig): EndpointBuilder;
176
+ /** Internal builder; users get one via {@link endpoint}. */
177
+ export declare class EndpointBuilder {
178
+ private readonly name;
179
+ private readonly config;
180
+ private apps;
181
+ private nwireServables;
182
+ private foreignServables;
183
+ constructor(name: string, config: EndpointConfig);
184
+ /**
185
+ * Add something to serve. Multiple calls compose. Apps are booted first
186
+ * (any order between them is OK — boot order matters within an app, not
187
+ * across them), then Nwire interfaces and foreign apps are attached.
188
+ */
189
+ serve(target: Servable): EndpointBuilder;
190
+ /**
191
+ * Boot all served apps in order, then attach all served interfaces +
192
+ * foreign apps, then start listening. Returns a {@link RunningEndpoint}
193
+ * with handles for manual shutdown (useful in tests).
194
+ *
195
+ * The shutdown order is reverse: foreign apps stop accepting via their
196
+ * own server.close(), then Nwire interfaces shutdown(), then apps
197
+ * shutdown() in reverse boot order. The lifecycle manager wraps the
198
+ * whole sequence with K8s drain semantics.
199
+ */
200
+ run(): Promise<RunningEndpoint>;
201
+ }
202
+ /** True when the target is a Nwire interface (http/queue/etc) compiled for endpoint mount. */
203
+ export declare function isNwireServable(x: unknown): x is NwireServable;
204
+ /** True when the target is a Nwire app (container + lifecycle). */
205
+ export declare function isAppServable(x: unknown): x is AppServable;
206
+ /** True when the target looks like a framework app — has `.listen(port, ...)`. */
207
+ export declare function isForeignServable(x: unknown): x is ForeignServable;
208
+ /**
209
+ * Per-transport host factory shape (provided by interface packages like
210
+ * `@nwire/http`). The endpoint stays transport-agnostic by holding a
211
+ * reference to this through `_buildHost` and asking the transport to
212
+ * produce a Server when needed.
213
+ */
214
+ export interface HostFactory {
215
+ addCheck(check: HealthCheck): void;
216
+ listen(port: number, host: string): Promise<Server>;
217
+ }
218
+ //# sourceMappingURL=endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../src/endpoint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,376 @@
1
+ /**
2
+ * `endpoint()` — the canonical Nwire entry point for running a process.
3
+ *
4
+ * An endpoint is the outermost layer of a Nwire app. It owns:
5
+ *
6
+ * - the port (when listening for HTTP-class transports)
7
+ * - the signal handling (SIGTERM / SIGINT)
8
+ * - the graceful drain (http-terminator)
9
+ * - the K8s probes (lightship — startup / liveness / readiness)
10
+ * - the boot order (apps first, transports second)
11
+ * - the shutdown order (transports first, apps reverse-of-boot)
12
+ *
13
+ * The endpoint is the only Nwire layer that talks to the operating
14
+ * system. Everything else — apps, modules, interfaces, resolvers — is
15
+ * pure data + functions until an endpoint wraps them in a process.
16
+ *
17
+ * ## What can be served
18
+ *
19
+ * `endpoint(name, config).serve(target)` accepts any of:
20
+ *
21
+ * - A Nwire interface compiled to a Node server adapter (the common case)
22
+ * - An existing Express / Fastify / Koa / Nest app (the interop case)
23
+ * - A `@nwire/app` instance (boot order + container DI)
24
+ *
25
+ * Multiple `.serve()` calls compose: serve the app first to boot its
26
+ * container, then serve one or more interfaces that use that container.
27
+ *
28
+ * ## Why this exists
29
+ *
30
+ * Every framework eventually needs a process layer — something that knows
31
+ * about SIGTERM, drains in-flight requests, exposes `/healthz`, and binds
32
+ * to a port. Most frameworks bolt this on as a deployment afterthought.
33
+ * `@nwire/endpoint` makes it the first-class entry point so you don't
34
+ * write the same K8s-readiness code on every project. It works alone —
35
+ * install only this package and wrap any Node server.
36
+ */
37
+ import { attachLifecycle, } from "./lifecycle.js";
38
+ // ─── Builder API ──────────────────────────────────────────────────────────
39
+ /**
40
+ * Build an endpoint. The returned builder is chainable: `.serve()` adds
41
+ * apps + interfaces in declaration order; `.run()` boots and listens.
42
+ *
43
+ * ```ts
44
+ * await endpoint("api", { port: 3000 })
45
+ * .serve(app) // optional — boots container first
46
+ * .serve(httpApi) // mounts an HTTP interface
47
+ * .serve(workers) // mounts a queue worker
48
+ * .run()
49
+ * ```
50
+ *
51
+ * For a foreign-framework integration:
52
+ *
53
+ * ```ts
54
+ * await endpoint("api", { port: 3000 })
55
+ * .serve(existingExpressApp)
56
+ * .run()
57
+ * ```
58
+ */
59
+ export function endpoint(name, config = {}) {
60
+ return new EndpointBuilder(name, config);
61
+ }
62
+ /** Internal builder; users get one via {@link endpoint}. */
63
+ export class EndpointBuilder {
64
+ name;
65
+ config;
66
+ apps = [];
67
+ nwireServables = [];
68
+ foreignServables = [];
69
+ constructor(name, config) {
70
+ this.name = name;
71
+ this.config = config;
72
+ }
73
+ /**
74
+ * Add something to serve. Multiple calls compose. Apps are booted first
75
+ * (any order between them is OK — boot order matters within an app, not
76
+ * across them), then Nwire interfaces and foreign apps are attached.
77
+ */
78
+ serve(target) {
79
+ if (isAppServable(target)) {
80
+ this.apps.push(target);
81
+ }
82
+ else if (isNwireServable(target)) {
83
+ this.nwireServables.push(target);
84
+ }
85
+ else if (isForeignServable(target)) {
86
+ this.foreignServables.push(target);
87
+ }
88
+ else {
89
+ throw new Error(`endpoint("${this.name}").serve(): target is not a Nwire app, Nwire interface, or framework app.`);
90
+ }
91
+ return this;
92
+ }
93
+ /**
94
+ * Boot all served apps in order, then attach all served interfaces +
95
+ * foreign apps, then start listening. Returns a {@link RunningEndpoint}
96
+ * with handles for manual shutdown (useful in tests).
97
+ *
98
+ * The shutdown order is reverse: foreign apps stop accepting via their
99
+ * own server.close(), then Nwire interfaces shutdown(), then apps
100
+ * shutdown() in reverse boot order. The lifecycle manager wraps the
101
+ * whole sequence with K8s drain semantics.
102
+ */
103
+ async run() {
104
+ // 1. Boot apps in declaration order so transports can resolve from
105
+ // their containers from the first request.
106
+ for (const app of this.apps) {
107
+ await app.boot();
108
+ }
109
+ // Helper to dispatch a lifecycle event on every served app's bus.
110
+ // Apps that don't expose `dispatchFrameworkEvent` are silently
111
+ // skipped (L2 standalone case has no apps; observers register on
112
+ // the app's bus so there's nothing to dispatch to).
113
+ const fireLifecycle = async (eventName, payload) => {
114
+ let allowed = true;
115
+ for (const app of this.apps) {
116
+ if (app.dispatchFrameworkEvent) {
117
+ const ok = await app.dispatchFrameworkEvent(eventName, payload);
118
+ if (!ok)
119
+ allowed = false;
120
+ }
121
+ }
122
+ return allowed;
123
+ };
124
+ // The container the host hands to interfaces. If multiple apps were
125
+ // served, we use the first app's container as primary — interfaces
126
+ // can reach into others via `ctx.resolve` if those bindings exist.
127
+ // Most apps have one container; this case-edge is for BFFs.
128
+ //
129
+ // L2 path: no app served (`.serve(api)` on a bare httpInterface). The
130
+ // interface already has a container set via `.provide(...)`; we must
131
+ // NOT clobber it. Pass `undefined` so the interface's attach() can
132
+ // preserve its own provision.
133
+ const container = this.apps[0]?.container;
134
+ // 2. Start the operating-system listener. For HTTP-class endpoints we
135
+ // spin a single Node http.Server that hosts all NwireServables +
136
+ // any ForeignServable. Workers-only endpoints skip the listener
137
+ // entirely; the probes server is still available.
138
+ //
139
+ // Each `s.attach(...)` is wrapped with WireMounting (interceptable)
140
+ // and WireMounted (observable) so dev logger + Studio Live show
141
+ // every wire as it comes up.
142
+ let server;
143
+ if (this.config.port !== undefined &&
144
+ (this.nwireServables.length || this.foreignServables.length)) {
145
+ // Pre-attach hook: fire WireMounting for each interface BEFORE
146
+ // startListener does the actual attach.
147
+ for (const s of this.nwireServables) {
148
+ const ok = await fireLifecycle("nwire.wire.mounting", {
149
+ appName: this.apps[0]?.appName ?? this.name,
150
+ transport: s.transport,
151
+ manifest: undefined,
152
+ });
153
+ if (!ok) {
154
+ throw new Error(`endpoint("${this.name}").serve(): wire "${s.transport}" mount prevented by a WireMounting subscriber.`);
155
+ }
156
+ }
157
+ server = await startListener({
158
+ port: this.config.port,
159
+ host: this.config.host,
160
+ nwireServables: this.nwireServables,
161
+ foreignServables: this.foreignServables,
162
+ container,
163
+ });
164
+ // Post-attach: every wire is now attached. Fire WireMounted.
165
+ for (const s of this.nwireServables) {
166
+ await fireLifecycle("nwire.wire.mounted", {
167
+ appName: this.apps[0]?.appName ?? this.name,
168
+ transport: s.transport,
169
+ manifest: undefined,
170
+ });
171
+ }
172
+ }
173
+ else {
174
+ // Workers / queue / cron — attach NwireServables to a non-HTTP host.
175
+ for (const s of this.nwireServables) {
176
+ const ok = await fireLifecycle("nwire.wire.mounting", {
177
+ appName: this.apps[0]?.appName ?? this.name,
178
+ transport: s.transport,
179
+ manifest: undefined,
180
+ });
181
+ if (!ok) {
182
+ throw new Error(`endpoint("${this.name}").serve(): wire "${s.transport}" mount prevented by a WireMounting subscriber.`);
183
+ }
184
+ await s.attach({ container, addCheck: () => undefined });
185
+ await fireLifecycle("nwire.wire.mounted", {
186
+ appName: this.apps[0]?.appName ?? this.name,
187
+ transport: s.transport,
188
+ manifest: undefined,
189
+ });
190
+ }
191
+ }
192
+ // 3. Wrap the server (if any) with graceful shutdown + probes.
193
+ const lifecycle = await attachLifecycle({
194
+ server: server ?? makeProbeOnlyServer(),
195
+ shutdown: {
196
+ ...this.config.shutdown,
197
+ onShutdown: async () => {
198
+ // Fire WireUnmounted for each interface BEFORE its shutdown
199
+ // runs, so observers can record "wire stopping" with full
200
+ // context. Errors during dispatch are swallowed — shutdown
201
+ // must complete regardless.
202
+ for (let i = this.nwireServables.length - 1; i >= 0; i--) {
203
+ const s = this.nwireServables[i];
204
+ try {
205
+ await fireLifecycle("nwire.wire.unmounted", {
206
+ appName: this.apps[0]?.appName ?? this.name,
207
+ transport: s.transport,
208
+ });
209
+ }
210
+ catch {
211
+ /* swallow — shutdown must proceed */
212
+ }
213
+ }
214
+ // Interface shutdowns — let them disconnect cleanly.
215
+ for (let i = this.nwireServables.length - 1; i >= 0; i--) {
216
+ await this.nwireServables[i].shutdown?.();
217
+ }
218
+ // Apps shutdown in reverse boot order.
219
+ for (let i = this.apps.length - 1; i >= 0; i--) {
220
+ await this.apps[i].shutdown();
221
+ }
222
+ // User-supplied shutdown hook last, if any.
223
+ await this.config.shutdown?.onShutdown?.();
224
+ },
225
+ },
226
+ health: {
227
+ ...this.config.probes,
228
+ checks: [
229
+ ...(this.config.probes?.checks ?? []),
230
+ ...this.nwireServables.flatMap((s) => s.checks ?? []),
231
+ ],
232
+ },
233
+ exitOnShutdown: this.config.exitOnShutdown,
234
+ });
235
+ // 4. Register interface checks dynamically too — they may have been
236
+ // discovered during attach (e.g., queue probes once connected).
237
+ for (const s of this.nwireServables) {
238
+ for (const c of s.checks ?? [])
239
+ lifecycle.addCheck(c);
240
+ }
241
+ // 5. Flip readiness and print the banner. AppReady fires AFTER
242
+ // lightship marks /ready 200 — that's the moment the wire actually
243
+ // accepts traffic.
244
+ lifecycle.ready();
245
+ for (const app of this.apps) {
246
+ if (app.dispatchFrameworkEvent) {
247
+ await app.dispatchFrameworkEvent("nwire.app.ready", {
248
+ appName: app.appName ?? this.name,
249
+ readyAt: new Date().toISOString(),
250
+ });
251
+ }
252
+ }
253
+ if (this.config.banner !== false) {
254
+ // Only advertise probes if lightship actually bound. The bind
255
+ // can be skipped (probes disabled) or fail silently on
256
+ // EADDRINUSE (handled in attachLifecycle).
257
+ const probesBound = lifecycle.lightship !== undefined;
258
+ printBanner({
259
+ name: this.name,
260
+ port: this.config.port,
261
+ probePort: probesBound ? (this.config.probes?.port ?? 9_400) : undefined,
262
+ });
263
+ }
264
+ return {
265
+ name: this.name,
266
+ server,
267
+ lifecycle,
268
+ shutdown: (reason = "manual") => lifecycle.shutdown(reason),
269
+ };
270
+ }
271
+ }
272
+ // ─── Type guards ──────────────────────────────────────────────────────────
273
+ /** True when the target is a Nwire interface (http/queue/etc) compiled for endpoint mount. */
274
+ export function isNwireServable(x) {
275
+ return (typeof x === "object" &&
276
+ x !== null &&
277
+ x.$nwireServable === true);
278
+ }
279
+ /** True when the target is a Nwire app (container + lifecycle). */
280
+ export function isAppServable(x) {
281
+ return typeof x === "object" && x !== null && x.$nwireApp === true;
282
+ }
283
+ /** True when the target looks like a framework app — has `.listen(port, ...)`. */
284
+ export function isForeignServable(x) {
285
+ return (typeof x === "object" && x !== null && typeof x.listen === "function");
286
+ }
287
+ // ─── Internal helpers ─────────────────────────────────────────────────────
288
+ /**
289
+ * Start the operating-system listener. When multiple NwireServables share
290
+ * a port, this composes them onto a single http.Server. ForeignServables
291
+ * use their own `.listen()` and the resulting Server is what gets wrapped
292
+ * with lifecycle — meaning today only ONE ForeignServable per endpoint is
293
+ * supported (more would require an internal multiplexer). The 99% case
294
+ * is fine; multi-foreign on one port is exotic and you'd build it yourself.
295
+ */
296
+ async function startListener(opts) {
297
+ if (opts.foreignServables.length > 1) {
298
+ throw new Error(`endpoint: more than one foreign-framework target on one port is not supported; ` +
299
+ `mount them separately or compose them inside the framework first.`);
300
+ }
301
+ // Case A — foreign app owns the server. Nwire interfaces attach onto it
302
+ // via to* adapters (toExpress, toKoa) the user calls themselves before
303
+ // serving the framework. The endpoint just wraps the resulting Server.
304
+ if (opts.foreignServables.length === 1) {
305
+ const foreign = opts.foreignServables[0];
306
+ const server = await new Promise((resolve) => {
307
+ const s = foreign.listen(opts.port, opts.host ?? "0.0.0.0", () => resolve(s));
308
+ });
309
+ // NwireServables attached to a foreign endpoint must register
310
+ // themselves separately — there's no way for us to mount them onto
311
+ // the foreign framework's router automatically without knowing the
312
+ // framework. Future: detect Express/Fastify/Koa and auto-mount.
313
+ for (const s of opts.nwireServables) {
314
+ await s.attach({ container: opts.container, addCheck: () => undefined });
315
+ }
316
+ return server;
317
+ }
318
+ // Case B — Nwire interfaces only. Build a host that they attach to.
319
+ // For now we delegate to the first servable's transport-specific host
320
+ // builder. Multi-transport on one port (REST + GraphQL + WS) requires
321
+ // a more elaborate host; that's @nwire/http's job.
322
+ if (opts.nwireServables.length === 0) {
323
+ throw new Error("endpoint: nothing to listen on (no Nwire interfaces, no foreign app)");
324
+ }
325
+ // Lean fallback: assume the first servable carries an internal listener
326
+ // factory. The richer multi-transport host lives in @nwire/http.
327
+ const primary = opts.nwireServables[0];
328
+ const buildHost = primary._buildHost;
329
+ const hostFactory = buildHost ? buildHost.bind(primary) : undefined;
330
+ if (!hostFactory) {
331
+ throw new Error(`endpoint: interface "${primary.transport}" does not expose _buildHost(); ` +
332
+ `it cannot be the primary listener. Install a transport package that supports it.`);
333
+ }
334
+ const built = hostFactory();
335
+ for (const s of opts.nwireServables) {
336
+ await s.attach({ container: opts.container, addCheck: built.addCheck });
337
+ }
338
+ return await built.listen(opts.port, opts.host ?? "0.0.0.0");
339
+ }
340
+ /** Probe-only server for workers/cron — nothing on the data port. */
341
+ function makeProbeOnlyServer() {
342
+ // Lazy import keeps the cold-path module load out of the hot start.
343
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
344
+ const http = require("node:http");
345
+ const srv = http.createServer((_req, res) => {
346
+ res.statusCode = 404;
347
+ res.end();
348
+ });
349
+ // We don't actually .listen() this; attachLifecycle uses it only as
350
+ // the http-terminator target. Workers have no data port to drain;
351
+ // the probe port (lightship) is separate.
352
+ return srv;
353
+ }
354
+ // `emptyContainer()` was previously used when no app was served — but
355
+ // it throws on every resolve, which clobbered the interface's own
356
+ // `.provide()` container. The endpoint now passes `undefined` upward
357
+ // and lets the interface preserve its own provision (the L2 path).
358
+ /** Boot-time banner — friendly, informative, not chatty. */
359
+ function printBanner(opts) {
360
+ const lines = [
361
+ `nwire endpoint "${opts.name}" listening`,
362
+ opts.port !== undefined
363
+ ? ` data: http://0.0.0.0:${opts.port}`
364
+ : ` data: (no HTTP listener)`,
365
+ ];
366
+ if (opts.probePort !== undefined) {
367
+ // lightship's actual paths — not /readyz + /healthz (which was a
368
+ // historical misnaming in our docs).
369
+ lines.push(` probes: http://0.0.0.0:${opts.probePort}/live · /ready`);
370
+ }
371
+ else {
372
+ lines.push(` probes: (disabled — port already in use or probes disabled)`);
373
+ }
374
+ console.log(lines.join("\n"));
375
+ }
376
+ //# sourceMappingURL=endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../src/endpoint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Graceful runtime lifecycle — boot + readiness + shutdown orchestration.
3
+ *
4
+ * Composes two battle-tested libraries:
5
+ *
6
+ * - **http-terminator** — properly drains keep-alive connections during
7
+ * `server.close()`. Without it, in-flight requests on persistent
8
+ * connections silently die when the process exits.
9
+ * - **lightship** — Kubernetes-style readiness/liveness probes on a
10
+ * separate operational port. Flips `/readyz` to 503 the moment shutdown
11
+ * starts so the load balancer stops sending new traffic — the single
12
+ * most important step in a zero-downtime rolling deploy.
13
+ *
14
+ * Sequence on SIGTERM/SIGINT:
15
+ *
16
+ * 1. lightship: /readyz → 503 (LB removes us from rotation)
17
+ * 2. wait `drainDelay` (LB has time to catch up; default 10s)
18
+ * 3. stop accepting new connections (http-terminator)
19
+ * 4. drain in-flight requests (http-terminator; bounded by drainTimeout)
20
+ * 5. run user `onShutdown` hooks (close DBs, Redis, queues — in reverse boot order)
21
+ * 6. flush logs, exit 0 (or 1 if hardTimeout breached → SIGKILL)
22
+ *
23
+ * The whole sequence is bounded by `hardTimeout` — if shutdown takes
24
+ * longer, the process self-kills so the orchestrator doesn't wait forever.
25
+ *
26
+ * This module is intentionally framework-agnostic: it takes a Node `Server`
27
+ * and wraps it. Use it with any HTTP framework — Express, Fastify, Koa, Nest,
28
+ * Hono, or a raw `http.createServer` listener. The high-level
29
+ * `endpoint(name, config).serve(target).run()` wrapper composes this with
30
+ * server-construction for the common cases.
31
+ */
32
+ import type { Server } from "node:http";
33
+ import { type Lightship } from "lightship";
34
+ /**
35
+ * A single health probe. Each check has a name, a function returning
36
+ * void-or-promise, and an optional timeout. Liveness checks kill the pod;
37
+ * readiness checks only flip the load-balancer signal.
38
+ */
39
+ export interface HealthCheck {
40
+ readonly name: string;
41
+ readonly check: () => Promise<void> | void;
42
+ /** Per-check timeout in ms. Default 5_000. */
43
+ readonly timeout?: number;
44
+ /**
45
+ * Marks the check as `liveness` (kills the pod when it fails) vs the
46
+ * default `readiness` (just removes from rotation). Liveness checks
47
+ * should be cheap and only fail when the process is genuinely broken.
48
+ */
49
+ readonly kind?: "readiness" | "liveness";
50
+ }
51
+ /** Factory for a HealthCheck. Use to register checks from inside plugins/providers. */
52
+ export declare function defineCheck(name: string, check: () => Promise<void> | void, opts?: Omit<HealthCheck, "name" | "check">): HealthCheck;
53
+ /** Timing budgets for graceful shutdown. All in milliseconds. */
54
+ export interface ShutdownConfig {
55
+ /** ms to wait after marking not-ready before stopping listen. Default 10_000. */
56
+ readonly drainDelay?: number;
57
+ /** ms to wait for in-flight requests to finish. Default 30_000. */
58
+ readonly drainTimeout?: number;
59
+ /** ms total budget before SIGKILL. Default 45_000. */
60
+ readonly hardTimeout?: number;
61
+ /**
62
+ * Run-after-drain hook — close DBs, Redis, queues, etc. The endpoint
63
+ * wires this to plugin/provider shutdown in reverse boot order when an
64
+ * app is served.
65
+ */
66
+ readonly onShutdown?: () => Promise<void> | void;
67
+ }
68
+ /** Operational HTTP server config — exposes readiness/liveness probes. */
69
+ export interface HealthConfig {
70
+ /** Disable lightship entirely. Default `true` (enabled). */
71
+ readonly enabled?: boolean;
72
+ /**
73
+ * Port for the operational endpoints. Default `9_400`.
74
+ *
75
+ * Moved off the historic `9_000` because that's claimed by MinIO's S3
76
+ * API (and several other common dev services). `9_400` is in a quiet
77
+ * IANA range and doesn't collide with anything in our docker-compose.
78
+ */
79
+ readonly port?: number;
80
+ /**
81
+ * **Documentation-only** — lightship hardcodes its endpoints to `/live`
82
+ * and `/ready`; these fields are accepted for API back-compat but DO
83
+ * NOT actually change the paths. Track lightship upstream for a fix
84
+ * (or replace lightship with a custom probe server if path customization
85
+ * becomes important enough to own).
86
+ */
87
+ readonly livenessPath?: string;
88
+ /** See `livenessPath` — documentation-only. Lightship serves `/ready`. */
89
+ readonly readinessPath?: string;
90
+ readonly checks?: readonly HealthCheck[];
91
+ }
92
+ /** What `attachLifecycle()` returns — control surface for tests and dynamic checks. */
93
+ export interface LifecycleManager {
94
+ /** Mark the process ready — call once the HTTP server is listening. */
95
+ ready(): void;
96
+ /**
97
+ * Run the full shutdown sequence: flip readiness → drain → close →
98
+ * onShutdown → exit. Idempotent; subsequent calls are no-ops.
99
+ */
100
+ shutdown(reason: string): Promise<void>;
101
+ /**
102
+ * Add a health check after the lifecycle was created — used by
103
+ * providers/plugins to register their own probes.
104
+ */
105
+ addCheck(check: HealthCheck): void;
106
+ /** The lightship instance, if enabled — for advanced/test access. */
107
+ readonly lightship?: Lightship;
108
+ }
109
+ /**
110
+ * Attach the graceful-shutdown lifecycle to a running HTTP server.
111
+ *
112
+ * Returns a `LifecycleManager` whose `shutdown()` flips readiness,
113
+ * drains the server, runs `onShutdown`, and exits. Wires `SIGTERM` and
114
+ * `SIGINT` to `shutdown()` automatically (via `process.once(...)`, so
115
+ * manual `shutdown()` calls during tests don't fight with signal-driven ones).
116
+ */
117
+ export declare function attachLifecycle(opts: {
118
+ readonly server: Server;
119
+ readonly shutdown?: ShutdownConfig;
120
+ readonly health?: HealthConfig;
121
+ /** Test seam — set to false to skip process.exit() at the end. */
122
+ readonly exitOnShutdown?: boolean;
123
+ }): Promise<LifecycleManager>;
124
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3C,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;CAC1C;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EACjC,IAAI,GAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAM,GAC7C,WAAW,CAEb;AAED,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,sDAAsD;IACtD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAClD;AAED,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C;AAED,uFAAuF;AACvF,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,KAAK,IAAI,IAAI,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IACnC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgJ5B"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Graceful runtime lifecycle — boot + readiness + shutdown orchestration.
3
+ *
4
+ * Composes two battle-tested libraries:
5
+ *
6
+ * - **http-terminator** — properly drains keep-alive connections during
7
+ * `server.close()`. Without it, in-flight requests on persistent
8
+ * connections silently die when the process exits.
9
+ * - **lightship** — Kubernetes-style readiness/liveness probes on a
10
+ * separate operational port. Flips `/readyz` to 503 the moment shutdown
11
+ * starts so the load balancer stops sending new traffic — the single
12
+ * most important step in a zero-downtime rolling deploy.
13
+ *
14
+ * Sequence on SIGTERM/SIGINT:
15
+ *
16
+ * 1. lightship: /readyz → 503 (LB removes us from rotation)
17
+ * 2. wait `drainDelay` (LB has time to catch up; default 10s)
18
+ * 3. stop accepting new connections (http-terminator)
19
+ * 4. drain in-flight requests (http-terminator; bounded by drainTimeout)
20
+ * 5. run user `onShutdown` hooks (close DBs, Redis, queues — in reverse boot order)
21
+ * 6. flush logs, exit 0 (or 1 if hardTimeout breached → SIGKILL)
22
+ *
23
+ * The whole sequence is bounded by `hardTimeout` — if shutdown takes
24
+ * longer, the process self-kills so the orchestrator doesn't wait forever.
25
+ *
26
+ * This module is intentionally framework-agnostic: it takes a Node `Server`
27
+ * and wraps it. Use it with any HTTP framework — Express, Fastify, Koa, Nest,
28
+ * Hono, or a raw `http.createServer` listener. The high-level
29
+ * `endpoint(name, config).serve(target).run()` wrapper composes this with
30
+ * server-construction for the common cases.
31
+ */
32
+ import { createHttpTerminator } from "http-terminator";
33
+ import { createLightship } from "lightship";
34
+ /** Factory for a HealthCheck. Use to register checks from inside plugins/providers. */
35
+ export function defineCheck(name, check, opts = {}) {
36
+ return { name, check, ...opts };
37
+ }
38
+ /**
39
+ * Attach the graceful-shutdown lifecycle to a running HTTP server.
40
+ *
41
+ * Returns a `LifecycleManager` whose `shutdown()` flips readiness,
42
+ * drains the server, runs `onShutdown`, and exits. Wires `SIGTERM` and
43
+ * `SIGINT` to `shutdown()` automatically (via `process.once(...)`, so
44
+ * manual `shutdown()` calls during tests don't fight with signal-driven ones).
45
+ */
46
+ export async function attachLifecycle(opts) {
47
+ const shutdownCfg = {
48
+ drainDelay: opts.shutdown?.drainDelay ?? 10_000,
49
+ drainTimeout: opts.shutdown?.drainTimeout ?? 30_000,
50
+ hardTimeout: opts.shutdown?.hardTimeout ?? 45_000,
51
+ onShutdown: opts.shutdown?.onShutdown ?? (() => undefined),
52
+ };
53
+ const healthCfg = {
54
+ enabled: opts.health?.enabled ?? true,
55
+ port: opts.health?.port ?? 9_400,
56
+ livenessPath: opts.health?.livenessPath ?? "/healthz",
57
+ readinessPath: opts.health?.readinessPath ?? "/readyz",
58
+ checks: opts.health?.checks ?? [],
59
+ };
60
+ const terminator = createHttpTerminator({
61
+ server: opts.server,
62
+ gracefulTerminationTimeout: shutdownCfg.drainTimeout,
63
+ });
64
+ /**
65
+ * Lightship binds its own operational HTTP server on a separate port.
66
+ * It's the right move — operational concerns shouldn't share rate
67
+ * limits / middleware with app traffic. `createLightship` is async
68
+ * because it binds to a port.
69
+ */
70
+ let lightship;
71
+ if (healthCfg.enabled) {
72
+ try {
73
+ lightship = await createLightship({
74
+ port: healthCfg.port,
75
+ detectKubernetes: false, // we own the lifecycle; don't auto-detect
76
+ shutdownDelay: shutdownCfg.drainDelay,
77
+ shutdownHandlerTimeout: shutdownCfg.drainTimeout,
78
+ gracefulShutdownTimeout: shutdownCfg.hardTimeout,
79
+ });
80
+ }
81
+ catch (err) {
82
+ // EADDRINUSE on the probe port is the common dev case (another
83
+ // dev process, MinIO on 9_000, etc). Don't crash the data port —
84
+ // log clearly and run without lightship. K8s probes will fail
85
+ // until the user moves the port; that's the right signal.
86
+ const code = err.code;
87
+ if (code === "EADDRINUSE") {
88
+ console.warn(`[lifecycle] probe port :${healthCfg.port} already in use — probes disabled. ` +
89
+ `Override with \`probes: { port: <other> }\` on endpoint().`);
90
+ }
91
+ else {
92
+ throw err;
93
+ }
94
+ }
95
+ }
96
+ const dynamicChecks = [...healthCfg.checks];
97
+ let shuttingDown = false;
98
+ /** Run every registered check; returns true only if all pass. */
99
+ const runChecks = async () => {
100
+ if (!lightship)
101
+ return true;
102
+ for (const c of dynamicChecks) {
103
+ const timeout = c.timeout ?? 5_000;
104
+ try {
105
+ await Promise.race([
106
+ Promise.resolve(c.check()),
107
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`check '${c.name}' timed out after ${timeout}ms`)), timeout)),
108
+ ]);
109
+ }
110
+ catch (err) {
111
+ console.error(`[lifecycle] readiness check '${c.name}' failed:`, err.message);
112
+ lightship.signalNotReady();
113
+ return false;
114
+ }
115
+ }
116
+ return true;
117
+ };
118
+ const ready = () => {
119
+ if (!lightship)
120
+ return;
121
+ void runChecks().then((ok) => {
122
+ if (!shuttingDown && ok)
123
+ lightship.signalReady();
124
+ });
125
+ };
126
+ const shutdown = async (reason) => {
127
+ if (shuttingDown)
128
+ return;
129
+ shuttingDown = true;
130
+ console.log(`[lifecycle] shutdown initiated (${reason})`);
131
+ // Hard timeout — last-ditch SIGKILL if anything below hangs.
132
+ const hardKill = setTimeout(() => {
133
+ console.error(`[lifecycle] hard timeout (${shutdownCfg.hardTimeout}ms) — forcing exit`);
134
+ if (opts.exitOnShutdown !== false)
135
+ process.exit(1);
136
+ }, shutdownCfg.hardTimeout);
137
+ hardKill.unref(); // don't keep the loop alive on its own
138
+ try {
139
+ if (lightship) {
140
+ lightship.signalNotReady();
141
+ console.log(`[lifecycle] /readyz → 503; waiting ${shutdownCfg.drainDelay}ms for LB to catch up`);
142
+ }
143
+ await new Promise((r) => setTimeout(r, shutdownCfg.drainDelay));
144
+ console.log(`[lifecycle] draining in-flight requests`);
145
+ await terminator.terminate();
146
+ console.log(`[lifecycle] running onShutdown`);
147
+ await shutdownCfg.onShutdown();
148
+ if (lightship) {
149
+ await lightship.shutdown();
150
+ }
151
+ console.log(`[lifecycle] shutdown complete`);
152
+ }
153
+ catch (err) {
154
+ console.error(`[lifecycle] shutdown error:`, err);
155
+ }
156
+ finally {
157
+ clearTimeout(hardKill);
158
+ if (opts.exitOnShutdown !== false) {
159
+ setImmediate(() => process.exit(0));
160
+ }
161
+ }
162
+ };
163
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
164
+ process.once("SIGINT", () => void shutdown("SIGINT"));
165
+ return {
166
+ ready,
167
+ shutdown,
168
+ addCheck: (c) => {
169
+ dynamicChecks.push(c);
170
+ },
171
+ get lightship() {
172
+ return lightship;
173
+ },
174
+ };
175
+ }
176
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@nwire/endpoint",
3
+ "version": "0.7.0",
4
+ "description": "Nwire — production process lifecycle. Wraps any Node server (Express, Fastify, Koa, Nest, Nwire interfaces) with K8s-grade graceful shutdown, http-terminator drain, and lightship readiness/liveness probes. Standalone — no framework dependency beyond @nwire/container.",
5
+ "keywords": [
6
+ "endpoint",
7
+ "graceful-shutdown",
8
+ "health-check",
9
+ "k8s",
10
+ "kubernetes",
11
+ "lifecycle",
12
+ "nwire"
13
+ ],
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/endpoint-index.js",
20
+ "types": "./dist/endpoint-index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/endpoint-index.js",
24
+ "types": "./dist/endpoint-index.d.ts"
25
+ }
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "http-terminator": "^3.2.0",
32
+ "lightship": "^9.0.4",
33
+ "@nwire/container": "0.7.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.19.9",
37
+ "typescript": "^5.9.3",
38
+ "vitest": "^4.0.18"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
42
+ "dev": "tsc --watch",
43
+ "typecheck": "tsc --noEmit"
44
+ }
45
+ }