@nwire/wires 0.10.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 +50 -0
- package/dist/cron/index.d.ts +34 -0
- package/dist/cron/index.js +30 -0
- package/dist/graphql/index.d.ts +37 -0
- package/dist/graphql/index.js +44 -0
- package/dist/http/index.d.ts +95 -0
- package/dist/http/index.js +49 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +72 -0
- package/dist/interface-builder.d.ts +293 -0
- package/dist/interface-builder.js +125 -0
- package/dist/mcp/index.d.ts +54 -0
- package/dist/mcp/index.js +54 -0
- package/dist/queue/index.d.ts +43 -0
- package/dist/queue/index.js +33 -0
- package/package.json +62 -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,50 @@
|
|
|
1
|
+
# @nwire/wires
|
|
2
|
+
|
|
3
|
+
> Transport contract — the abstract base every Nwire transport extends, and the shape foreign hosts implement to plug in.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
`NwireInterface` is the structural shape every transport satisfies (`http`, `queue`, `mcp`, `graphql`, future `ws`/`grpc`). `InterfaceBuilder` is the abstract base each transport extends.
|
|
8
|
+
|
|
9
|
+
The verb surface is uniform across transports:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.use(...plugins) augment the chain
|
|
13
|
+
.wire(binding, handler?, o?) bind an outbound surface
|
|
14
|
+
.from(source) declare an inbound stream
|
|
15
|
+
.mount(target) attach this iface to a host
|
|
16
|
+
.run(opts?) → Lifecycle serve standalone
|
|
17
|
+
.boot(opts?) → Booted<T> build without listening
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Plus two internal seams (`.manifest()` for scanner/Studio; `.attach(host)` for host lifecycle) and one DI sugar (`.provide(container)`).
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add @nwire/wires
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Within nwire-app
|
|
29
|
+
|
|
30
|
+
You usually never import this package directly — `@nwire/http` / `@nwire/queue` / `@nwire/mcp` each ship their own builder that already extends `InterfaceBuilder`. Touch this package only when writing a new transport or a foreign-host adapter.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { endpoint } from "@nwire/endpoint";
|
|
34
|
+
import { http } from "@nwire/http"; // extends InterfaceBuilder
|
|
35
|
+
import { queue } from "@nwire/queue"; // extends InterfaceBuilder
|
|
36
|
+
|
|
37
|
+
const api = http().wire(getUsers);
|
|
38
|
+
const worker = queue().wire(sendEmail);
|
|
39
|
+
|
|
40
|
+
await endpoint("app").serve(api).serve(worker).run();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
- `NwireInterface` — structural shape `endpoint().serve()` consumes.
|
|
46
|
+
- `InterfaceBuilder<TBinding, TPlugin, TFromSource, TMountTarget, TArtifact>` — abstract base with the six verbs + manifest + attach.
|
|
47
|
+
- `Lifecycle` / `RunOptions` / `Booted` / `BootOptions` / `InterfaceManifest` — return + option types shared across transports.
|
|
48
|
+
- `makeContainerPlugin` / `isContainerPlugin` — sentinel for transports that need to lift containers out of the `.use()` chain.
|
|
49
|
+
- `isNwireInterface` — structural type narrow.
|
|
50
|
+
- Re-exports `HandlerLike` + `HandlerDefinition` from `@nwire/handler` so transport authors have one import.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/cron` — cron binding factory.
|
|
3
|
+
*
|
|
4
|
+
* import { cron } from "@nwire/wires/cron";
|
|
5
|
+
*
|
|
6
|
+
* createInterface().wire(cron("0 *\/15 * * * *"), refreshFeed);
|
|
7
|
+
*
|
|
8
|
+
* Adopter (`@nwire/cron`) filters wires by `binding.$adapter === "cron"`
|
|
9
|
+
* and schedules each handler against the cron expression.
|
|
10
|
+
*/
|
|
11
|
+
import type { Binding } from "../index.js";
|
|
12
|
+
export interface CronBindingOptions {
|
|
13
|
+
/** Timezone (IANA) the cron expression is evaluated in. Adopter default applies otherwise. */
|
|
14
|
+
readonly timezone?: string;
|
|
15
|
+
/** Free-form source tag — Studio and observability group by it. */
|
|
16
|
+
readonly source?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Set true to opt the binding out of the adopter's overlap guard. Default
|
|
19
|
+
* false — adopters skip overlapping invocations by default.
|
|
20
|
+
*/
|
|
21
|
+
readonly overlap?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface CronBinding extends Binding {
|
|
24
|
+
readonly $kind: "binding";
|
|
25
|
+
readonly $adapter: "cron";
|
|
26
|
+
readonly kind: "cron";
|
|
27
|
+
readonly schedule: string;
|
|
28
|
+
readonly timezone?: string;
|
|
29
|
+
readonly source?: string;
|
|
30
|
+
readonly overlap?: boolean;
|
|
31
|
+
/** Chainable source tag — returns a fresh binding. */
|
|
32
|
+
from(source: string): CronBinding;
|
|
33
|
+
}
|
|
34
|
+
export declare function cron(schedule: string, options?: CronBindingOptions): CronBinding;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/cron` — cron binding factory.
|
|
3
|
+
*
|
|
4
|
+
* import { cron } from "@nwire/wires/cron";
|
|
5
|
+
*
|
|
6
|
+
* createInterface().wire(cron("0 *\/15 * * * *"), refreshFeed);
|
|
7
|
+
*
|
|
8
|
+
* Adopter (`@nwire/cron`) filters wires by `binding.$adapter === "cron"`
|
|
9
|
+
* and schedules each handler against the cron expression.
|
|
10
|
+
*/
|
|
11
|
+
function buildBinding(schedule, options, source) {
|
|
12
|
+
const binding = {
|
|
13
|
+
$kind: "binding",
|
|
14
|
+
$adapter: "cron",
|
|
15
|
+
kind: "cron",
|
|
16
|
+
schedule,
|
|
17
|
+
...(options ?? {}),
|
|
18
|
+
...(source !== undefined ? { source } : {}),
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(binding, "from", {
|
|
21
|
+
value: (next) => buildBinding(schedule, options, next),
|
|
22
|
+
enumerable: false,
|
|
23
|
+
writable: false,
|
|
24
|
+
configurable: true,
|
|
25
|
+
});
|
|
26
|
+
return binding;
|
|
27
|
+
}
|
|
28
|
+
export function cron(schedule, options) {
|
|
29
|
+
return buildBinding(schedule, options, undefined);
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/graphql` — GraphQL field binding factories.
|
|
3
|
+
*
|
|
4
|
+
* import { query, mutation, subscription } from "@nwire/wires/graphql";
|
|
5
|
+
*
|
|
6
|
+
* createInterface()
|
|
7
|
+
* .wire(query("orders"), listOrders)
|
|
8
|
+
* .wire(mutation("createOrder"), createOrder);
|
|
9
|
+
*
|
|
10
|
+
* Thin universal-routing shape — the wire just names which GraphQL
|
|
11
|
+
* field this handler implements. The adopter (Apollo / Yoga / raw)
|
|
12
|
+
* decides how to map: build a resolver tree, federate a schema, stitch
|
|
13
|
+
* a remote endpoint, etc. Schemas live with the typedefs the adopter
|
|
14
|
+
* loads, not on the binding.
|
|
15
|
+
*
|
|
16
|
+
* Bindings carry `$adapter: "graphql"` so the GraphQL adopter filters
|
|
17
|
+
* via `interface.forAdapter("graphql")`.
|
|
18
|
+
*/
|
|
19
|
+
import type { Binding } from "../index.js";
|
|
20
|
+
export type GraphqlFieldKind = "query" | "mutation" | "subscription";
|
|
21
|
+
export interface GraphqlBindingOptions {
|
|
22
|
+
/** Optional parent type for fields under custom roots (federated graphs). */
|
|
23
|
+
readonly typeName?: string;
|
|
24
|
+
readonly source?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface GraphqlBinding extends Binding {
|
|
27
|
+
readonly $kind: "binding";
|
|
28
|
+
readonly $adapter: "graphql";
|
|
29
|
+
readonly kind: GraphqlFieldKind;
|
|
30
|
+
readonly field: string;
|
|
31
|
+
readonly typeName?: string;
|
|
32
|
+
readonly source?: string;
|
|
33
|
+
from(source: string): GraphqlBinding;
|
|
34
|
+
}
|
|
35
|
+
export declare function query(field: string, options?: GraphqlBindingOptions): GraphqlBinding;
|
|
36
|
+
export declare function mutation(field: string, options?: GraphqlBindingOptions): GraphqlBinding;
|
|
37
|
+
export declare function subscription(field: string, options?: GraphqlBindingOptions): GraphqlBinding;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/graphql` — GraphQL field binding factories.
|
|
3
|
+
*
|
|
4
|
+
* import { query, mutation, subscription } from "@nwire/wires/graphql";
|
|
5
|
+
*
|
|
6
|
+
* createInterface()
|
|
7
|
+
* .wire(query("orders"), listOrders)
|
|
8
|
+
* .wire(mutation("createOrder"), createOrder);
|
|
9
|
+
*
|
|
10
|
+
* Thin universal-routing shape — the wire just names which GraphQL
|
|
11
|
+
* field this handler implements. The adopter (Apollo / Yoga / raw)
|
|
12
|
+
* decides how to map: build a resolver tree, federate a schema, stitch
|
|
13
|
+
* a remote endpoint, etc. Schemas live with the typedefs the adopter
|
|
14
|
+
* loads, not on the binding.
|
|
15
|
+
*
|
|
16
|
+
* Bindings carry `$adapter: "graphql"` so the GraphQL adopter filters
|
|
17
|
+
* via `interface.forAdapter("graphql")`.
|
|
18
|
+
*/
|
|
19
|
+
function buildBinding(kind, field, options, source) {
|
|
20
|
+
const binding = {
|
|
21
|
+
$kind: "binding",
|
|
22
|
+
$adapter: "graphql",
|
|
23
|
+
kind,
|
|
24
|
+
field,
|
|
25
|
+
...(options ?? {}),
|
|
26
|
+
...(source !== undefined ? { source } : {}),
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(binding, "from", {
|
|
29
|
+
value: (next) => buildBinding(kind, field, options, next),
|
|
30
|
+
enumerable: false,
|
|
31
|
+
writable: false,
|
|
32
|
+
configurable: true,
|
|
33
|
+
});
|
|
34
|
+
return binding;
|
|
35
|
+
}
|
|
36
|
+
export function query(field, options) {
|
|
37
|
+
return buildBinding("query", field, options, undefined);
|
|
38
|
+
}
|
|
39
|
+
export function mutation(field, options) {
|
|
40
|
+
return buildBinding("mutation", field, options, undefined);
|
|
41
|
+
}
|
|
42
|
+
export function subscription(field, options) {
|
|
43
|
+
return buildBinding("subscription", field, options, undefined);
|
|
44
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/http` — HTTP binding factories.
|
|
3
|
+
*
|
|
4
|
+
* import { post, get, del, put, patch } from "@nwire/wires/http";
|
|
5
|
+
*
|
|
6
|
+
* createInterface()
|
|
7
|
+
* .wire(post("/orders", { body: CreateOrder }), placeOrder)
|
|
8
|
+
* .wire(get("/orders/:id", { params: OrderParams }), getOrder);
|
|
9
|
+
*
|
|
10
|
+
* Bindings carry `$adapter: "http"` so an HTTP adopter filters them via
|
|
11
|
+
* `interface.forAdapter("http")`. Schemas are zod-shaped; the adopter
|
|
12
|
+
* parses request data at dispatch time.
|
|
13
|
+
*
|
|
14
|
+
* Foreign-import factories (`fromExpress` / `fromKoa` / `fromFastify`)
|
|
15
|
+
* live in the adopter packages, not here.
|
|
16
|
+
*/
|
|
17
|
+
import type { ZodTypeAny, z } from "zod";
|
|
18
|
+
import type { Binding } from "../index.js";
|
|
19
|
+
export type HttpVerb = "get" | "post" | "put" | "patch" | "delete";
|
|
20
|
+
/** Lifecycle of an exposed operation — surfaces on the OpenAPI doc. */
|
|
21
|
+
export type RouteStatus = "draft" | "active" | "deprecated" | "sunset";
|
|
22
|
+
/**
|
|
23
|
+
* OpenAPI / contract metadata for one route. The HTTP adopter reads it at
|
|
24
|
+
* boot to emit the operation id, summary, tags, response shapes, error
|
|
25
|
+
* shapes, and lifecycle status.
|
|
26
|
+
*/
|
|
27
|
+
export interface RouteOpenApi {
|
|
28
|
+
readonly operation: string;
|
|
29
|
+
readonly version?: number;
|
|
30
|
+
readonly status?: RouteStatus;
|
|
31
|
+
readonly summary?: string;
|
|
32
|
+
readonly description?: string;
|
|
33
|
+
readonly tags?: ReadonlyArray<string>;
|
|
34
|
+
readonly returns?: ReadonlyArray<any>;
|
|
35
|
+
readonly errors?: ReadonlyArray<any>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Per-route middleware shape — `unknown` at this layer to keep
|
|
39
|
+
* `@nwire/wires/http` free of a Koa dep. Adopters narrow at consume
|
|
40
|
+
* time.
|
|
41
|
+
*/
|
|
42
|
+
export type RouteMiddleware = unknown;
|
|
43
|
+
/** Schemas attached to a route binding. */
|
|
44
|
+
export interface RouteSchemas {
|
|
45
|
+
readonly params?: ZodTypeAny;
|
|
46
|
+
readonly body?: ZodTypeAny;
|
|
47
|
+
readonly query?: ZodTypeAny;
|
|
48
|
+
readonly middleware?: readonly RouteMiddleware[];
|
|
49
|
+
readonly openapi?: RouteOpenApi;
|
|
50
|
+
}
|
|
51
|
+
type ZodOutput<T> = T extends ZodTypeAny ? z.output<T> : unknown;
|
|
52
|
+
/**
|
|
53
|
+
* Infer the handler's flat input type from a `RouteSchemas` shape. The
|
|
54
|
+
* adopter merges params + body + query into a single object the handler
|
|
55
|
+
* sees — this type mirrors that merge.
|
|
56
|
+
*/
|
|
57
|
+
export type InferRouteInput<S> = (S extends {
|
|
58
|
+
params?: infer P;
|
|
59
|
+
} ? P extends ZodTypeAny ? ZodOutput<P> : unknown : unknown) & (S extends {
|
|
60
|
+
query?: infer Q;
|
|
61
|
+
} ? (Q extends ZodTypeAny ? ZodOutput<Q> : unknown) : unknown) & (S extends {
|
|
62
|
+
body?: infer B;
|
|
63
|
+
} ? (B extends ZodTypeAny ? ZodOutput<B> : unknown) : unknown);
|
|
64
|
+
/**
|
|
65
|
+
* `RouteBinding` — the binding shape HTTP wires carry. Extends the base
|
|
66
|
+
* `Binding` contract with HTTP specifics (verb, path, schemas, OpenAPI).
|
|
67
|
+
* `$adapter` is fixed to `"http"`; an HTTP adopter consumes wires by
|
|
68
|
+
* that tag.
|
|
69
|
+
*/
|
|
70
|
+
export interface RouteBinding<TInput = unknown> extends Binding {
|
|
71
|
+
readonly $kind: "binding";
|
|
72
|
+
readonly $adapter: "http";
|
|
73
|
+
readonly kind: "route";
|
|
74
|
+
readonly verb: HttpVerb;
|
|
75
|
+
readonly path: string;
|
|
76
|
+
readonly params?: ZodTypeAny;
|
|
77
|
+
readonly body?: ZodTypeAny;
|
|
78
|
+
readonly query?: ZodTypeAny;
|
|
79
|
+
readonly middleware?: readonly RouteMiddleware[];
|
|
80
|
+
readonly openapi?: RouteOpenApi;
|
|
81
|
+
readonly source?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Chainable source tag — Studio and observability group by it.
|
|
84
|
+
* Returns a fresh binding; original is unchanged.
|
|
85
|
+
*/
|
|
86
|
+
from(source: string): RouteBinding<TInput>;
|
|
87
|
+
/** Phantom — TS inference of the handler's input shape. */
|
|
88
|
+
readonly __inputType?: TInput;
|
|
89
|
+
}
|
|
90
|
+
export declare function get<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
|
|
91
|
+
export declare function post<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
|
|
92
|
+
export declare function put<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
|
|
93
|
+
export declare function patch<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
|
|
94
|
+
export declare function del<S extends RouteSchemas = RouteSchemas>(path: string, schemas?: S): RouteBinding<InferRouteInput<S>>;
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/http` — HTTP binding factories.
|
|
3
|
+
*
|
|
4
|
+
* import { post, get, del, put, patch } from "@nwire/wires/http";
|
|
5
|
+
*
|
|
6
|
+
* createInterface()
|
|
7
|
+
* .wire(post("/orders", { body: CreateOrder }), placeOrder)
|
|
8
|
+
* .wire(get("/orders/:id", { params: OrderParams }), getOrder);
|
|
9
|
+
*
|
|
10
|
+
* Bindings carry `$adapter: "http"` so an HTTP adopter filters them via
|
|
11
|
+
* `interface.forAdapter("http")`. Schemas are zod-shaped; the adopter
|
|
12
|
+
* parses request data at dispatch time.
|
|
13
|
+
*
|
|
14
|
+
* Foreign-import factories (`fromExpress` / `fromKoa` / `fromFastify`)
|
|
15
|
+
* live in the adopter packages, not here.
|
|
16
|
+
*/
|
|
17
|
+
function buildBinding(verb, path, schemas, source) {
|
|
18
|
+
const binding = {
|
|
19
|
+
$kind: "binding",
|
|
20
|
+
$adapter: "http",
|
|
21
|
+
kind: "route",
|
|
22
|
+
verb,
|
|
23
|
+
path,
|
|
24
|
+
...(schemas ?? {}),
|
|
25
|
+
...(source !== undefined ? { source } : {}),
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(binding, "from", {
|
|
28
|
+
value: (next) => buildBinding(verb, path, schemas, next),
|
|
29
|
+
enumerable: false,
|
|
30
|
+
writable: false,
|
|
31
|
+
configurable: true,
|
|
32
|
+
});
|
|
33
|
+
return binding;
|
|
34
|
+
}
|
|
35
|
+
export function get(path, schemas) {
|
|
36
|
+
return buildBinding("get", path, schemas, undefined);
|
|
37
|
+
}
|
|
38
|
+
export function post(path, schemas) {
|
|
39
|
+
return buildBinding("post", path, schemas, undefined);
|
|
40
|
+
}
|
|
41
|
+
export function put(path, schemas) {
|
|
42
|
+
return buildBinding("put", path, schemas, undefined);
|
|
43
|
+
}
|
|
44
|
+
export function patch(path, schemas) {
|
|
45
|
+
return buildBinding("patch", path, schemas, undefined);
|
|
46
|
+
}
|
|
47
|
+
export function del(path, schemas) {
|
|
48
|
+
return buildBinding("delete", path, schemas, undefined);
|
|
49
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires` — standalone wire collection + binding factories.
|
|
3
|
+
*
|
|
4
|
+
* The Interface owns a list of `{ binding, handler }` pairs (wires) plus
|
|
5
|
+
* any `provide()`'d ctx builders. Adopters read this list, filter by
|
|
6
|
+
* `binding.$adapter`, and build their transport runtimes from the matched
|
|
7
|
+
* wires. The Interface itself has no transport awareness, no lifecycle,
|
|
8
|
+
* no DI — pure data + context.
|
|
9
|
+
*
|
|
10
|
+
* import { createInterface } from "@nwire/wires";
|
|
11
|
+
* import { post, get } from "@nwire/wires/http";
|
|
12
|
+
* import { queue } from "@nwire/wires/queue";
|
|
13
|
+
*
|
|
14
|
+
* const api = createInterface()
|
|
15
|
+
* .wire(post("/orders"), placeOrder)
|
|
16
|
+
* .wire(get("/orders/:id"), getOrder)
|
|
17
|
+
* .wire(queue("orders.process"), processOrder);
|
|
18
|
+
*
|
|
19
|
+
* api.forAdapter("http").length; // 2
|
|
20
|
+
* api.forAdapter("queue").length; // 1
|
|
21
|
+
*
|
|
22
|
+
* Multiple interfaces compose via `.merge()`; adopters never see the
|
|
23
|
+
* boundary.
|
|
24
|
+
*/
|
|
25
|
+
export type { HandlerLike, HandlerDefinition } from "@nwire/handler";
|
|
26
|
+
/**
|
|
27
|
+
* `Binding` — the descriptor every wire pairs with a handler. Adopters
|
|
28
|
+
* filter wires by `binding.$adapter` to find what's theirs. Per-adopter
|
|
29
|
+
* binding shapes (RouteBinding, QueueBinding, etc.) extend this.
|
|
30
|
+
*/
|
|
31
|
+
export interface Binding {
|
|
32
|
+
/** Discriminant — `"binding"`. */
|
|
33
|
+
readonly $kind: "binding";
|
|
34
|
+
/** Adopter tag — every adopter consumes wires whose tag matches. */
|
|
35
|
+
readonly $adapter: string;
|
|
36
|
+
/** Free-form subtype within the adopter (`"route"`, `"job"`, `"tool"`, …). */
|
|
37
|
+
readonly kind?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `HandlerDef` — the structural shape the wire-collection holds. Kept
|
|
41
|
+
* deliberately loose at this layer (any function or `HandlerLike` from
|
|
42
|
+
* `@nwire/handler`); concrete validation happens at the adopter when
|
|
43
|
+
* dispatch fires.
|
|
44
|
+
*/
|
|
45
|
+
export type HandlerDef = ((input: any, ctx: any) => unknown | Promise<unknown>) | {
|
|
46
|
+
run: (input: any, ctx: any) => unknown | Promise<unknown>;
|
|
47
|
+
} | {
|
|
48
|
+
$kind: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* `Wire` — `{ binding, handler }`. Optional `app` field tags which App
|
|
52
|
+
* the wire came from (set by `app.interface.wire()` / `appCompose`),
|
|
53
|
+
* which adopters use via `containerOf(wire)` to route to the right
|
|
54
|
+
* container.
|
|
55
|
+
*/
|
|
56
|
+
export interface Wire {
|
|
57
|
+
readonly binding: Binding;
|
|
58
|
+
readonly handler: HandlerDef;
|
|
59
|
+
/** Source app reference (opaque at this layer — endpoint resolves it). */
|
|
60
|
+
readonly app?: any;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Per-request ctx builder. Same shape as the legacy `CtxBuilder` from
|
|
64
|
+
* interface-builder.ts — re-declared here under the new surface for
|
|
65
|
+
* standalone use without pulling the legacy types.
|
|
66
|
+
*/
|
|
67
|
+
export type WireCtxBuilder<TExtras extends object = object, TRequest = any> = TExtras | ((request: TRequest) => TExtras | Promise<TExtras>);
|
|
68
|
+
/**
|
|
69
|
+
* `Interface` — the wire collection contract.
|
|
70
|
+
*
|
|
71
|
+
* wires — every registered wire (read-only snapshot)
|
|
72
|
+
* wire(b,h) — register one wire; returns this for chaining
|
|
73
|
+
* merge(other)— append another Interface's wires
|
|
74
|
+
* forAdapter — slice by binding.$adapter; adopters consume their slice
|
|
75
|
+
* provide — accumulate ctx builders adopters apply per request
|
|
76
|
+
*/
|
|
77
|
+
export interface Interface {
|
|
78
|
+
readonly $kind: "interface";
|
|
79
|
+
readonly wires: readonly Wire[];
|
|
80
|
+
wire(binding: Binding, handler: HandlerDef): this;
|
|
81
|
+
merge(other: Interface): this;
|
|
82
|
+
forAdapter(kind: string): readonly Wire[];
|
|
83
|
+
provide<TExtras extends object>(builder: WireCtxBuilder<TExtras>): this;
|
|
84
|
+
/**
|
|
85
|
+
* Compose accumulated ctx extras for an incoming request. Adopters
|
|
86
|
+
* call this per request; the result is merged onto the handler ctx.
|
|
87
|
+
* Last-write wins on key collision.
|
|
88
|
+
*/
|
|
89
|
+
composeCtxExtras(request: unknown): Promise<Record<string, unknown>>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Construct a new, empty `Interface`. Apps construct one internally and
|
|
93
|
+
* expose it as `app.interface`; consumers can also build standalone wire
|
|
94
|
+
* collections that get merged into apps or attached to endpoints
|
|
95
|
+
* directly.
|
|
96
|
+
*/
|
|
97
|
+
export declare function createInterface(): Interface;
|
|
98
|
+
/** Type narrow — does this value look like a new-shape Interface? */
|
|
99
|
+
export declare function isInterface(x: unknown): x is Interface;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires` — standalone wire collection + binding factories.
|
|
3
|
+
*
|
|
4
|
+
* The Interface owns a list of `{ binding, handler }` pairs (wires) plus
|
|
5
|
+
* any `provide()`'d ctx builders. Adopters read this list, filter by
|
|
6
|
+
* `binding.$adapter`, and build their transport runtimes from the matched
|
|
7
|
+
* wires. The Interface itself has no transport awareness, no lifecycle,
|
|
8
|
+
* no DI — pure data + context.
|
|
9
|
+
*
|
|
10
|
+
* import { createInterface } from "@nwire/wires";
|
|
11
|
+
* import { post, get } from "@nwire/wires/http";
|
|
12
|
+
* import { queue } from "@nwire/wires/queue";
|
|
13
|
+
*
|
|
14
|
+
* const api = createInterface()
|
|
15
|
+
* .wire(post("/orders"), placeOrder)
|
|
16
|
+
* .wire(get("/orders/:id"), getOrder)
|
|
17
|
+
* .wire(queue("orders.process"), processOrder);
|
|
18
|
+
*
|
|
19
|
+
* api.forAdapter("http").length; // 2
|
|
20
|
+
* api.forAdapter("queue").length; // 1
|
|
21
|
+
*
|
|
22
|
+
* Multiple interfaces compose via `.merge()`; adopters never see the
|
|
23
|
+
* boundary.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Construct a new, empty `Interface`. Apps construct one internally and
|
|
27
|
+
* expose it as `app.interface`; consumers can also build standalone wire
|
|
28
|
+
* collections that get merged into apps or attached to endpoints
|
|
29
|
+
* directly.
|
|
30
|
+
*/
|
|
31
|
+
export function createInterface() {
|
|
32
|
+
const wires = [];
|
|
33
|
+
const ctxBuilders = [];
|
|
34
|
+
const iface = {
|
|
35
|
+
$kind: "interface",
|
|
36
|
+
get wires() {
|
|
37
|
+
return wires;
|
|
38
|
+
},
|
|
39
|
+
wire(binding, handler) {
|
|
40
|
+
wires.push({ binding, handler });
|
|
41
|
+
return iface;
|
|
42
|
+
},
|
|
43
|
+
merge(other) {
|
|
44
|
+
for (const w of other.wires)
|
|
45
|
+
wires.push(w);
|
|
46
|
+
return iface;
|
|
47
|
+
},
|
|
48
|
+
forAdapter(kind) {
|
|
49
|
+
return wires.filter((w) => w.binding.$adapter === kind);
|
|
50
|
+
},
|
|
51
|
+
provide(builder) {
|
|
52
|
+
ctxBuilders.push(builder);
|
|
53
|
+
return iface;
|
|
54
|
+
},
|
|
55
|
+
async composeCtxExtras(request) {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const b of ctxBuilders) {
|
|
58
|
+
const extras = typeof b === "function" ? await b(request) : b;
|
|
59
|
+
Object.assign(out, extras);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return iface;
|
|
65
|
+
}
|
|
66
|
+
/** Type narrow — does this value look like a new-shape Interface? */
|
|
67
|
+
export function isInterface(x) {
|
|
68
|
+
return (typeof x === "object" &&
|
|
69
|
+
x !== null &&
|
|
70
|
+
x.$kind === "interface" &&
|
|
71
|
+
Array.isArray(x.wires));
|
|
72
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `InterfaceBuilder` — the abstract base every Nwire transport extends.
|
|
3
|
+
*
|
|
4
|
+
* Six verbs are universal and form the public surface every transport
|
|
5
|
+
* supports (HTTP, queue, MCP, GraphQL, WebSocket, CLI, …):
|
|
6
|
+
*
|
|
7
|
+
* .use(...plugins) augment the chain (middleware/plugins)
|
|
8
|
+
* .wire(binding, handler?, o?) bind an outbound surface to a handler
|
|
9
|
+
* .from(source) declare an inbound stream the iface consumes
|
|
10
|
+
* .mount(target) attach this iface to a host (express/koa/…)
|
|
11
|
+
* .run(opts?) serve standalone — returns a Lifecycle
|
|
12
|
+
* .boot(opts?) build internals without listening
|
|
13
|
+
*
|
|
14
|
+
* Two internal seams that aren't verbs:
|
|
15
|
+
*
|
|
16
|
+
* .manifest() pure data describing the wired surface
|
|
17
|
+
* (scanner / Studio / cache consume this)
|
|
18
|
+
* .attach(host) lifecycle hook called by the host endpoint
|
|
19
|
+
* when this interface is mounted on another
|
|
20
|
+
*
|
|
21
|
+
* And one DI shortcut kept for ergonomics:
|
|
22
|
+
*
|
|
23
|
+
* .provide(container) sugar that wires a Container into the chain
|
|
24
|
+
* via the same channel `.use()` uses.
|
|
25
|
+
*
|
|
26
|
+
* Each transport refines the generics:
|
|
27
|
+
* - `TBinding` — outbound binding shape (RouteBinding / JobBinding / …)
|
|
28
|
+
* - `TPlugin` — what `.use()` accepts (Koa middleware / plain fn / …)
|
|
29
|
+
* - `TFromSource` — what `.from()` accepts (eventDef / remote ref / …)
|
|
30
|
+
* - `TMountTarget` — what `.mount()` accepts (Koa app / express / iface)
|
|
31
|
+
* - `TArtifact` — what `.boot()` produces (an opaque handle the host owns)
|
|
32
|
+
*
|
|
33
|
+
* The base owns the type signatures + manifest plumbing. It implements one
|
|
34
|
+
* verb concretely — `.provide()` — as a default that delegates to `.use()`.
|
|
35
|
+
* Transports may override if they need a different DI dispatch shape.
|
|
36
|
+
*/
|
|
37
|
+
import type { HandlerLike } from "@nwire/handler";
|
|
38
|
+
/**
|
|
39
|
+
* What `.provide()` accepts — either a static extras object, or a builder
|
|
40
|
+
* function the transport calls per request/job/invocation. The interface
|
|
41
|
+
* stores it opaquely and applies it during dispatch; how it composes is
|
|
42
|
+
* the wire's concern, not the transport's.
|
|
43
|
+
*
|
|
44
|
+
* api.provide({ logger: console });
|
|
45
|
+
* api.provide((req) => ({ user: req.user, requestId: crypto.randomUUID() }));
|
|
46
|
+
* api.provide((req) => ({ ...root.cradle, user: req.user })); // wire reads container
|
|
47
|
+
*
|
|
48
|
+
* The transport never imports `@nwire/container` — what the wire passes
|
|
49
|
+
* is opaque.
|
|
50
|
+
*/
|
|
51
|
+
export type CtxBuilder<TExtras extends object, TRequest = unknown> = TExtras | ((request: TRequest) => TExtras | Promise<TExtras>);
|
|
52
|
+
/**
|
|
53
|
+
* Health probe registration — aligned with `@nwire/endpoint`'s `HealthCheck`
|
|
54
|
+
* so every transport's `attach()` accepts the same shape the endpoint passes.
|
|
55
|
+
*/
|
|
56
|
+
export interface InterfaceHealthCheck {
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly check: () => Promise<void> | void;
|
|
59
|
+
readonly timeout?: number;
|
|
60
|
+
readonly kind?: "readiness" | "liveness";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Passed by the host (typically `@nwire/endpoint`) to `attach()` when this
|
|
64
|
+
* interface is being mounted into a larger composition. The host gives
|
|
65
|
+
* the interface a place to register transport-specific health checks.
|
|
66
|
+
*
|
|
67
|
+
* The host does NOT pass a container — DI composition is a wire-layer
|
|
68
|
+
* concern, threaded through `.provide(builder)` before `.serve()` runs.
|
|
69
|
+
* Interface contract stays container-agnostic.
|
|
70
|
+
*/
|
|
71
|
+
export interface AttachBindings {
|
|
72
|
+
/** Register a health check the host reports on `/live` / `/ready`. */
|
|
73
|
+
addCheck(check: InterfaceHealthCheck): void;
|
|
74
|
+
/**
|
|
75
|
+
* The host's primary DI container, when one is available (i.e. the
|
|
76
|
+
* endpoint was given `.serve(app)` before this interface). Interfaces
|
|
77
|
+
* that need a container — e.g. for `ctx.resolve(...)` inside handlers
|
|
78
|
+
* — should fall back to this when no `.provide(container)` was set
|
|
79
|
+
* on the interface itself. Typed as `unknown` so the interface contract
|
|
80
|
+
* stays free of a `@nwire/container` dependency.
|
|
81
|
+
*/
|
|
82
|
+
readonly container?: unknown;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `NwireInterface` — the structural shape `@nwire/endpoint` looks for when
|
|
86
|
+
* `.serve(interfaceValue)` is called. Any object exposing `$nwireServable`,
|
|
87
|
+
* `transport`, and `attach()` satisfies it. `InterfaceBuilder` implements
|
|
88
|
+
* the shape; foreign hosts can implement it directly to plug in.
|
|
89
|
+
*/
|
|
90
|
+
export interface NwireInterface {
|
|
91
|
+
readonly $nwireServable: true;
|
|
92
|
+
readonly transport: string;
|
|
93
|
+
attach(host: AttachBindings): void;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `InterfaceManifest` — the data shape `.manifest()` returns. Scanner,
|
|
97
|
+
* Studio, and the cache all read this; they never reach into the live
|
|
98
|
+
* builder. Each transport extends with its own concrete shape (RouteEntry
|
|
99
|
+
* for HTTP, JobEntry for queue, etc.); the base only nails down the
|
|
100
|
+
* uniform fields.
|
|
101
|
+
*/
|
|
102
|
+
export interface InterfaceManifest {
|
|
103
|
+
/** Transport id — matches the builder's `transport` field. */
|
|
104
|
+
readonly transport: string;
|
|
105
|
+
/** Optional prefix or namespace applied to every wired binding. */
|
|
106
|
+
readonly prefix?: string;
|
|
107
|
+
/** Outbound bindings — opaque at this level; transports type their own. */
|
|
108
|
+
readonly wired: readonly unknown[];
|
|
109
|
+
/** Inbound sources declared via `.from()` — same opacity. */
|
|
110
|
+
readonly from?: readonly unknown[];
|
|
111
|
+
/** Plugins applied via `.use()`. Stored by name when possible. */
|
|
112
|
+
readonly plugins?: readonly string[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Options accepted by `.run()`. Transports refine via their own union.
|
|
116
|
+
* The base only knows about the universal lifecycle knobs the endpoint
|
|
117
|
+
* also exposes; transports use the same names so `.run({port: 3000})`
|
|
118
|
+
* means the same thing on an http interface as on an endpoint.
|
|
119
|
+
*/
|
|
120
|
+
export interface RunOptions {
|
|
121
|
+
/** Bind port (HTTP/WS/queue admin). Transports may ignore. */
|
|
122
|
+
readonly port?: number;
|
|
123
|
+
/** Bind host. Default `"0.0.0.0"` where applicable. */
|
|
124
|
+
readonly host?: string;
|
|
125
|
+
/** Drain timeout in ms before the lifecycle force-exits. */
|
|
126
|
+
readonly shutdownTimeoutMs?: number;
|
|
127
|
+
/** Override the default signal handlers. Default `true` (install handlers). */
|
|
128
|
+
readonly installSignalHandlers?: boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The handle `.run()` returns. Owners call `close()` to drain gracefully.
|
|
132
|
+
* Mirrors `@nwire/endpoint`'s `Lifecycle` so swapping the runner is
|
|
133
|
+
* transparent for callers that already use the endpoint pattern.
|
|
134
|
+
*/
|
|
135
|
+
export interface Lifecycle {
|
|
136
|
+
/** Drain in-flight work + release resources. Idempotent. */
|
|
137
|
+
close(): Promise<void>;
|
|
138
|
+
/** Resolved when the lifecycle has finished closing (manual or signal). */
|
|
139
|
+
readonly closed: Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Options accepted by `.boot()`. Boot builds internals (validates the
|
|
143
|
+
* routing table, freezes the middleware chain, opens DI containers) but
|
|
144
|
+
* does NOT bind a port or start serving traffic. The endpoint uses this
|
|
145
|
+
* to take ownership of the artifact before deciding when to listen.
|
|
146
|
+
*/
|
|
147
|
+
export interface BootOptions {
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The opaque handle `.boot()` returns. Transports type `value` to their
|
|
151
|
+
* concrete artifact (a Koa app, a queue worker, an MCP server, …). The
|
|
152
|
+
* host calls `.serve()` on the result when it's ready to bind.
|
|
153
|
+
*/
|
|
154
|
+
export interface Booted<TArtifact = unknown> {
|
|
155
|
+
/** The concrete artifact — opaque at the base level. */
|
|
156
|
+
readonly value: TArtifact;
|
|
157
|
+
/** Manifest snapshot taken at boot. */
|
|
158
|
+
readonly manifest: InterfaceManifest;
|
|
159
|
+
/** Shutdown handle for the booted artifact. */
|
|
160
|
+
shutdown(): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Base class every transport extends.
|
|
164
|
+
*
|
|
165
|
+
* Implementing the contract:
|
|
166
|
+
*
|
|
167
|
+
* class HttpInterfaceImpl extends InterfaceBuilder<
|
|
168
|
+
* RouteBinding,
|
|
169
|
+
* Koa.Middleware,
|
|
170
|
+
* RouteBinding, // `from` source shape (forwarded route)
|
|
171
|
+
* Koa | NwireInterface, // `mount` target shape
|
|
172
|
+
* RawHttpHandler // boot artifact
|
|
173
|
+
* > {
|
|
174
|
+
* readonly transport = "http" as const;
|
|
175
|
+
* use(...mw) { … return this; }
|
|
176
|
+
* wire(binding, handler) { … return this; }
|
|
177
|
+
* from(src) { … return this; }
|
|
178
|
+
* mount(target) { … return this; }
|
|
179
|
+
* async run(opts) { … return lifecycle; }
|
|
180
|
+
* async boot(opts) { … return booted; }
|
|
181
|
+
* manifest() { … return data; }
|
|
182
|
+
* attach(host) { … }
|
|
183
|
+
* }
|
|
184
|
+
*/
|
|
185
|
+
export declare abstract class InterfaceBuilder<TBinding = unknown, TPlugin = unknown, TFromSource = unknown, TMountTarget = unknown, TArtifact = unknown> implements NwireInterface {
|
|
186
|
+
readonly $nwireServable: true;
|
|
187
|
+
/** Transport id — `"http"` / `"queue"` / `"mcp"` / `"graphql"` / … */
|
|
188
|
+
abstract readonly transport: string;
|
|
189
|
+
/** Augment the chain with one or more plugins/middleware values. */
|
|
190
|
+
abstract use(...plugins: TPlugin[]): this;
|
|
191
|
+
/**
|
|
192
|
+
* Bind an outbound surface (route / topic / tool / channel) to a handler.
|
|
193
|
+
* `handler` may be a `HandlerDefinition` (typed + validated) or a bare
|
|
194
|
+
* async function. Transports may overload to accept per-binding options.
|
|
195
|
+
*/
|
|
196
|
+
abstract wire(binding: TBinding, handler?: HandlerLike, options?: unknown): this;
|
|
197
|
+
/**
|
|
198
|
+
* Declare an inbound stream the interface consumes. Semantically the
|
|
199
|
+
* opposite of `.wire()` — `.wire()` exposes; `.from()` ingests. Each
|
|
200
|
+
* transport defines what counts as a source:
|
|
201
|
+
*
|
|
202
|
+
* - HTTP: a remote route or upstream API to proxy/forward
|
|
203
|
+
* - queue: an event definition or another queue to consume from
|
|
204
|
+
* - MCP: a remote tool list to re-export
|
|
205
|
+
* - GraphQL: a remote schema to stitch
|
|
206
|
+
*
|
|
207
|
+
* Transports that have no meaningful inbound concept omit `.from()` from
|
|
208
|
+
* their concrete class signature (the abstract declaration here exists
|
|
209
|
+
* so the universal verb table is uniform on the type level).
|
|
210
|
+
*/
|
|
211
|
+
abstract from(source: TFromSource): this;
|
|
212
|
+
/**
|
|
213
|
+
* Mount this interface onto an external host. Returns the host (so the
|
|
214
|
+
* caller can use it), not `this`. Transports type the target shape:
|
|
215
|
+
*
|
|
216
|
+
* - HTTP: Koa | express.Application | NwireInterface
|
|
217
|
+
* - queue: QueueWorker | NwireInterface
|
|
218
|
+
* - MCP: existing MCP server | NwireInterface
|
|
219
|
+
*
|
|
220
|
+
* Use this for interop (mounting on Express / Fastify) or for composing
|
|
221
|
+
* one interface inside another (HTTP gateway → queue producer).
|
|
222
|
+
*/
|
|
223
|
+
abstract mount(target: TMountTarget, options?: unknown): unknown;
|
|
224
|
+
/**
|
|
225
|
+
* Boot the interface (validate, freeze, build internals) and serve it
|
|
226
|
+
* standalone. Installs default signal handlers unless opts say
|
|
227
|
+
* otherwise. Returns a `Lifecycle` the caller can close to drain.
|
|
228
|
+
*
|
|
229
|
+
* Use this when the interface is the entire app. For multi-interface
|
|
230
|
+
* apps, prefer `@nwire/endpoint` which orchestrates several `.boot()`d
|
|
231
|
+
* interfaces together with full lightship + http-terminator semantics.
|
|
232
|
+
*/
|
|
233
|
+
abstract run(options?: RunOptions): Promise<Lifecycle>;
|
|
234
|
+
/**
|
|
235
|
+
* Build the interface's runnable artifact WITHOUT binding a port. The
|
|
236
|
+
* host (typically `@nwire/endpoint`) calls this, takes ownership of the
|
|
237
|
+
* returned `Booted` handle, and decides when to start serving.
|
|
238
|
+
*
|
|
239
|
+
* Boot is the "compile + freeze" moment — after `.boot()` returns,
|
|
240
|
+
* additional `.use()` / `.wire()` calls are not honored on the booted
|
|
241
|
+
* artifact (the builder still mutates, but a re-boot is required to
|
|
242
|
+
* pick up changes).
|
|
243
|
+
*/
|
|
244
|
+
abstract boot(options?: BootOptions): Promise<Booted<TArtifact>>;
|
|
245
|
+
/**
|
|
246
|
+
* Pure-data snapshot of the wired surface. Scanner, Studio, and the
|
|
247
|
+
* cache read this. Calling `.manifest()` does NOT freeze the builder —
|
|
248
|
+
* the manifest is a copy taken at the call site.
|
|
249
|
+
*/
|
|
250
|
+
abstract manifest(): InterfaceManifest;
|
|
251
|
+
/**
|
|
252
|
+
* Lifecycle hook called by a host that's composing this interface into
|
|
253
|
+
* a larger app. The host passes its container + a way to register
|
|
254
|
+
* health checks. Default no-op subclasses override as needed.
|
|
255
|
+
*/
|
|
256
|
+
attach(_host: AttachBindings): void;
|
|
257
|
+
/**
|
|
258
|
+
* Builders accumulated via `.provide()`. Transports apply each per
|
|
259
|
+
* request/job/invocation, spreading the merged result onto handler ctx.
|
|
260
|
+
* Last-write wins on key collision.
|
|
261
|
+
*/
|
|
262
|
+
protected readonly ctxBuilders: CtxBuilder<object>[];
|
|
263
|
+
/**
|
|
264
|
+
* Compose ctx extras for an incoming request. Transports call this in
|
|
265
|
+
* their dispatch loop to build the per-invocation extras object that
|
|
266
|
+
* gets spread onto handler ctx.
|
|
267
|
+
*
|
|
268
|
+
* The interface is agnostic to where the values come from. A wire
|
|
269
|
+
* builder may close over a container, read req headers, mint a request
|
|
270
|
+
* id, fetch a tenant — all opaque to the transport.
|
|
271
|
+
*/
|
|
272
|
+
protected composeCtxExtras(request: unknown): Promise<Record<string, unknown>>;
|
|
273
|
+
/**
|
|
274
|
+
* Register a ctx builder applied per request/job/invocation.
|
|
275
|
+
*
|
|
276
|
+
* - Static extras: every request gets the same object merged in.
|
|
277
|
+
* - Builder fn: receives the transport's native request value (`req`,
|
|
278
|
+
* `job`, `evt`), returns extras to merge onto ctx.
|
|
279
|
+
*
|
|
280
|
+
* Multiple `.provide()` calls compose — each builder runs in
|
|
281
|
+
* registration order; later writes win on key collision.
|
|
282
|
+
*
|
|
283
|
+
* api.provide({ logger: console }); // static
|
|
284
|
+
* api.provide((req) => ({ user: req.user })); // dynamic
|
|
285
|
+
* api.provide((req) => ({ ...root.cradle, tenant: req.tenant })); // closes over container
|
|
286
|
+
*
|
|
287
|
+
* The interface NEVER imports `@nwire/container`. Whatever the wire
|
|
288
|
+
* passes is opaque here.
|
|
289
|
+
*/
|
|
290
|
+
provide<TExtras extends object>(builder: CtxBuilder<TExtras>): this;
|
|
291
|
+
}
|
|
292
|
+
/** Type narrow — does this value look like a Nwire interface? */
|
|
293
|
+
export declare function isNwireInterface(x: unknown): x is NwireInterface;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `InterfaceBuilder` — the abstract base every Nwire transport extends.
|
|
3
|
+
*
|
|
4
|
+
* Six verbs are universal and form the public surface every transport
|
|
5
|
+
* supports (HTTP, queue, MCP, GraphQL, WebSocket, CLI, …):
|
|
6
|
+
*
|
|
7
|
+
* .use(...plugins) augment the chain (middleware/plugins)
|
|
8
|
+
* .wire(binding, handler?, o?) bind an outbound surface to a handler
|
|
9
|
+
* .from(source) declare an inbound stream the iface consumes
|
|
10
|
+
* .mount(target) attach this iface to a host (express/koa/…)
|
|
11
|
+
* .run(opts?) serve standalone — returns a Lifecycle
|
|
12
|
+
* .boot(opts?) build internals without listening
|
|
13
|
+
*
|
|
14
|
+
* Two internal seams that aren't verbs:
|
|
15
|
+
*
|
|
16
|
+
* .manifest() pure data describing the wired surface
|
|
17
|
+
* (scanner / Studio / cache consume this)
|
|
18
|
+
* .attach(host) lifecycle hook called by the host endpoint
|
|
19
|
+
* when this interface is mounted on another
|
|
20
|
+
*
|
|
21
|
+
* And one DI shortcut kept for ergonomics:
|
|
22
|
+
*
|
|
23
|
+
* .provide(container) sugar that wires a Container into the chain
|
|
24
|
+
* via the same channel `.use()` uses.
|
|
25
|
+
*
|
|
26
|
+
* Each transport refines the generics:
|
|
27
|
+
* - `TBinding` — outbound binding shape (RouteBinding / JobBinding / …)
|
|
28
|
+
* - `TPlugin` — what `.use()` accepts (Koa middleware / plain fn / …)
|
|
29
|
+
* - `TFromSource` — what `.from()` accepts (eventDef / remote ref / …)
|
|
30
|
+
* - `TMountTarget` — what `.mount()` accepts (Koa app / express / iface)
|
|
31
|
+
* - `TArtifact` — what `.boot()` produces (an opaque handle the host owns)
|
|
32
|
+
*
|
|
33
|
+
* The base owns the type signatures + manifest plumbing. It implements one
|
|
34
|
+
* verb concretely — `.provide()` — as a default that delegates to `.use()`.
|
|
35
|
+
* Transports may override if they need a different DI dispatch shape.
|
|
36
|
+
*/
|
|
37
|
+
/* ─── The abstract base ─────────────────────────────────────────────────── */
|
|
38
|
+
/**
|
|
39
|
+
* Base class every transport extends.
|
|
40
|
+
*
|
|
41
|
+
* Implementing the contract:
|
|
42
|
+
*
|
|
43
|
+
* class HttpInterfaceImpl extends InterfaceBuilder<
|
|
44
|
+
* RouteBinding,
|
|
45
|
+
* Koa.Middleware,
|
|
46
|
+
* RouteBinding, // `from` source shape (forwarded route)
|
|
47
|
+
* Koa | NwireInterface, // `mount` target shape
|
|
48
|
+
* RawHttpHandler // boot artifact
|
|
49
|
+
* > {
|
|
50
|
+
* readonly transport = "http" as const;
|
|
51
|
+
* use(...mw) { … return this; }
|
|
52
|
+
* wire(binding, handler) { … return this; }
|
|
53
|
+
* from(src) { … return this; }
|
|
54
|
+
* mount(target) { … return this; }
|
|
55
|
+
* async run(opts) { … return lifecycle; }
|
|
56
|
+
* async boot(opts) { … return booted; }
|
|
57
|
+
* manifest() { … return data; }
|
|
58
|
+
* attach(host) { … }
|
|
59
|
+
* }
|
|
60
|
+
*/
|
|
61
|
+
export class InterfaceBuilder {
|
|
62
|
+
$nwireServable = true;
|
|
63
|
+
/**
|
|
64
|
+
* Lifecycle hook called by a host that's composing this interface into
|
|
65
|
+
* a larger app. The host passes its container + a way to register
|
|
66
|
+
* health checks. Default no-op subclasses override as needed.
|
|
67
|
+
*/
|
|
68
|
+
attach(_host) {
|
|
69
|
+
// No-op by default. Transports override when they need to adopt the
|
|
70
|
+
// host's container or register transport-specific health checks.
|
|
71
|
+
}
|
|
72
|
+
/* ─── Per-request ctx provider ─────────────────────────────────────── */
|
|
73
|
+
/**
|
|
74
|
+
* Builders accumulated via `.provide()`. Transports apply each per
|
|
75
|
+
* request/job/invocation, spreading the merged result onto handler ctx.
|
|
76
|
+
* Last-write wins on key collision.
|
|
77
|
+
*/
|
|
78
|
+
ctxBuilders = [];
|
|
79
|
+
/**
|
|
80
|
+
* Compose ctx extras for an incoming request. Transports call this in
|
|
81
|
+
* their dispatch loop to build the per-invocation extras object that
|
|
82
|
+
* gets spread onto handler ctx.
|
|
83
|
+
*
|
|
84
|
+
* The interface is agnostic to where the values come from. A wire
|
|
85
|
+
* builder may close over a container, read req headers, mint a request
|
|
86
|
+
* id, fetch a tenant — all opaque to the transport.
|
|
87
|
+
*/
|
|
88
|
+
async composeCtxExtras(request) {
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const builder of this.ctxBuilders) {
|
|
91
|
+
const extras = typeof builder === "function" ? await builder(request) : builder;
|
|
92
|
+
Object.assign(result, extras);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register a ctx builder applied per request/job/invocation.
|
|
98
|
+
*
|
|
99
|
+
* - Static extras: every request gets the same object merged in.
|
|
100
|
+
* - Builder fn: receives the transport's native request value (`req`,
|
|
101
|
+
* `job`, `evt`), returns extras to merge onto ctx.
|
|
102
|
+
*
|
|
103
|
+
* Multiple `.provide()` calls compose — each builder runs in
|
|
104
|
+
* registration order; later writes win on key collision.
|
|
105
|
+
*
|
|
106
|
+
* api.provide({ logger: console }); // static
|
|
107
|
+
* api.provide((req) => ({ user: req.user })); // dynamic
|
|
108
|
+
* api.provide((req) => ({ ...root.cradle, tenant: req.tenant })); // closes over container
|
|
109
|
+
*
|
|
110
|
+
* The interface NEVER imports `@nwire/container`. Whatever the wire
|
|
111
|
+
* passes is opaque here.
|
|
112
|
+
*/
|
|
113
|
+
provide(builder) {
|
|
114
|
+
this.ctxBuilders.push(builder);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/* ─── Structural type narrows ────────────────────────────────────────── */
|
|
119
|
+
/** Type narrow — does this value look like a Nwire interface? */
|
|
120
|
+
export function isNwireInterface(x) {
|
|
121
|
+
return (typeof x === "object" &&
|
|
122
|
+
x !== null &&
|
|
123
|
+
x.$nwireServable === true &&
|
|
124
|
+
typeof x.transport === "string");
|
|
125
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/mcp` — MCP binding factories.
|
|
3
|
+
*
|
|
4
|
+
* import { tool, resource } from "@nwire/wires/mcp";
|
|
5
|
+
*
|
|
6
|
+
* createInterface()
|
|
7
|
+
* .wire(tool("create-order"), createOrder)
|
|
8
|
+
* .wire(resource("orders://{id}"), getOrderResource);
|
|
9
|
+
*
|
|
10
|
+
* The `@nwire/mcp` adopter consumes wires by `$adapter === "mcp"` and
|
|
11
|
+
* registers each binding with the MCP server (`tool` becomes an MCP tool,
|
|
12
|
+
* `resource` becomes a resource template).
|
|
13
|
+
*/
|
|
14
|
+
import type { ZodTypeAny, z } from "zod";
|
|
15
|
+
import type { Binding } from "../index.js";
|
|
16
|
+
type ZodOutput<T> = T extends ZodTypeAny ? z.output<T> : unknown;
|
|
17
|
+
export interface ToolBindingOptions {
|
|
18
|
+
/** Schema for the tool's input arguments. */
|
|
19
|
+
readonly input?: ZodTypeAny;
|
|
20
|
+
/** Human-readable summary surfaced on tool discovery. */
|
|
21
|
+
readonly description?: string;
|
|
22
|
+
readonly source?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ToolBinding<TInput = unknown> extends Binding {
|
|
25
|
+
readonly $kind: "binding";
|
|
26
|
+
readonly $adapter: "mcp";
|
|
27
|
+
readonly kind: "tool";
|
|
28
|
+
readonly tool: string;
|
|
29
|
+
readonly input?: ZodTypeAny;
|
|
30
|
+
readonly description?: string;
|
|
31
|
+
readonly source?: string;
|
|
32
|
+
from(source: string): ToolBinding<TInput>;
|
|
33
|
+
readonly __inputType?: TInput;
|
|
34
|
+
}
|
|
35
|
+
export declare function tool<O extends ToolBindingOptions = ToolBindingOptions>(toolName: string, options?: O): ToolBinding<O extends {
|
|
36
|
+
input: infer I;
|
|
37
|
+
} ? (I extends ZodTypeAny ? ZodOutput<I> : unknown) : unknown>;
|
|
38
|
+
export interface ResourceBindingOptions {
|
|
39
|
+
readonly description?: string;
|
|
40
|
+
readonly mimeType?: string;
|
|
41
|
+
readonly source?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ResourceBinding extends Binding {
|
|
44
|
+
readonly $kind: "binding";
|
|
45
|
+
readonly $adapter: "mcp";
|
|
46
|
+
readonly kind: "resource";
|
|
47
|
+
readonly uriTemplate: string;
|
|
48
|
+
readonly description?: string;
|
|
49
|
+
readonly mimeType?: string;
|
|
50
|
+
readonly source?: string;
|
|
51
|
+
from(source: string): ResourceBinding;
|
|
52
|
+
}
|
|
53
|
+
export declare function resource(uriTemplate: string, options?: ResourceBindingOptions): ResourceBinding;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/mcp` — MCP binding factories.
|
|
3
|
+
*
|
|
4
|
+
* import { tool, resource } from "@nwire/wires/mcp";
|
|
5
|
+
*
|
|
6
|
+
* createInterface()
|
|
7
|
+
* .wire(tool("create-order"), createOrder)
|
|
8
|
+
* .wire(resource("orders://{id}"), getOrderResource);
|
|
9
|
+
*
|
|
10
|
+
* The `@nwire/mcp` adopter consumes wires by `$adapter === "mcp"` and
|
|
11
|
+
* registers each binding with the MCP server (`tool` becomes an MCP tool,
|
|
12
|
+
* `resource` becomes a resource template).
|
|
13
|
+
*/
|
|
14
|
+
function buildToolBinding(toolName, options, source) {
|
|
15
|
+
const binding = {
|
|
16
|
+
$kind: "binding",
|
|
17
|
+
$adapter: "mcp",
|
|
18
|
+
kind: "tool",
|
|
19
|
+
tool: toolName,
|
|
20
|
+
...(options ?? {}),
|
|
21
|
+
...(source !== undefined ? { source } : {}),
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(binding, "from", {
|
|
24
|
+
value: (next) => buildToolBinding(toolName, options, next),
|
|
25
|
+
enumerable: false,
|
|
26
|
+
writable: false,
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
29
|
+
return binding;
|
|
30
|
+
}
|
|
31
|
+
export function tool(toolName, options) {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
return buildToolBinding(toolName, options, undefined);
|
|
34
|
+
}
|
|
35
|
+
function buildResourceBinding(uriTemplate, options, source) {
|
|
36
|
+
const binding = {
|
|
37
|
+
$kind: "binding",
|
|
38
|
+
$adapter: "mcp",
|
|
39
|
+
kind: "resource",
|
|
40
|
+
uriTemplate,
|
|
41
|
+
...(options ?? {}),
|
|
42
|
+
...(source !== undefined ? { source } : {}),
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(binding, "from", {
|
|
45
|
+
value: (next) => buildResourceBinding(uriTemplate, options, next),
|
|
46
|
+
enumerable: false,
|
|
47
|
+
writable: false,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
return binding;
|
|
51
|
+
}
|
|
52
|
+
export function resource(uriTemplate, options) {
|
|
53
|
+
return buildResourceBinding(uriTemplate, options, undefined);
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/queue` — queue binding factory.
|
|
3
|
+
*
|
|
4
|
+
* import { queue } from "@nwire/wires/queue";
|
|
5
|
+
*
|
|
6
|
+
* createInterface().wire(queue("orders.process"), processOrder);
|
|
7
|
+
*
|
|
8
|
+
* The binding carries `$adapter: "queue"` so a queue adopter
|
|
9
|
+
* (`@nwire/bullmq`, `@nwire/queue-redis`, …) filters wires via
|
|
10
|
+
* `interface.forAdapter("queue")`. Adopters interpret the queue-name
|
|
11
|
+
* field as a topic / channel / queue identifier per their impl.
|
|
12
|
+
*/
|
|
13
|
+
import type { ZodTypeAny, z } from "zod";
|
|
14
|
+
import type { Binding } from "../index.js";
|
|
15
|
+
/** Options accepted by `queue()`. */
|
|
16
|
+
export interface QueueBindingOptions {
|
|
17
|
+
/** Schema for the message body. The adopter validates at dispatch. */
|
|
18
|
+
readonly input?: ZodTypeAny;
|
|
19
|
+
/** Free-form trigger source tag — Studio and observability group by it. */
|
|
20
|
+
readonly source?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Per-binding concurrency hint — the adopter may honor or ignore.
|
|
23
|
+
* Default left to the adopter.
|
|
24
|
+
*/
|
|
25
|
+
readonly concurrency?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface QueueBinding<TInput = unknown> extends Binding {
|
|
28
|
+
readonly $kind: "binding";
|
|
29
|
+
readonly $adapter: "queue";
|
|
30
|
+
readonly kind: "queue";
|
|
31
|
+
readonly queue: string;
|
|
32
|
+
readonly input?: ZodTypeAny;
|
|
33
|
+
readonly source?: string;
|
|
34
|
+
readonly concurrency?: number;
|
|
35
|
+
/** Chainable source tag — returns a fresh binding. */
|
|
36
|
+
from(source: string): QueueBinding<TInput>;
|
|
37
|
+
readonly __inputType?: TInput;
|
|
38
|
+
}
|
|
39
|
+
type ZodOutput<T> = T extends ZodTypeAny ? z.output<T> : unknown;
|
|
40
|
+
export declare function queue<O extends QueueBindingOptions = QueueBindingOptions>(queueName: string, options?: O): QueueBinding<O extends {
|
|
41
|
+
input: infer I;
|
|
42
|
+
} ? (I extends ZodTypeAny ? ZodOutput<I> : unknown) : unknown>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/wires/queue` — queue binding factory.
|
|
3
|
+
*
|
|
4
|
+
* import { queue } from "@nwire/wires/queue";
|
|
5
|
+
*
|
|
6
|
+
* createInterface().wire(queue("orders.process"), processOrder);
|
|
7
|
+
*
|
|
8
|
+
* The binding carries `$adapter: "queue"` so a queue adopter
|
|
9
|
+
* (`@nwire/bullmq`, `@nwire/queue-redis`, …) filters wires via
|
|
10
|
+
* `interface.forAdapter("queue")`. Adopters interpret the queue-name
|
|
11
|
+
* field as a topic / channel / queue identifier per their impl.
|
|
12
|
+
*/
|
|
13
|
+
function buildBinding(queueName, options, source) {
|
|
14
|
+
const binding = {
|
|
15
|
+
$kind: "binding",
|
|
16
|
+
$adapter: "queue",
|
|
17
|
+
kind: "queue",
|
|
18
|
+
queue: queueName,
|
|
19
|
+
...(options ?? {}),
|
|
20
|
+
...(source !== undefined ? { source } : {}),
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(binding, "from", {
|
|
23
|
+
value: (next) => buildBinding(queueName, options, next),
|
|
24
|
+
enumerable: false,
|
|
25
|
+
writable: false,
|
|
26
|
+
configurable: true,
|
|
27
|
+
});
|
|
28
|
+
return binding;
|
|
29
|
+
}
|
|
30
|
+
export function queue(queueName, options) {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
return buildBinding(queueName, options, undefined);
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/wires",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Nwire — transport-interface contract. `InterfaceBuilder` is the abstract base every transport (HTTP, queue, MCP, GraphQL, …) extends. Foreign hosts implement it. Six universal verbs (.use / .wire / .from / .mount / .run / .boot) plus manifest + attach seams are typed at this layer so transports stay consistent.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"interface",
|
|
7
|
+
"nwire",
|
|
8
|
+
"transport"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./http": {
|
|
25
|
+
"import": "./dist/http/index.js",
|
|
26
|
+
"types": "./dist/http/index.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./queue": {
|
|
29
|
+
"import": "./dist/queue/index.js",
|
|
30
|
+
"types": "./dist/queue/index.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./cron": {
|
|
33
|
+
"import": "./dist/cron/index.js",
|
|
34
|
+
"types": "./dist/cron/index.d.ts"
|
|
35
|
+
},
|
|
36
|
+
"./mcp": {
|
|
37
|
+
"import": "./dist/mcp/index.js",
|
|
38
|
+
"types": "./dist/mcp/index.d.ts"
|
|
39
|
+
},
|
|
40
|
+
"./graphql": {
|
|
41
|
+
"import": "./dist/graphql/index.js",
|
|
42
|
+
"types": "./dist/graphql/index.d.ts"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"zod": "^4.0.0",
|
|
50
|
+
"@nwire/handler": "0.10.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.19.9",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.0.18"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
59
|
+
"dev": "tsc --watch",
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
61
|
+
}
|
|
62
|
+
}
|