@nwire/endpoint 0.10.1 → 0.11.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/README.md +9 -12
- package/dist/adapter.d.ts +25 -0
- package/dist/endpoint-index.d.ts +1 -1
- package/dist/endpoint.d.ts +26 -4
- package/dist/endpoint.js +38 -8
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -18,10 +18,7 @@ import { httpKoa } from "@nwire/koa";
|
|
|
18
18
|
const app = createApp({ appName: "api" });
|
|
19
19
|
// ... app.wire(...) wires here ...
|
|
20
20
|
|
|
21
|
-
await endpoint("api", { port: 3000 })
|
|
22
|
-
.use(httpKoa())
|
|
23
|
-
.mount(app)
|
|
24
|
-
.run();
|
|
21
|
+
await endpoint("api", { port: 3000 }).use(httpKoa()).mount(app).run();
|
|
25
22
|
```
|
|
26
23
|
|
|
27
24
|
`.use(adopter)` installs a transport runtime — `httpKoa`, `expressAdapter`,
|
|
@@ -70,12 +67,12 @@ through the runtime when forge is in play, or directly otherwise.
|
|
|
70
67
|
|
|
71
68
|
## Lifecycle
|
|
72
69
|
|
|
73
|
-
| Stage
|
|
74
|
-
|
|
|
75
|
-
| `boot`
|
|
76
|
-
| `serve`
|
|
77
|
-
| `drain`
|
|
78
|
-
| `close`
|
|
70
|
+
| Stage | What runs |
|
|
71
|
+
| ------- | ------------------------------------------------------ |
|
|
72
|
+
| `boot` | App plugins boot; adopters consume wires; probes open. |
|
|
73
|
+
| `serve` | Adopters listen / subscribe; readiness flips green. |
|
|
74
|
+
| `drain` | SIGTERM/SIGINT: stop accepting new work, finish open. |
|
|
75
|
+
| `close` | Adopters shut down in reverse order; probes close. |
|
|
79
76
|
|
|
80
77
|
`endpoint()` returns a `RunningEndpoint` from `.run()`. Tests call
|
|
81
78
|
`running.shutdown("test")` to skip the SIGTERM dance.
|
|
@@ -84,10 +81,10 @@ through the runtime when forge is in play, or directly otherwise.
|
|
|
84
81
|
|
|
85
82
|
```ts
|
|
86
83
|
endpoint("api", {
|
|
87
|
-
banner: true,
|
|
84
|
+
banner: true, // boot banner on stdout
|
|
88
85
|
probes: { port: 9_400, enabled: true },
|
|
89
86
|
shutdown: { drainTimeout: 30_000, hardTimeout: 5_000 },
|
|
90
|
-
exitOnShutdown: true,
|
|
87
|
+
exitOnShutdown: true, // tests pass false
|
|
91
88
|
});
|
|
92
89
|
```
|
|
93
90
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { Wire } from "@nwire/wires";
|
|
13
13
|
import type { Container } from "@nwire/container";
|
|
14
14
|
import type { Logger } from "@nwire/logger";
|
|
15
|
+
import type { OutboundStage } from "@nwire/runtime";
|
|
15
16
|
import type { HealthCheck } from "./lifecycle.js";
|
|
16
17
|
export interface AdapterBootContext {
|
|
17
18
|
/**
|
|
@@ -38,15 +39,39 @@ export interface AdapterBootContext {
|
|
|
38
39
|
readonly port?: number;
|
|
39
40
|
/** Endpoint-level host hint, paired with `port`. */
|
|
40
41
|
readonly host?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Install an outbound pipeline stage on the mounted App's runtime.
|
|
44
|
+
* Outbound adapters (queuePublisher, natsPublisher, webhookSink, …) call
|
|
45
|
+
* this at boot to install their terminal stage; inbound adapters ignore
|
|
46
|
+
* it. Undefined when no App is mounted — the adapter is free to skip
|
|
47
|
+
* or fall back to a foreign-host integration.
|
|
48
|
+
*/
|
|
49
|
+
readonly installSinkStage?: (stage: OutboundStage) => void;
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
52
|
* Structural shape every transport adopter satisfies. Adopters never
|
|
44
53
|
* extend a base class — the contract is duck-typed at the endpoint.
|
|
54
|
+
*
|
|
55
|
+
* `direction` distinguishes the two membrane roles:
|
|
56
|
+
*
|
|
57
|
+
* - "inbound" — transport → execute. HTTP server, queue listener, cron.
|
|
58
|
+
* These consume wires and translate inbound traffic to
|
|
59
|
+
* `app.execute(handler, input)`.
|
|
60
|
+
*
|
|
61
|
+
* - "outbound" — sink → transport. Queue publisher, NATS publisher,
|
|
62
|
+
* webhook delivery, OTLP exporter. These install a
|
|
63
|
+
* terminal sink stage at boot and translate published
|
|
64
|
+
* events into transport messages.
|
|
65
|
+
*
|
|
66
|
+
* Optional, defaulting to "inbound" so adapters that haven't declared
|
|
67
|
+
* direction continue to behave as inbound transports.
|
|
45
68
|
*/
|
|
46
69
|
export interface Adapter {
|
|
47
70
|
readonly $kind: "adapter";
|
|
48
71
|
/** Adopter-kind tag — matches `binding.$adapter` on the wires it consumes. */
|
|
49
72
|
readonly kind: string;
|
|
73
|
+
/** Membrane direction. Defaults to "inbound" if absent. */
|
|
74
|
+
readonly direction?: "inbound" | "outbound";
|
|
50
75
|
boot(opts: AdapterBootContext): Promise<void>;
|
|
51
76
|
shutdown(reason?: string): Promise<void>;
|
|
52
77
|
/**
|
package/dist/endpoint-index.d.ts
CHANGED
|
@@ -19,6 +19,6 @@
|
|
|
19
19
|
* (`http-terminator` + `lightship`). The high-level builder is what most
|
|
20
20
|
* projects reach for; the low-level form is for adapters and tests.
|
|
21
21
|
*/
|
|
22
|
-
export { endpoint, EndpointBuilder, isAppServable, isForeignServable, type AppServable, type EndpointConfig, type ForeignServable, type RunningEndpoint, type Servable, } from "./endpoint.js";
|
|
22
|
+
export { endpoint, EndpointBuilder, isAppServable, isForeignServable, type AppServable, type EndpointConfig, type ProcessConfig, type ForeignServable, type RunningEndpoint, type Servable, } from "./endpoint.js";
|
|
23
23
|
export { attachLifecycle, defineCheck, type HealthCheck, type HealthConfig, type LifecycleManager, type ShutdownConfig, } from "./lifecycle.js";
|
|
24
24
|
export { isAdapter, type Adapter, type AdapterBootContext } from "./adapter.js";
|
package/dist/endpoint.d.ts
CHANGED
|
@@ -90,7 +90,12 @@ export interface ForeignServable {
|
|
|
90
90
|
export interface AppServable {
|
|
91
91
|
readonly $nwireApp: true;
|
|
92
92
|
readonly container: Container;
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Boot the app. Optional `appConfig` — when present, bound on the app's
|
|
95
|
+
* container as "config" so plugin boot callbacks can read it (foundation
|
|
96
|
+
* §19). Endpoint threads what was passed to `.mount(app, appConfig)`.
|
|
97
|
+
*/
|
|
98
|
+
boot(appConfig?: unknown): Promise<void>;
|
|
94
99
|
shutdown(): Promise<void>;
|
|
95
100
|
/**
|
|
96
101
|
* Optional — app name used in lifecycle event payloads. Defaults to
|
|
@@ -105,6 +110,12 @@ export interface AppServable {
|
|
|
105
110
|
*/
|
|
106
111
|
dispatchFrameworkEvent?(eventName: string, payload: unknown): Promise<boolean>;
|
|
107
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Process-level configuration for an endpoint. Owns OS-shaped concerns
|
|
115
|
+
* only: port, bind address, signal/drain budgets, probe port, banner
|
|
116
|
+
* toggle. App-level configuration (database URLs, API keys, feature
|
|
117
|
+
* flags) goes through `.mount(app, appConfig)`.
|
|
118
|
+
*/
|
|
108
119
|
export interface EndpointConfig {
|
|
109
120
|
/** Port for HTTP-class transports. Omit for queue-only / cron-only endpoints. */
|
|
110
121
|
readonly port?: number;
|
|
@@ -119,6 +130,8 @@ export interface EndpointConfig {
|
|
|
119
130
|
/** Test seam — skip process.exit() during shutdown. Default unset. */
|
|
120
131
|
readonly exitOnShutdown?: boolean;
|
|
121
132
|
}
|
|
133
|
+
/** Foundation §19 alias — `endpoint("name", processConfig)` reads as intent. */
|
|
134
|
+
export type ProcessConfig = EndpointConfig;
|
|
122
135
|
export interface RunningEndpoint {
|
|
123
136
|
readonly name: string;
|
|
124
137
|
readonly server?: Server;
|
|
@@ -162,6 +175,8 @@ export declare class EndpointBuilder {
|
|
|
162
175
|
private foreignServables;
|
|
163
176
|
private adapters;
|
|
164
177
|
private mounted;
|
|
178
|
+
/** App config threaded into `app.boot(...)` at `run()`. */
|
|
179
|
+
private mountedAppConfig;
|
|
165
180
|
/**
|
|
166
181
|
* Per-phase lifecycle hooks. Created lazily at builder-construction so
|
|
167
182
|
* they appear in `listHooks()` (and `.nwire/hooks.json` after scan)
|
|
@@ -192,10 +207,17 @@ export declare class EndpointBuilder {
|
|
|
192
207
|
use(adopter: Adapter): EndpointBuilder;
|
|
193
208
|
/**
|
|
194
209
|
* Mount the source whose wires the adopters consume. One source per
|
|
195
|
-
* endpoint — App or standalone Interface. For multi-app
|
|
196
|
-
* compose via `appCompose(...)` from `@nwire/app` and
|
|
210
|
+
* endpoint — an App or a standalone Interface. For multi-app
|
|
211
|
+
* topologies, compose via `appCompose(...)` from `@nwire/app` and
|
|
212
|
+
* mount the result.
|
|
213
|
+
*
|
|
214
|
+
* `appConfig` (when mounting an App) is threaded to `app.boot(appConfig)`
|
|
215
|
+
* at `.run()` time. Plugin boot callbacks read it via
|
|
216
|
+
* `container.resolve("config")`. The process/app config split:
|
|
217
|
+
* process config goes to `endpoint(name, processConfig)`, app config
|
|
218
|
+
* goes here.
|
|
197
219
|
*/
|
|
198
|
-
mount(source: WireSource): EndpointBuilder;
|
|
220
|
+
mount(source: WireSource, appConfig?: unknown): EndpointBuilder;
|
|
199
221
|
/**
|
|
200
222
|
* Boot all served apps in order, then attach all served interfaces +
|
|
201
223
|
* foreign apps, then start listening. Returns a {@link RunningEndpoint}
|
package/dist/endpoint.js
CHANGED
|
@@ -81,6 +81,8 @@ export class EndpointBuilder {
|
|
|
81
81
|
// ─── New-shape state (.use / .mount) ─────────────────────────────────
|
|
82
82
|
adapters = [];
|
|
83
83
|
mounted;
|
|
84
|
+
/** App config threaded into `app.boot(...)` at `run()`. */
|
|
85
|
+
mountedAppConfig = undefined;
|
|
84
86
|
/**
|
|
85
87
|
* Per-phase lifecycle hooks. Created lazily at builder-construction so
|
|
86
88
|
* they appear in `listHooks()` (and `.nwire/hooks.json` after scan)
|
|
@@ -146,10 +148,17 @@ export class EndpointBuilder {
|
|
|
146
148
|
}
|
|
147
149
|
/**
|
|
148
150
|
* Mount the source whose wires the adopters consume. One source per
|
|
149
|
-
* endpoint — App or standalone Interface. For multi-app
|
|
150
|
-
* compose via `appCompose(...)` from `@nwire/app` and
|
|
151
|
+
* endpoint — an App or a standalone Interface. For multi-app
|
|
152
|
+
* topologies, compose via `appCompose(...)` from `@nwire/app` and
|
|
153
|
+
* mount the result.
|
|
154
|
+
*
|
|
155
|
+
* `appConfig` (when mounting an App) is threaded to `app.boot(appConfig)`
|
|
156
|
+
* at `.run()` time. Plugin boot callbacks read it via
|
|
157
|
+
* `container.resolve("config")`. The process/app config split:
|
|
158
|
+
* process config goes to `endpoint(name, processConfig)`, app config
|
|
159
|
+
* goes here.
|
|
151
160
|
*/
|
|
152
|
-
mount(source) {
|
|
161
|
+
mount(source, appConfig) {
|
|
153
162
|
if (this.mounted) {
|
|
154
163
|
throw new Error(`endpoint("${this.name}").mount(): a source is already mounted ("${"appName" in this.mounted ? this.mounted.appName : "interface"}"). ` +
|
|
155
164
|
`One source per endpoint — use appCompose(...) from "@nwire/app" to compose multiple apps.`);
|
|
@@ -158,6 +167,7 @@ export class EndpointBuilder {
|
|
|
158
167
|
throw new Error(`endpoint("${this.name}").mount(): expected an App with .interface, or a standalone Interface.`);
|
|
159
168
|
}
|
|
160
169
|
this.mounted = source;
|
|
170
|
+
this.mountedAppConfig = appConfig;
|
|
161
171
|
// If mounting an App, also boot it via the existing apps array.
|
|
162
172
|
if (isAppWithInterface(source) && !this.apps.includes(source)) {
|
|
163
173
|
this.apps.push(source);
|
|
@@ -200,7 +210,15 @@ export class EndpointBuilder {
|
|
|
200
210
|
startedAt: new Date().toISOString(),
|
|
201
211
|
});
|
|
202
212
|
for (const app of this.apps) {
|
|
203
|
-
|
|
213
|
+
// Pass appConfig only to the mounted app — appCompose sub-apps don't
|
|
214
|
+
// carry their own per-app config in this path.
|
|
215
|
+
const appConfig = this.mounted && app === this.mounted ? this.mountedAppConfig : undefined;
|
|
216
|
+
if (appConfig !== undefined) {
|
|
217
|
+
await app.boot(appConfig);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
await app.boot();
|
|
221
|
+
}
|
|
204
222
|
}
|
|
205
223
|
// The container the host hands to each adopter — the first served
|
|
206
224
|
// app's. Multi-app composites carry per-wire app references that
|
|
@@ -243,14 +261,24 @@ export class EndpointBuilder {
|
|
|
243
261
|
? mountedSource
|
|
244
262
|
: mountedSource.interface;
|
|
245
263
|
const containerOf = (_w) => {
|
|
246
|
-
//
|
|
247
|
-
// container
|
|
248
|
-
//
|
|
249
|
-
// per-wire container routing.
|
|
264
|
+
// Wires tagged with a source app (via `appCompose`) route to that
|
|
265
|
+
// app's container; un-tagged wires fall back to the mounted
|
|
266
|
+
// source's container.
|
|
250
267
|
const sourceApp = mountedSource && !isInterfaceShape(mountedSource) ? mountedSource : undefined;
|
|
251
268
|
const wireApp = _w.app;
|
|
252
269
|
return wireApp?.container ?? sourceApp?.container;
|
|
253
270
|
};
|
|
271
|
+
// Outbound adapters install sink terminals on the mounted App's
|
|
272
|
+
// runtime at boot. Available only when an App with a Runtime is
|
|
273
|
+
// mounted.
|
|
274
|
+
const mountedAppRuntime = mountedSource && isAppWithInterface(mountedSource)
|
|
275
|
+
? mountedSource.runtime
|
|
276
|
+
: undefined;
|
|
277
|
+
const installSinkStage = mountedAppRuntime?.sink
|
|
278
|
+
? (stage) => {
|
|
279
|
+
mountedAppRuntime.sink(stage);
|
|
280
|
+
}
|
|
281
|
+
: undefined;
|
|
254
282
|
for (const adopter of this.adapters) {
|
|
255
283
|
const wires = sourceInterface?.forAdapter(adopter.kind) ?? [];
|
|
256
284
|
await adopter.boot({
|
|
@@ -260,6 +288,8 @@ export class EndpointBuilder {
|
|
|
260
288
|
addCheck: (c) => accumulatedChecks.push(c),
|
|
261
289
|
port: this.config.port,
|
|
262
290
|
host: this.config.host,
|
|
291
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
292
|
+
installSinkStage: installSinkStage,
|
|
263
293
|
});
|
|
264
294
|
}
|
|
265
295
|
// 3. Wrap the server (if any) with graceful shutdown + probes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/endpoint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"endpoint",
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"http-terminator": "^3.2.0",
|
|
34
34
|
"lightship": "^9.0.4",
|
|
35
|
-
"@nwire/hooks": "0.
|
|
36
|
-
"@nwire/
|
|
37
|
-
"@nwire/
|
|
38
|
-
"@nwire/wires": "0.
|
|
35
|
+
"@nwire/hooks": "0.11.0",
|
|
36
|
+
"@nwire/runtime": "0.11.0",
|
|
37
|
+
"@nwire/container": "0.11.0",
|
|
38
|
+
"@nwire/wires": "0.11.0",
|
|
39
|
+
"@nwire/logger": "0.11.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "^22.19.9",
|