@adhd/apigen-gateway 0.1.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/index.d.ts +2 -0
- package/index.js +1 -0
- package/index.mjs +229 -0
- package/lib/gateway.d.ts +219 -0
- package/package.json +14 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const __apigen_pkg = "@adhd/apigen-gateway";
|
|
2
|
+
export { createGateway, type Gateway, type GatewayOptions, type GatewayHealth, type HostHealth, type HostStatus, type HostAdapter, type HostRequest, type InProcessRuntime, createInProcessHostAdapter, type BackoffPolicy, defaultBackoff, GATEWAY_ERROR_CODES, type GatewayErrorCode, GATEWAY_HTTP_STATUS, GATEWAY_GRPC_CODE, type GatewayErrorDetail, makeUnavailableError, makeDeadlineExceededError, isGatewayError, } from './lib/gateway';
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class m extends Error{constructor(r,i,o){super(i),this.name="ApiError",this.code=r,this.details=o,Object.setPrototypeOf(this,new.target.prototype)}toJSON(){const r={code:this.code,message:this.message};return this.details!==void 0&&(r.details=this.details),r}}const S=["unavailable","deadline_exceeded"],v={unavailable:503,deadline_exceeded:504},g={unavailable:"UNAVAILABLE",deadline_exceeded:"DEADLINE_EXCEEDED"};function l(s,r,i){const o={gatewayCode:"unavailable",host:s,operationId:r,httpStatus:v.unavailable,grpcCode:g.unavailable};return new m("internal",`host '${s}' unavailable: ${i}`,o)}function T(s,r,i){const o={gatewayCode:"deadline_exceeded",host:s,operationId:r,httpStatus:v.deadline_exceeded,grpcCode:g.deadline_exceeded};return new m("internal",`op '${r}' on host '${s}' exceeded ${i}ms deadline`,o)}function C(s){if(!(s instanceof m)||s.details==null||typeof s.details!="object")return!1;const r=s.details;return r.gatewayCode==="unavailable"||r.gatewayCode==="deadline_exceeded"}function D(s,r,i={}){let o=!1;const n=i.readyWhenStarted??!0;return{host:s,hopCost:0,async start(){o=!0},async ready(){return o&&n},async invoke(c,w){if(!o)throw l(s,c.operation.id,"not started");return r.invoke(c,w)},onExit(){return()=>{}},async stop(){o=!1}}}const k={delayMs(s){return Math.min(50*2**(s-1),5e3)},maxRestarts:0};function P(s){const r=s.defaultDeadlineMs??3e4,i=s.backoff??k,o=s.timers??{setTimeout:(e,a)=>setTimeout(e,a),clearTimeout:e=>clearTimeout(e)},n=new Map;for(const e of s.adapters){if(n.has(e.host))throw new Error(`apigen-gateway: duplicate host adapter '${e.host}'`);n.set(e.host,{adapter:e,status:"down",restarts:0,unsubscribe:()=>{},retired:!1})}function c(e){return new Promise(a=>{if(e<=0){a();return}o.setTimeout(a,e)})}async function w(e){if(e.retired)return;e.unsubscribe(),e.unsubscribe=e.adapter.onExit(t=>{x(e)}),await e.adapter.start();let a=!1;try{a=await e.adapter.ready()}catch{a=!1}e.retired||(e.status=a?"ready":"degraded")}async function x(e,a){if(e.retired)return;e.status="down";let t=0;for(;!e.retired;){if(t+=1,e.restarts+=1,i.maxRestarts>0&&t>i.maxRestarts){e.status="down";return}if(await c(i.delayMs(t)),e.retired)return;try{await e.adapter.start();const d=await e.adapter.ready();if(e.retired)return;if(d){e.status="ready";return}e.status="degraded"}catch{e.status="down"}}}async function _(e){if(e.retired||e.status==="down")return;let a=!1;try{a=await e.adapter.ready()}catch{a=!1}e.status=a?"ready":"degraded"}function y(e,a,t,d){return new Promise((E,b)=>{let u=!1;const f=new AbortController,p=()=>f.abort(t.reason);t.aborted?f.abort(t.reason):t.addEventListener("abort",p,{once:!0});const A=o.setTimeout(()=>{u||(u=!0,t.removeEventListener("abort",p),f.abort("deadline"),b(T(e.adapter.host,a.operation.id,d)))},d);e.adapter.invoke(a,f.signal).then(h=>{u||(u=!0,o.clearTimeout(A),t.removeEventListener("abort",p),E(h))},h=>{u||(u=!0,o.clearTimeout(A),t.removeEventListener("abort",p),e.status!=="ready"&&!C(h)?b(l(e.adapter.host,a.operation.id,"host not serving")):b(h))})})}return{async start(){await Promise.all([...n.values()].map(e=>w(e)))},async route(e,a){const t=e.operation.host,d=n.get(t);if(d==null)throw l(t,e.operation.id,"no adapter registered for host");if(d.status==="degraded"&&await _(d),d.status!=="ready")throw l(t,e.operation.id,`host status '${d.status}'`);const E=a??new AbortController().signal;return y(d,e,E,r)},async health(){const e={};let a=!1;return await Promise.all([...n.values()].map(async t=>{t.status==="degraded"&&await _(t),t.status==="ready"&&(a=!0),e[t.adapter.host]={host:t.adapter.host,status:t.status,hopCost:t.adapter.hopCost,restarts:t.restarts}})),{hosts:e,serving:a}},topologyCost(){let e=0;for(const a of n.values())e+=a.adapter.hopCost;return e},async stop(){await Promise.all([...n.values()].map(async e=>{e.retired=!0,e.unsubscribe(),e.status="down",await e.adapter.stop()}))}}}const R="@adhd/apigen-gateway";exports.GATEWAY_ERROR_CODES=S;exports.GATEWAY_GRPC_CODE=g;exports.GATEWAY_HTTP_STATUS=v;exports.__apigen_pkg=R;exports.createGateway=P;exports.createInProcessHostAdapter=D;exports.defaultBackoff=k;exports.isGatewayError=C;exports.makeDeadlineExceededError=T;exports.makeUnavailableError=l;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
class v extends Error {
|
|
2
|
+
constructor(r, i, o) {
|
|
3
|
+
super(i), this.name = "ApiError", this.code = r, this.details = o, Object.setPrototypeOf(this, new.target.prototype);
|
|
4
|
+
}
|
|
5
|
+
/** Serialise to a plain object suitable for JSON transport. */
|
|
6
|
+
toJSON() {
|
|
7
|
+
const r = {
|
|
8
|
+
code: this.code,
|
|
9
|
+
message: this.message
|
|
10
|
+
};
|
|
11
|
+
return this.details !== void 0 && (r.details = this.details), r;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const D = ["unavailable", "deadline_exceeded"], _ = {
|
|
15
|
+
unavailable: 503,
|
|
16
|
+
deadline_exceeded: 504
|
|
17
|
+
}, A = {
|
|
18
|
+
unavailable: "UNAVAILABLE",
|
|
19
|
+
deadline_exceeded: "DEADLINE_EXCEEDED"
|
|
20
|
+
};
|
|
21
|
+
function h(s, r, i) {
|
|
22
|
+
const o = {
|
|
23
|
+
gatewayCode: "unavailable",
|
|
24
|
+
host: s,
|
|
25
|
+
operationId: r,
|
|
26
|
+
httpStatus: _.unavailable,
|
|
27
|
+
grpcCode: A.unavailable
|
|
28
|
+
};
|
|
29
|
+
return new v("internal", `host '${s}' unavailable: ${i}`, o);
|
|
30
|
+
}
|
|
31
|
+
function T(s, r, i) {
|
|
32
|
+
const o = {
|
|
33
|
+
gatewayCode: "deadline_exceeded",
|
|
34
|
+
host: s,
|
|
35
|
+
operationId: r,
|
|
36
|
+
httpStatus: _.deadline_exceeded,
|
|
37
|
+
grpcCode: A.deadline_exceeded
|
|
38
|
+
};
|
|
39
|
+
return new v("internal", `op '${r}' on host '${s}' exceeded ${i}ms deadline`, o);
|
|
40
|
+
}
|
|
41
|
+
function k(s) {
|
|
42
|
+
if (!(s instanceof v) || s.details == null || typeof s.details != "object")
|
|
43
|
+
return !1;
|
|
44
|
+
const r = s.details;
|
|
45
|
+
return r.gatewayCode === "unavailable" || r.gatewayCode === "deadline_exceeded";
|
|
46
|
+
}
|
|
47
|
+
function P(s, r, i = {}) {
|
|
48
|
+
let o = !1;
|
|
49
|
+
const n = i.readyWhenStarted ?? !0;
|
|
50
|
+
return {
|
|
51
|
+
host: s,
|
|
52
|
+
hopCost: 0,
|
|
53
|
+
async start() {
|
|
54
|
+
o = !0;
|
|
55
|
+
},
|
|
56
|
+
async ready() {
|
|
57
|
+
return o && n;
|
|
58
|
+
},
|
|
59
|
+
async invoke(l, w) {
|
|
60
|
+
if (!o)
|
|
61
|
+
throw h(s, l.operation.id, "not started");
|
|
62
|
+
return r.invoke(l, w);
|
|
63
|
+
},
|
|
64
|
+
onExit() {
|
|
65
|
+
return () => {
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
async stop() {
|
|
69
|
+
o = !1;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const y = {
|
|
74
|
+
delayMs(s) {
|
|
75
|
+
return Math.min(50 * 2 ** (s - 1), 5e3);
|
|
76
|
+
},
|
|
77
|
+
maxRestarts: 0
|
|
78
|
+
};
|
|
79
|
+
function R(s) {
|
|
80
|
+
const r = s.defaultDeadlineMs ?? 3e4, i = s.backoff ?? y, o = s.timers ?? {
|
|
81
|
+
setTimeout: (e, a) => setTimeout(e, a),
|
|
82
|
+
clearTimeout: (e) => clearTimeout(e)
|
|
83
|
+
}, n = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const e of s.adapters) {
|
|
85
|
+
if (n.has(e.host))
|
|
86
|
+
throw new Error(`apigen-gateway: duplicate host adapter '${e.host}'`);
|
|
87
|
+
n.set(e.host, {
|
|
88
|
+
adapter: e,
|
|
89
|
+
status: "down",
|
|
90
|
+
restarts: 0,
|
|
91
|
+
unsubscribe: () => {
|
|
92
|
+
},
|
|
93
|
+
retired: !1
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function l(e) {
|
|
97
|
+
return new Promise((a) => {
|
|
98
|
+
if (e <= 0) {
|
|
99
|
+
a();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
o.setTimeout(a, e);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async function w(e) {
|
|
106
|
+
if (e.retired)
|
|
107
|
+
return;
|
|
108
|
+
e.unsubscribe(), e.unsubscribe = e.adapter.onExit((t) => {
|
|
109
|
+
C(e);
|
|
110
|
+
}), await e.adapter.start();
|
|
111
|
+
let a = !1;
|
|
112
|
+
try {
|
|
113
|
+
a = await e.adapter.ready();
|
|
114
|
+
} catch {
|
|
115
|
+
a = !1;
|
|
116
|
+
}
|
|
117
|
+
e.retired || (e.status = a ? "ready" : "degraded");
|
|
118
|
+
}
|
|
119
|
+
async function C(e, a) {
|
|
120
|
+
if (e.retired)
|
|
121
|
+
return;
|
|
122
|
+
e.status = "down";
|
|
123
|
+
let t = 0;
|
|
124
|
+
for (; !e.retired; ) {
|
|
125
|
+
if (t += 1, e.restarts += 1, i.maxRestarts > 0 && t > i.maxRestarts) {
|
|
126
|
+
e.status = "down";
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (await l(i.delayMs(t)), e.retired)
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
await e.adapter.start();
|
|
133
|
+
const d = await e.adapter.ready();
|
|
134
|
+
if (e.retired)
|
|
135
|
+
return;
|
|
136
|
+
if (d) {
|
|
137
|
+
e.status = "ready";
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
e.status = "degraded";
|
|
141
|
+
} catch {
|
|
142
|
+
e.status = "down";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function E(e) {
|
|
147
|
+
if (e.retired || e.status === "down")
|
|
148
|
+
return;
|
|
149
|
+
let a = !1;
|
|
150
|
+
try {
|
|
151
|
+
a = await e.adapter.ready();
|
|
152
|
+
} catch {
|
|
153
|
+
a = !1;
|
|
154
|
+
}
|
|
155
|
+
e.status = a ? "ready" : "degraded";
|
|
156
|
+
}
|
|
157
|
+
function x(e, a, t, d) {
|
|
158
|
+
return new Promise((b, m) => {
|
|
159
|
+
let u = !1;
|
|
160
|
+
const c = new AbortController(), f = () => c.abort(t.reason);
|
|
161
|
+
t.aborted ? c.abort(t.reason) : t.addEventListener("abort", f, { once: !0 });
|
|
162
|
+
const g = o.setTimeout(() => {
|
|
163
|
+
u || (u = !0, t.removeEventListener("abort", f), c.abort("deadline"), m(T(e.adapter.host, a.operation.id, d)));
|
|
164
|
+
}, d);
|
|
165
|
+
e.adapter.invoke(a, c.signal).then(
|
|
166
|
+
(p) => {
|
|
167
|
+
u || (u = !0, o.clearTimeout(g), t.removeEventListener("abort", f), b(p));
|
|
168
|
+
},
|
|
169
|
+
(p) => {
|
|
170
|
+
u || (u = !0, o.clearTimeout(g), t.removeEventListener("abort", f), e.status !== "ready" && !k(p) ? m(h(e.adapter.host, a.operation.id, "host not serving")) : m(p));
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
async start() {
|
|
177
|
+
await Promise.all([...n.values()].map((e) => w(e)));
|
|
178
|
+
},
|
|
179
|
+
async route(e, a) {
|
|
180
|
+
const t = e.operation.host, d = n.get(t);
|
|
181
|
+
if (d == null)
|
|
182
|
+
throw h(t, e.operation.id, "no adapter registered for host");
|
|
183
|
+
if (d.status === "degraded" && await E(d), d.status !== "ready")
|
|
184
|
+
throw h(t, e.operation.id, `host status '${d.status}'`);
|
|
185
|
+
const b = a ?? new AbortController().signal;
|
|
186
|
+
return x(d, e, b, r);
|
|
187
|
+
},
|
|
188
|
+
async health() {
|
|
189
|
+
const e = {};
|
|
190
|
+
let a = !1;
|
|
191
|
+
return await Promise.all(
|
|
192
|
+
[...n.values()].map(async (t) => {
|
|
193
|
+
t.status === "degraded" && await E(t), t.status === "ready" && (a = !0), e[t.adapter.host] = {
|
|
194
|
+
host: t.adapter.host,
|
|
195
|
+
status: t.status,
|
|
196
|
+
hopCost: t.adapter.hopCost,
|
|
197
|
+
restarts: t.restarts
|
|
198
|
+
};
|
|
199
|
+
})
|
|
200
|
+
), { hosts: e, serving: a };
|
|
201
|
+
},
|
|
202
|
+
topologyCost() {
|
|
203
|
+
let e = 0;
|
|
204
|
+
for (const a of n.values())
|
|
205
|
+
e += a.adapter.hopCost;
|
|
206
|
+
return e;
|
|
207
|
+
},
|
|
208
|
+
async stop() {
|
|
209
|
+
await Promise.all(
|
|
210
|
+
[...n.values()].map(async (e) => {
|
|
211
|
+
e.retired = !0, e.unsubscribe(), e.status = "down", await e.adapter.stop();
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const S = "@adhd/apigen-gateway";
|
|
218
|
+
export {
|
|
219
|
+
D as GATEWAY_ERROR_CODES,
|
|
220
|
+
A as GATEWAY_GRPC_CODE,
|
|
221
|
+
_ as GATEWAY_HTTP_STATUS,
|
|
222
|
+
S as __apigen_pkg,
|
|
223
|
+
R as createGateway,
|
|
224
|
+
P as createInProcessHostAdapter,
|
|
225
|
+
y as defaultBackoff,
|
|
226
|
+
k as isGatewayError,
|
|
227
|
+
T as makeDeadlineExceededError,
|
|
228
|
+
h as makeUnavailableError
|
|
229
|
+
};
|
package/lib/gateway.d.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Operation, Transport } from '@adhd/apigen-core';
|
|
2
|
+
import { ApiError } from '@adhd/apigen-errors';
|
|
3
|
+
|
|
4
|
+
/** The distributed-system error codes the gateway raises (SPEC §13.1). */
|
|
5
|
+
export declare const GATEWAY_ERROR_CODES: readonly ["unavailable", "deadline_exceeded"];
|
|
6
|
+
/** String-union type for the gateway error codes. */
|
|
7
|
+
export type GatewayErrorCode = (typeof GATEWAY_ERROR_CODES)[number];
|
|
8
|
+
/** code → HTTP status (SPEC §9.1: unavailable = 503, deadline_exceeded = 504). */
|
|
9
|
+
export declare const GATEWAY_HTTP_STATUS: Record<GatewayErrorCode, number>;
|
|
10
|
+
/** code → gRPC status name (SPEC §9.1). */
|
|
11
|
+
export declare const GATEWAY_GRPC_CODE: Record<GatewayErrorCode, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Structured detail attached to a gateway `ApiError` so transport adapters can read
|
|
14
|
+
* the distributed-system code and host without string-matching the message.
|
|
15
|
+
*/
|
|
16
|
+
export interface GatewayErrorDetail {
|
|
17
|
+
/** The §13.1 code: `unavailable` or `deadline_exceeded`. */
|
|
18
|
+
gatewayCode: GatewayErrorCode;
|
|
19
|
+
/** The host whose op failed. */
|
|
20
|
+
host: string;
|
|
21
|
+
/** The operation id that was being routed. */
|
|
22
|
+
operationId: string;
|
|
23
|
+
/** Suggested HTTP status for the transport adapter. */
|
|
24
|
+
httpStatus: number;
|
|
25
|
+
/** Suggested gRPC status name for the transport adapter. */
|
|
26
|
+
grpcCode: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the `ApiError` raised when a host is not serving (down / not-ready / restarting).
|
|
30
|
+
* Maps to the core `internal` taxonomy slot but carries the real §13.1 `unavailable`
|
|
31
|
+
* code in `details.gatewayCode` (and a 503 hint) — the shared `ApiError` type has no
|
|
32
|
+
* `unavailable` member, so the gateway code travels in `details`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function makeUnavailableError(host: string, operationId: string, reason: string): ApiError;
|
|
35
|
+
/**
|
|
36
|
+
* Build the `ApiError` raised when a cross-host op exceeds its deadline (SPEC §13.1).
|
|
37
|
+
* Carries the real `deadline_exceeded` code + a 504 hint in `details`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function makeDeadlineExceededError(host: string, operationId: string, deadlineMs: number): ApiError;
|
|
40
|
+
/** Type guard: was this `ApiError` raised by the gateway failure model? */
|
|
41
|
+
export declare function isGatewayError(err: unknown): err is ApiError & {
|
|
42
|
+
details: GatewayErrorDetail;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A host's status as reported by the gateway's aggregate `_meta/health` (SPEC §13.1):
|
|
46
|
+
*
|
|
47
|
+
* - `ready` — the sidecar reported ready via its `_meta/health` mount; ops route.
|
|
48
|
+
* - `degraded` — the host is up but its readiness probe is failing (ops do NOT route).
|
|
49
|
+
* - `down` — the host crashed / is not spawned / is restarting; ops do NOT route.
|
|
50
|
+
*/
|
|
51
|
+
export type HostStatus = 'ready' | 'degraded' | 'down';
|
|
52
|
+
/**
|
|
53
|
+
* The single request the gateway forwards to a host runtime over its IPC.
|
|
54
|
+
*
|
|
55
|
+
* Mirrors the cross-host-portable subset of a core `Call`: the operation, the bare domain
|
|
56
|
+
* `data`, the metadata `envelope`, and the transport tag. `signal` and the deadline are
|
|
57
|
+
* handled by the gateway around the adapter call — they do not cross the IPC boundary as
|
|
58
|
+
* part of this struct (the adapter receives `signal` as a separate argument so it can
|
|
59
|
+
* propagate cancellation natively).
|
|
60
|
+
*/
|
|
61
|
+
export interface HostRequest {
|
|
62
|
+
/** The operation being invoked (carries `operation.host` → routing key). */
|
|
63
|
+
operation: Operation;
|
|
64
|
+
/** Bare domain params (the `data`-wrapper dissolved; ctx excluded). */
|
|
65
|
+
data: Record<string, unknown>;
|
|
66
|
+
/** Transport-native side-channel metadata (session, auth, …). */
|
|
67
|
+
envelope: Record<string, unknown>;
|
|
68
|
+
/** Which transport delivered the original call. */
|
|
69
|
+
transport: Transport;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A host runtime as seen by the gateway (SPEC §14.4 "gateway adapter").
|
|
73
|
+
*
|
|
74
|
+
* This is the ONE seam every host language plugs into. The in-process TS adapter
|
|
75
|
+
* ({@link createInProcessHostAdapter}) calls the runtime directly (zero hop); the
|
|
76
|
+
* out-of-process Python adapter (the `python-host` state) spawns a subprocess and speaks
|
|
77
|
+
* the IPC (one round-trip); the in-memory fake (tests) closes over a map. The gateway is
|
|
78
|
+
* written ONLY against this interface — it never knows which kind it routes to.
|
|
79
|
+
*/
|
|
80
|
+
export interface HostAdapter {
|
|
81
|
+
/** The host tag this adapter serves (matches `operation.host`). */
|
|
82
|
+
readonly host: string;
|
|
83
|
+
/**
|
|
84
|
+
* Routing cost of this host, in IPC round-trips per op (SPEC §13.1 cost function):
|
|
85
|
+
* - `0` — in-process (zero hop; the TS fast path).
|
|
86
|
+
* - `1` — out-of-process sidecar (serialize → IPC → deserialize).
|
|
87
|
+
* The CLI's "simplest viable topology" selector minimises the sum of these.
|
|
88
|
+
*/
|
|
89
|
+
readonly hopCost: 0 | 1;
|
|
90
|
+
/**
|
|
91
|
+
* Bring the host up (spawn the sidecar / initialise the in-process runtime). Resolves
|
|
92
|
+
* when the process exists — NOT when it is ready. Readiness is reported separately via
|
|
93
|
+
* {@link ready}. Idempotent: calling `start()` on an already-started adapter is a no-op.
|
|
94
|
+
*/
|
|
95
|
+
start(): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Readiness probe — the gateway calls this to learn whether the host's `_meta/health`
|
|
98
|
+
* mount reports ready (SPEC §13.1). The gateway routes the host's ops ONLY when this
|
|
99
|
+
* resolves `true`. Must not throw; a failed probe resolves `false`.
|
|
100
|
+
*/
|
|
101
|
+
ready(): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Forward a single operation to the host runtime and return its result.
|
|
104
|
+
*
|
|
105
|
+
* The gateway wraps this in the deadline timer and cancellation wiring — the adapter
|
|
106
|
+
* receives `signal` so it can abort native work (HTTP abort / kill the in-flight IPC).
|
|
107
|
+
* The adapter MUST reject if the host process has died mid-call (the gateway maps that
|
|
108
|
+
* to `unavailable` and triggers supervision/restart).
|
|
109
|
+
*/
|
|
110
|
+
invoke(req: HostRequest, signal: AbortSignal): Promise<unknown>;
|
|
111
|
+
/**
|
|
112
|
+
* Register a one-shot listener fired when the host process exits unexpectedly (crash).
|
|
113
|
+
* The gateway uses this to drive supervision/restart (SPEC §13.1). In-process adapters
|
|
114
|
+
* that cannot crash may install a no-op. Returns an unsubscribe function.
|
|
115
|
+
*/
|
|
116
|
+
onExit(listener: (reason: unknown) => void): () => void;
|
|
117
|
+
/** Tear the host down gracefully (kill the sidecar / release resources). */
|
|
118
|
+
stop(): Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The runtime contract the in-process adapter calls — the TS harness's `invoke`, reduced
|
|
122
|
+
* to the cross-host-portable shape. The real `@adhd/apigen-ts-runtime` harness satisfies
|
|
123
|
+
* this; tests pass a plain function.
|
|
124
|
+
*/
|
|
125
|
+
export interface InProcessRuntime {
|
|
126
|
+
invoke(req: HostRequest, signal: AbortSignal): Promise<unknown>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create the in-process host adapter (SPEC §13.1 single-host fast path / zero hop).
|
|
130
|
+
*
|
|
131
|
+
* Wraps a same-process runtime so the gateway can treat the local TS host identically to
|
|
132
|
+
* a remote sidecar — but every call is a direct function invocation (`hopCost: 0`). An
|
|
133
|
+
* in-process runtime cannot "crash" the way a subprocess can, so `onExit` is a no-op and
|
|
134
|
+
* `ready()` returns the supplied readiness flag (default: ready once started).
|
|
135
|
+
*/
|
|
136
|
+
export declare function createInProcessHostAdapter(host: string, runtime: InProcessRuntime, opts?: {
|
|
137
|
+
readyWhenStarted?: boolean;
|
|
138
|
+
}): HostAdapter;
|
|
139
|
+
/**
|
|
140
|
+
* Backoff policy for restarting a crashed sidecar (SPEC §13.1 supervision).
|
|
141
|
+
* Exponential with a cap; the gateway sleeps `delayMs(attempt)` between restart attempts.
|
|
142
|
+
*/
|
|
143
|
+
export interface BackoffPolicy {
|
|
144
|
+
/** Delay before restart attempt `n` (1-based). */
|
|
145
|
+
delayMs(attempt: number): number;
|
|
146
|
+
/** Max consecutive restart attempts before the host is parked `down` (0 = unlimited). */
|
|
147
|
+
maxRestarts: number;
|
|
148
|
+
}
|
|
149
|
+
/** Default exponential backoff: 50ms · 2^(n-1), capped at 5s, unlimited restarts. */
|
|
150
|
+
export declare const defaultBackoff: BackoffPolicy;
|
|
151
|
+
/** Construction options for {@link createGateway}. */
|
|
152
|
+
export interface GatewayOptions {
|
|
153
|
+
/** The host adapters to route to, keyed by `operation.host`. */
|
|
154
|
+
adapters: HostAdapter[];
|
|
155
|
+
/** Default per-call deadline in ms when the call carries no signal-derived deadline. */
|
|
156
|
+
defaultDeadlineMs?: number;
|
|
157
|
+
/** Restart-with-backoff policy for crashed sidecars (default: {@link defaultBackoff}). */
|
|
158
|
+
backoff?: BackoffPolicy;
|
|
159
|
+
/**
|
|
160
|
+
* Injectable timer hooks — let tests drive deadlines deterministically without wall
|
|
161
|
+
* clock. Defaults to real `setTimeout`/`clearTimeout`. (CLAUDE.md §6: deterministic,
|
|
162
|
+
* no `sleep`.)
|
|
163
|
+
*/
|
|
164
|
+
timers?: {
|
|
165
|
+
setTimeout: (fn: () => void, ms: number) => unknown;
|
|
166
|
+
clearTimeout: (handle: unknown) => void;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Per-host snapshot in the aggregate `_meta/health` report (SPEC §13.1). */
|
|
170
|
+
export interface HostHealth {
|
|
171
|
+
host: string;
|
|
172
|
+
status: HostStatus;
|
|
173
|
+
/** Routing cost in IPC hops (SPEC §13.1 cost function). */
|
|
174
|
+
hopCost: 0 | 1;
|
|
175
|
+
/** Consecutive crash-restart attempts since last healthy (supervision telemetry). */
|
|
176
|
+
restarts: number;
|
|
177
|
+
}
|
|
178
|
+
/** The gateway's aggregate `_meta/health` payload (SPEC §13.1: per-host status). */
|
|
179
|
+
export interface GatewayHealth {
|
|
180
|
+
/** Per-host status, keyed by host tag. */
|
|
181
|
+
hosts: Record<string, HostHealth>;
|
|
182
|
+
/** True when at least one host is `ready` (the surface can serve something). */
|
|
183
|
+
serving: boolean;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The gateway surface presented to a transport adapter. The transport calls `route()`
|
|
187
|
+
* once per inbound request; the gateway picks the owning host and applies the full §13.1
|
|
188
|
+
* failure model around the adapter call.
|
|
189
|
+
*/
|
|
190
|
+
export interface Gateway {
|
|
191
|
+
/** Spawn + begin supervising every host. Resolves once all `start()` calls settle. */
|
|
192
|
+
start(): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* Route one operation to its owning host and return the result (SPEC §13).
|
|
195
|
+
* Applies readiness gating, deadline, cancellation and partial-availability mapping.
|
|
196
|
+
* @throws ApiError(`unavailable`) when the host is not serving.
|
|
197
|
+
* @throws ApiError(`deadline_exceeded`) when the deadline elapses.
|
|
198
|
+
*/
|
|
199
|
+
route(req: HostRequest, signal?: AbortSignal): Promise<unknown>;
|
|
200
|
+
/** The aggregate `_meta/health` report (SPEC §13.1 per-host status). */
|
|
201
|
+
health(): Promise<GatewayHealth>;
|
|
202
|
+
/** The total routing cost (sum of hop costs) — drives topology selection (SPEC §13.1). */
|
|
203
|
+
topologyCost(): number;
|
|
204
|
+
/** Shut every host down and stop supervision. */
|
|
205
|
+
stop(): Promise<void>;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Create the sidecar gateway (SPEC §13 / §13.1).
|
|
209
|
+
*
|
|
210
|
+
* The gateway routes every operation to the host named by `operation.host`, applying the
|
|
211
|
+
* normative failure model:
|
|
212
|
+
* - **partial availability** — an op for a `down`/`degraded` host throws `unavailable`;
|
|
213
|
+
* other hosts keep serving (one host's failure never affects another's route).
|
|
214
|
+
* - **readiness gating** — an op routes only when its host's `ready()` probe passed.
|
|
215
|
+
* - **deadline** — each route is raced against its deadline → `deadline_exceeded`.
|
|
216
|
+
* - **supervision** — a host's `onExit` triggers a restart-with-backoff loop; the
|
|
217
|
+
* gateway process stays up and the host returns to `ready` after a successful restart.
|
|
218
|
+
*/
|
|
219
|
+
export declare function createGateway(opts: GatewayOptions): Gateway;
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adhd/apigen-gateway",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./index.js",
|
|
5
|
+
"module": "./index.mjs",
|
|
6
|
+
"typings": "./index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@adhd/apigen-core": "^0.1.0",
|
|
9
|
+
"@adhd/apigen-errors": "^0.1.0"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
}
|
|
14
|
+
}
|