@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 +21 -0
- package/README.md +56 -0
- package/dist/__tests__/endpoint.test.d.ts +14 -0
- package/dist/__tests__/endpoint.test.d.ts.map +1 -0
- package/dist/__tests__/endpoint.test.js +95 -0
- package/dist/__tests__/endpoint.test.js.map +1 -0
- package/dist/endpoint-index.d.ts +24 -0
- package/dist/endpoint-index.d.ts.map +1 -0
- package/dist/endpoint-index.js +24 -0
- package/dist/endpoint-index.js.map +1 -0
- package/dist/endpoint.d.ts +218 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +376 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/lifecycle.d.ts +124 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +176 -0
- package/dist/lifecycle.js.map +1 -0
- package/package.json +45 -0
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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"}
|
package/dist/endpoint.js
ADDED
|
@@ -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
|
+
}
|