@nwire/endpoint 0.12.1 → 0.13.1
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/dist/adapter.d.ts +24 -0
- package/dist/endpoint-index.d.ts +1 -1
- package/dist/endpoint.d.ts +31 -3
- package/dist/endpoint.js +49 -4
- package/dist/lifecycle.js +5 -2
- package/package.json +6 -6
package/dist/adapter.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* `endpoint("api").use(adopter).mount(source).run()` is the nwire path.
|
|
11
11
|
*/
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
12
13
|
import type { Wire } from "@nwire/wires";
|
|
13
14
|
import type { Container } from "@nwire/container";
|
|
14
15
|
import type { Logger } from "@nwire/logger";
|
|
@@ -82,6 +83,29 @@ export interface Adapter {
|
|
|
82
83
|
* adopters or before boot.
|
|
83
84
|
*/
|
|
84
85
|
port?(): number | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Optional — a host-mountable `(req, res)` handler, available after
|
|
88
|
+
* `boot()`. HTTP-class adapters return their composed handler (koa's
|
|
89
|
+
* `.callback()`, the Express app, …) so a single in-process host (the
|
|
90
|
+
* one-Vite dev host, supertest) can mount the wire as middleware without
|
|
91
|
+
* binding a port. Non-HTTP adapters (queue, cron, nats, mcp) return
|
|
92
|
+
* `undefined` — they just run in-process. Transport-agnostic: the
|
|
93
|
+
* endpoint composes these without knowing which transport produced them.
|
|
94
|
+
*/
|
|
95
|
+
handler?(): ((req: IncomingMessage, res: ServerResponse) => void) | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Optional — does this adapter own the given URL path? Available after
|
|
98
|
+
* `boot()`. The one-Vite dev host calls this to route deterministically:
|
|
99
|
+
* paths the wire owns (its route prefix + well-known surfaces like
|
|
100
|
+
* `/openapi.json`, `/docs`, `/_nwire/*`) go to the wire's handler; every
|
|
101
|
+
* other path (`/`, Studio assets, Studio deep links) goes to Studio. This
|
|
102
|
+
* removes the response-sniffing heuristic for the common case — a wire with
|
|
103
|
+
* adopter-wide auth no longer 401s `/` and shadows Studio.
|
|
104
|
+
*
|
|
105
|
+
* Returns `undefined`/absent for adapters that don't compute ownership; the
|
|
106
|
+
* host then falls back to running the handler and detecting a pass-through.
|
|
107
|
+
*/
|
|
108
|
+
owns?(pathname: string): boolean;
|
|
85
109
|
}
|
|
86
110
|
/** Type narrow — is this an Adapter? */
|
|
87
111
|
export declare function isAdapter(x: unknown): x is Adapter;
|
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 ProcessConfig, 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, type DevHostMount, type DevHostCollector, } 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
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* write the same K8s-readiness code on every project. It works alone —
|
|
35
35
|
* install only this package and wrap any Node server.
|
|
36
36
|
*/
|
|
37
|
-
import { type Server } from "node:http";
|
|
37
|
+
import { type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
|
38
38
|
import type { Container } from "@nwire/container";
|
|
39
39
|
import type { Interface } from "@nwire/wires";
|
|
40
40
|
import { type HealthConfig, type LifecycleManager, type ShutdownConfig } from "./lifecycle.js";
|
|
@@ -135,10 +135,38 @@ export type ProcessConfig = EndpointConfig;
|
|
|
135
135
|
export interface RunningEndpoint {
|
|
136
136
|
readonly name: string;
|
|
137
137
|
readonly server?: Server;
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
/**
|
|
139
|
+
* The lifecycle manager — present for a normal `.run()`. Absent in dev-host
|
|
140
|
+
* mode, where the Vite host owns the process (no probes / signal handling).
|
|
141
|
+
*/
|
|
142
|
+
readonly lifecycle?: LifecycleManager;
|
|
143
|
+
/** Trigger shutdown manually. Idempotent. Drains adapters, then apps. */
|
|
140
144
|
shutdown(reason?: string): Promise<void>;
|
|
141
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* What a dev host collects from each `.run()` it loads (one Vite host mode).
|
|
148
|
+
* The host registers a collector on `globalThis.__nwireDevHost`; the endpoint
|
|
149
|
+
* boots port-less and hands over the booted app(s) + each adapter's mounted
|
|
150
|
+
* `(req, res)` handler so the host can mount the wire as middleware and read
|
|
151
|
+
* the in-process runtime directly. See the dev-host branch in `run()`.
|
|
152
|
+
*/
|
|
153
|
+
export interface DevHostMount {
|
|
154
|
+
readonly name: string;
|
|
155
|
+
readonly apps: readonly AppServable[];
|
|
156
|
+
readonly handlers: ReadonlyArray<(req: IncomingMessage, res: ServerResponse) => void>;
|
|
157
|
+
/**
|
|
158
|
+
* Does this endpoint's wire own the given URL path? OR of each HTTP adapter's
|
|
159
|
+
* `owns(pathname)`. Present only when at least one adapter computes ownership;
|
|
160
|
+
* the host routes owned paths to the wire and everything else to Studio. When
|
|
161
|
+
* absent, the host falls back to running handlers + detecting a pass-through.
|
|
162
|
+
*/
|
|
163
|
+
readonly owns?: (pathname: string) => boolean;
|
|
164
|
+
/** Drain this endpoint's adapters (reverse) then its apps. Host calls on close/HMR. */
|
|
165
|
+
readonly shutdown: (reason?: string) => Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
export interface DevHostCollector {
|
|
168
|
+
collect(mount: DevHostMount): void;
|
|
169
|
+
}
|
|
142
170
|
/**
|
|
143
171
|
* Build an endpoint. The returned builder is chainable: `.serve()` adds
|
|
144
172
|
* apps + interfaces in declaration order; `.run()` boots and listens.
|
package/dist/endpoint.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* write the same K8s-readiness code on every project. It works alone —
|
|
35
35
|
* install only this package and wrap any Node server.
|
|
36
36
|
*/
|
|
37
|
-
import { createServer as createHttpServer } from "node:http";
|
|
37
|
+
import { createServer as createHttpServer, } from "node:http";
|
|
38
38
|
import { hook } from "@nwire/hooks";
|
|
39
39
|
import { ConsoleLogger } from "@nwire/logger";
|
|
40
40
|
import { attachLifecycle, } from "./lifecycle.js";
|
|
@@ -235,8 +235,15 @@ export class EndpointBuilder {
|
|
|
235
235
|
phase: "serve",
|
|
236
236
|
startedAt: new Date().toISOString(),
|
|
237
237
|
});
|
|
238
|
+
// Dev-host mode (one Vite host): when a collector is registered on the
|
|
239
|
+
// global, the endpoint boots + mounts its adapters but binds NO port —
|
|
240
|
+
// httpKoa mounts its koa without listening (it only listens when a port
|
|
241
|
+
// is present), so the host can grab `adapter.koa().callback()` and mount
|
|
242
|
+
// the wire as middleware on its own single port. Foreign listeners and
|
|
243
|
+
// probes are skipped too. `main.ts` is unchanged: it still calls `.run()`.
|
|
244
|
+
const devHost = globalThis.__nwireDevHost;
|
|
238
245
|
let server;
|
|
239
|
-
if (this.config.port !== undefined && this.foreignServables.length) {
|
|
246
|
+
if (!devHost && this.config.port !== undefined && this.foreignServables.length) {
|
|
240
247
|
server = await startListener({
|
|
241
248
|
port: this.config.port,
|
|
242
249
|
host: this.config.host,
|
|
@@ -286,12 +293,48 @@ export class EndpointBuilder {
|
|
|
286
293
|
containerOf,
|
|
287
294
|
logger: adapterLogger,
|
|
288
295
|
addCheck: (c) => accumulatedChecks.push(c),
|
|
289
|
-
|
|
296
|
+
// Dev-host: no port → adapters mount without binding.
|
|
297
|
+
port: devHost ? undefined : this.config.port,
|
|
290
298
|
host: this.config.host,
|
|
291
299
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
292
300
|
installSinkStage: installSinkStage,
|
|
293
301
|
});
|
|
294
302
|
}
|
|
303
|
+
// Dev-host: hand the booted app + each adapter's mounted handler to the
|
|
304
|
+
// collector and return without binding a port or a probe server — the
|
|
305
|
+
// Vite host mounts the handlers + reads the in-process runtime directly.
|
|
306
|
+
if (devHost) {
|
|
307
|
+
// Transport-agnostic: ask each adapter for its mountable handler via the
|
|
308
|
+
// contract. HTTP adapters (koa, express, …) return one; non-HTTP adapters
|
|
309
|
+
// return undefined and keep running in-process. The endpoint never names
|
|
310
|
+
// a transport.
|
|
311
|
+
const handlers = [];
|
|
312
|
+
const ownsFns = [];
|
|
313
|
+
for (const adopter of this.adapters) {
|
|
314
|
+
const h = adopter.handler?.();
|
|
315
|
+
if (h)
|
|
316
|
+
handlers.push(h);
|
|
317
|
+
if (adopter.owns)
|
|
318
|
+
ownsFns.push((p) => adopter.owns(p));
|
|
319
|
+
}
|
|
320
|
+
// OR each adapter's ownership so the host can route deterministically:
|
|
321
|
+
// owned paths → wire, everything else → Studio. Undefined when no adapter
|
|
322
|
+
// computes ownership — the host then runs handlers + sniffs a 404.
|
|
323
|
+
const owns = ownsFns.length > 0 ? (pathname) => ownsFns.some((f) => f(pathname)) : undefined;
|
|
324
|
+
// Drain like prod: adapters in reverse boot order (so non-HTTP transports
|
|
325
|
+
// — queue/cron/nats — that started in-process get torn down), then stop
|
|
326
|
+
// the apps. The host calls this on close / HMR reload.
|
|
327
|
+
const reverseAdapters = [...this.adapters].reverse();
|
|
328
|
+
const shutdown = async (reason) => {
|
|
329
|
+
for (const adopter of reverseAdapters)
|
|
330
|
+
await adopter.shutdown(reason);
|
|
331
|
+
for (const app of this.apps)
|
|
332
|
+
await app.stop?.();
|
|
333
|
+
};
|
|
334
|
+
devHost.collect({ name: this.name, apps: this.apps, handlers, owns, shutdown });
|
|
335
|
+
// No lifecycle manager in dev-host mode — the Vite host owns the process.
|
|
336
|
+
return { name: this.name, shutdown };
|
|
337
|
+
}
|
|
295
338
|
// 3. Wrap the server (if any) with graceful shutdown + probes.
|
|
296
339
|
const lifecycle = await attachLifecycle({
|
|
297
340
|
server: server ?? (await makeProbeOnlyServer()),
|
|
@@ -363,7 +406,9 @@ export class EndpointBuilder {
|
|
|
363
406
|
name: this.name,
|
|
364
407
|
host: this.config.host ?? "0.0.0.0",
|
|
365
408
|
port: dataPort,
|
|
366
|
-
probePort: probesBound
|
|
409
|
+
probePort: probesBound
|
|
410
|
+
? (this.config.probes?.port ?? (Number(process.env.PROBE_PORT) || 9_400))
|
|
411
|
+
: undefined,
|
|
367
412
|
});
|
|
368
413
|
}
|
|
369
414
|
return {
|
package/dist/lifecycle.js
CHANGED
|
@@ -52,7 +52,10 @@ export async function attachLifecycle(opts) {
|
|
|
52
52
|
};
|
|
53
53
|
const healthCfg = {
|
|
54
54
|
enabled: opts.health?.enabled ?? true,
|
|
55
|
-
|
|
55
|
+
// Explicit config wins; else `$PROBE_PORT` (so the probe port is tunable
|
|
56
|
+
// without a code change — e.g. when :9400 is taken by another service);
|
|
57
|
+
// else the default. A non-numeric env value falls through to 9400.
|
|
58
|
+
port: opts.health?.port ?? (Number(process.env.PROBE_PORT) || 9_400),
|
|
56
59
|
livenessPath: opts.health?.livenessPath ?? "/live",
|
|
57
60
|
readinessPath: opts.health?.readinessPath ?? "/ready",
|
|
58
61
|
checks: opts.health?.checks ?? [],
|
|
@@ -86,7 +89,7 @@ export async function attachLifecycle(opts) {
|
|
|
86
89
|
const code = err.code;
|
|
87
90
|
if (code === "EADDRINUSE") {
|
|
88
91
|
console.warn(`[lifecycle] probe port :${healthCfg.port} already in use — probes disabled. ` +
|
|
89
|
-
`Override with \`probes: { port: <other> }\` on endpoint().`);
|
|
92
|
+
`Override with \`probes: { port: <other> }\` on endpoint(), or set $PROBE_PORT.`);
|
|
90
93
|
}
|
|
91
94
|
else {
|
|
92
95
|
throw err;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/endpoint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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,11 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"http-terminator": "^3.2.0",
|
|
34
34
|
"lightship": "^9.0.4",
|
|
35
|
-
"@nwire/
|
|
36
|
-
"@nwire/logger": "0.
|
|
37
|
-
"@nwire/
|
|
38
|
-
"@nwire/container": "0.
|
|
39
|
-
"@nwire/
|
|
35
|
+
"@nwire/runtime": "0.13.1",
|
|
36
|
+
"@nwire/logger": "0.13.1",
|
|
37
|
+
"@nwire/wires": "0.13.1",
|
|
38
|
+
"@nwire/container": "0.13.1",
|
|
39
|
+
"@nwire/hooks": "0.13.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^22.19.9",
|