@cortexkit/subc-client 0.8.1 → 0.11.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/client.d.ts +119 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +204 -33
- package/dist/client.js.map +1 -1
- package/dist/envelope.d.ts +4 -0
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +9 -1
- package/dist/envelope.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +5 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +6 -2
- package/dist/provider.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +301 -30
- package/src/envelope.ts +11 -1
- package/src/index.ts +12 -0
- package/src/provider.ts +4 -1
package/dist/client.d.ts
CHANGED
|
@@ -30,15 +30,61 @@ export interface RouteOpenOptions {
|
|
|
30
30
|
/** Optional consumer-declared reverse-request capabilities for this route.open. This is a declaration, not a verified privilege; providers must treat an omitted field as no reverse-request capability. Known MCP method-family values today are "elicitation", "sampling", and "roots". */
|
|
31
31
|
consumerCapabilities?: string[];
|
|
32
32
|
}
|
|
33
|
+
export interface CatalogCapabilityRequirement {
|
|
34
|
+
capability: string;
|
|
35
|
+
need: "required" | "optional";
|
|
36
|
+
}
|
|
37
|
+
export interface CatalogCapabilities {
|
|
38
|
+
provides: string[];
|
|
39
|
+
requires: CatalogCapabilityRequirement[];
|
|
40
|
+
must_never_reach: string[];
|
|
41
|
+
}
|
|
42
|
+
/** A self-signal's anchor relative to the external surface it shapes. */
|
|
43
|
+
export type CatalogSignalAnchor = "fixed_interval" | {
|
|
44
|
+
event: {
|
|
45
|
+
event: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/** The source of a self-signal's effective cadence. */
|
|
49
|
+
export type CatalogSignalCadence = {
|
|
50
|
+
literal: {
|
|
51
|
+
interval_ms: number;
|
|
52
|
+
};
|
|
53
|
+
} | {
|
|
54
|
+
derived: {
|
|
55
|
+
source: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** A self-signal declaration mirrored from a registered module manifest. */
|
|
59
|
+
export interface CatalogSelfSignalDeclaration {
|
|
60
|
+
name: string;
|
|
61
|
+
/** Open string for forward-compatible self-signal kinds. */
|
|
62
|
+
kind: string;
|
|
63
|
+
effect: "observe" | "mutate";
|
|
64
|
+
anchored_to: CatalogSignalAnchor;
|
|
65
|
+
cadence?: CatalogSignalCadence | null;
|
|
66
|
+
domain?: string | null;
|
|
67
|
+
note?: string | null;
|
|
68
|
+
}
|
|
33
69
|
export interface CatalogEntry {
|
|
34
70
|
module_id: string;
|
|
35
71
|
roles: unknown[];
|
|
36
72
|
control_ops: string[];
|
|
73
|
+
/** Static capability claims mirrored from the registered module manifest. */
|
|
74
|
+
capabilities?: CatalogCapabilities | null;
|
|
75
|
+
/** Self-signal declarations mirrored verbatim from the registered manifest. */
|
|
76
|
+
self_signals?: CatalogSelfSignalDeclaration[] | null;
|
|
37
77
|
}
|
|
38
78
|
export interface RequestOptions {
|
|
39
79
|
priority?: Priority;
|
|
40
80
|
admissionClass?: AdmissionClass;
|
|
41
81
|
timeoutMs?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Set when the request body is opaque bytes rather than JSON. This flag is the
|
|
84
|
+
* receiver's only signal about the body representation; reply decoding follows
|
|
85
|
+
* the response frame's flag instead of this request option.
|
|
86
|
+
*/
|
|
87
|
+
binary?: boolean;
|
|
42
88
|
/** Called for each interim PUSH / StreamData frame before the terminal reply. */
|
|
43
89
|
onProgress?: (body: Uint8Array) => void;
|
|
44
90
|
}
|
|
@@ -106,6 +152,13 @@ export declare class SubcCallError extends Error {
|
|
|
106
152
|
readonly code?: string | undefined;
|
|
107
153
|
readonly cause?: unknown | undefined;
|
|
108
154
|
constructor(kind: SubcCallErrorKind, message: string, code?: string | undefined, cause?: unknown | undefined);
|
|
155
|
+
/**
|
|
156
|
+
* Machine-parsable payload from the wire `ErrorBody.detail`, surfaced from
|
|
157
|
+
* the underlying Error frame when one caused this failure. Typed refusal
|
|
158
|
+
* reasons (for example certification refusals) ride this field; consumers
|
|
159
|
+
* classify on it and must not have to reach into `cause`.
|
|
160
|
+
*/
|
|
161
|
+
get detail(): unknown;
|
|
109
162
|
}
|
|
110
163
|
/**
|
|
111
164
|
* A live subscription to a provider's event stream, riding a single held-open
|
|
@@ -122,7 +175,21 @@ export interface Subscription {
|
|
|
122
175
|
}
|
|
123
176
|
export declare class SubcError extends Error {
|
|
124
177
|
readonly code?: string | undefined;
|
|
125
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Machine-parsable payload from the wire `ErrorBody.detail` field, carried
|
|
180
|
+
* verbatim. Providers use it for typed refusal reasons (for example
|
|
181
|
+
* synapse's certification-refusal reasons); dropping it here would strand
|
|
182
|
+
* those reasons at the transport boundary.
|
|
183
|
+
*/
|
|
184
|
+
readonly detail?: unknown | undefined;
|
|
185
|
+
constructor(message: string, code?: string | undefined,
|
|
186
|
+
/**
|
|
187
|
+
* Machine-parsable payload from the wire `ErrorBody.detail` field, carried
|
|
188
|
+
* verbatim. Providers use it for typed refusal reasons (for example
|
|
189
|
+
* synapse's certification-refusal reasons); dropping it here would strand
|
|
190
|
+
* those reasons at the transport boundary.
|
|
191
|
+
*/
|
|
192
|
+
detail?: unknown | undefined);
|
|
126
193
|
}
|
|
127
194
|
export interface ConnectOptions {
|
|
128
195
|
connectionFile: string;
|
|
@@ -135,6 +202,14 @@ export interface ConnectOptions {
|
|
|
135
202
|
reconnectBackoff?: ReconnectBackoff;
|
|
136
203
|
/** Injectable sleep for timer-free reconnect tests. */
|
|
137
204
|
sleep?: (ms: number) => Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* How long managed calls keep retrying route.open in-place on retryable
|
|
207
|
+
* refusals (module_reloading, module_warming, target_unavailable, …) before
|
|
208
|
+
* settling not_sent. This deadline is the ONLY binder on those retries —
|
|
209
|
+
* module reloads legitimately take tens of seconds (the daemon's drain alone
|
|
210
|
+
* defaults to 30s), so the default matches the daemon's drain ceiling.
|
|
211
|
+
*/
|
|
212
|
+
routeOpenRetryDeadlineMs?: number;
|
|
138
213
|
/**
|
|
139
214
|
* Window the post-deadline liveness probe waits for ANY inbound frame after
|
|
140
215
|
* sending its channel-0 Ping before convicting the socket as half-open.
|
|
@@ -171,6 +246,23 @@ export interface ControlPush {
|
|
|
171
246
|
/** The full parsed body, `op` included, for op-specific fields. */
|
|
172
247
|
body: Record<string, unknown>;
|
|
173
248
|
}
|
|
249
|
+
/** A known route-close reason, excluding forward-compatible unknown values. */
|
|
250
|
+
export type KnownRouteCloseReason = "reload" | "restart" | "disable" | "crash" | "capability_denied";
|
|
251
|
+
/** A route-close reason decoded from a daemon control push. */
|
|
252
|
+
export type RouteCloseReason = KnownRouteCloseReason | "unknown";
|
|
253
|
+
/** Whether the close reason alone permits reopening a route automatically. */
|
|
254
|
+
export type RouteCloseDisposition = "may_reopen" | "must_not_reopen";
|
|
255
|
+
/**
|
|
256
|
+
* Decode a daemon close reason without rejecting a future wire value.
|
|
257
|
+
*
|
|
258
|
+
* An unknown reason remains observable as `unknown`; callers must use the
|
|
259
|
+
* strict disposition instead of treating an unrecognized policy change as safe.
|
|
260
|
+
*/
|
|
261
|
+
export declare function parseRouteCloseReason(reason: unknown): RouteCloseReason;
|
|
262
|
+
/** Unknown close reasons take the strictest action and must not trigger a reopen. */
|
|
263
|
+
export declare function classifyRouteCloseReason(reason: unknown): RouteCloseDisposition;
|
|
264
|
+
/** Decode a close reason from a route lifecycle control push, if present. */
|
|
265
|
+
export declare function routeCloseReason(push: ControlPush): RouteCloseReason | undefined;
|
|
174
266
|
export declare class SubcClient {
|
|
175
267
|
private sock;
|
|
176
268
|
private currentConn;
|
|
@@ -193,6 +285,15 @@ export declare class SubcClient {
|
|
|
193
285
|
static connect(opts: ConnectOptions): Promise<SubcClient>;
|
|
194
286
|
/** List modules subc knows about (channel-0 catalog.list). */
|
|
195
287
|
catalogList(moduleId?: string): Promise<CatalogEntry[]>;
|
|
288
|
+
/**
|
|
289
|
+
* Resolve the sole catalog claimant for a capability.
|
|
290
|
+
*
|
|
291
|
+
* This expresses singular intent even when the fleet permits plural claims;
|
|
292
|
+
* ambiguity is returned instead of selecting an arbitrary module.
|
|
293
|
+
*/
|
|
294
|
+
resolveProvider(capability: string): Promise<string>;
|
|
295
|
+
/** Resolve every catalog claimant in deterministic module-id order. */
|
|
296
|
+
resolveProviders(capability: string): Promise<string[]>;
|
|
196
297
|
/** Open a route and return its connection-bound immutable handle. */
|
|
197
298
|
routeOpen(target: RouteTarget, identity: BindIdentity, opts?: RouteOpenOptions): Promise<RouteHandle>;
|
|
198
299
|
/** Send a data-plane request on exactly the supplied route generation. */
|
|
@@ -202,6 +303,14 @@ export declare class SubcClient {
|
|
|
202
303
|
* reconnecting and re-opening cached routes after connection drops.
|
|
203
304
|
*/
|
|
204
305
|
call<Response = unknown>(moduleId: string, method: string, params?: unknown, opts?: ManagedCallOptions): Promise<Response>;
|
|
306
|
+
/**
|
|
307
|
+
* Managed route + raw opaque-body request convenience. Unlike call(), this
|
|
308
|
+
* sends bytes without a JSON method envelope because the BINARY flag is the
|
|
309
|
+
* receiver's only signal that the body is not JSON. A BINARY reply resolves
|
|
310
|
+
* to Uint8Array; a non-binary reply is decoded as JSON.
|
|
311
|
+
*/
|
|
312
|
+
callBinary(moduleId: string, body: Uint8Array, opts?: ManagedCallOptions): Promise<Uint8Array | unknown>;
|
|
313
|
+
private managedCall;
|
|
205
314
|
/** Open a held request on exactly one route generation. */
|
|
206
315
|
subscribe(handle: RouteHandle, body: unknown, onEvent: (event: Uint8Array) => void, opts?: SubscribeOptions): Subscription;
|
|
207
316
|
/** Send a pure-header cancellation for an in-flight request. */
|
|
@@ -316,6 +425,8 @@ export declare class SubcClient {
|
|
|
316
425
|
private allocateCorr;
|
|
317
426
|
private closeConnectionAfterCleanupFailure;
|
|
318
427
|
private encode;
|
|
428
|
+
/** Decode a terminal response according to the representation on the wire. */
|
|
429
|
+
private decodeReply;
|
|
319
430
|
private parseJson;
|
|
320
431
|
}
|
|
321
432
|
export declare function isConsumerReconnectTransient(err: unknown): boolean;
|
|
@@ -325,12 +436,15 @@ export declare function isConsumerReconnectTransient(err: unknown): boolean;
|
|
|
325
436
|
* is booting, mid-reload, transiently absent, or the bind relay timed out. A
|
|
326
437
|
* daemon-rejected route.open is provably pre-send (no data frame ever left the
|
|
327
438
|
* client), so these classify as not_sent; the managed path retries them in-place
|
|
328
|
-
* within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (
|
|
329
|
-
* config_divergence, unknown_target, ...) are excluded — they are pre-send but
|
|
330
|
-
* would never succeed, so retrying them would only storm the daemon.
|
|
331
|
-
*
|
|
439
|
+
* within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (module_removed,
|
|
440
|
+
* bad_consumer_identity, config_divergence, unknown_target, ...) are excluded — they are pre-send but
|
|
441
|
+
* would never succeed, so retrying them would only storm the daemon. In
|
|
442
|
+
* particular, capability_forbidden is an explicit non-retryable policy refusal.
|
|
443
|
+
* Kept byte-identical to subc-client-rs is_retryable_route_open_code for cross-client
|
|
332
444
|
* classification parity.
|
|
333
445
|
*/
|
|
334
446
|
export declare function isRetryableRouteOpenCode(code: string | undefined): boolean;
|
|
335
447
|
export declare function connectionFileExists(path: string): Promise<boolean>;
|
|
448
|
+
/** Validate the exact capability-grammar identifier before catalog I/O. */
|
|
449
|
+
export declare function isValidCapabilityIdentifier(identifier: string): boolean;
|
|
336
450
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAeA,OAAO,EAA2C,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACpG,OAAO,EACL,cAAc,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -12,7 +12,7 @@ import { promises as fs } from "node:fs";
|
|
|
12
12
|
import { debuglog } from "node:util";
|
|
13
13
|
import { AuthError, authenticateClient } from "./auth.js";
|
|
14
14
|
import { ConnectionFileError, readConnectionFile } from "./connection-file.js";
|
|
15
|
-
import { AdmissionClass, buildFrame, buildFlags, encodeFrame, FrameType, Priority, } from "./envelope.js";
|
|
15
|
+
import { AdmissionClass, buildFrame, buildFlags, encodeFrame, FrameType, hasBinary, Priority, } from "./envelope.js";
|
|
16
16
|
import { belongsToConnection, createRouteHandle, newConnectionToken, sameRouteHandle, StaleRouteHandleError, } from "./route-handle.js";
|
|
17
17
|
import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, writeBorrowed, writeTrackedBorrowed, } from "./socket.js";
|
|
18
18
|
const debug = debuglog("subc-client");
|
|
@@ -89,14 +89,75 @@ export class SubcCallError extends Error {
|
|
|
89
89
|
this.cause = cause;
|
|
90
90
|
this.name = "SubcCallError";
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Machine-parsable payload from the wire `ErrorBody.detail`, surfaced from
|
|
94
|
+
* the underlying Error frame when one caused this failure. Typed refusal
|
|
95
|
+
* reasons (for example certification refusals) ride this field; consumers
|
|
96
|
+
* classify on it and must not have to reach into `cause`.
|
|
97
|
+
*/
|
|
98
|
+
get detail() {
|
|
99
|
+
return this.cause instanceof SubcError ? this.cause.detail : undefined;
|
|
100
|
+
}
|
|
92
101
|
}
|
|
93
102
|
export class SubcError extends Error {
|
|
94
103
|
code;
|
|
95
|
-
|
|
104
|
+
detail;
|
|
105
|
+
constructor(message, code,
|
|
106
|
+
/**
|
|
107
|
+
* Machine-parsable payload from the wire `ErrorBody.detail` field, carried
|
|
108
|
+
* verbatim. Providers use it for typed refusal reasons (for example
|
|
109
|
+
* synapse's certification-refusal reasons); dropping it here would strand
|
|
110
|
+
* those reasons at the transport boundary.
|
|
111
|
+
*/
|
|
112
|
+
detail) {
|
|
96
113
|
super(message);
|
|
97
114
|
this.code = code;
|
|
115
|
+
this.detail = detail;
|
|
98
116
|
}
|
|
99
117
|
}
|
|
118
|
+
function requireBinaryBody(body) {
|
|
119
|
+
if (body instanceof Uint8Array)
|
|
120
|
+
return body;
|
|
121
|
+
const type = body === null ? "null" : Array.isArray(body) ? "array" : typeof body;
|
|
122
|
+
throw new SubcError(`binary request body must be a Uint8Array; got ${type}`, "binary_body_required");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Decode a daemon close reason without rejecting a future wire value.
|
|
126
|
+
*
|
|
127
|
+
* An unknown reason remains observable as `unknown`; callers must use the
|
|
128
|
+
* strict disposition instead of treating an unrecognized policy change as safe.
|
|
129
|
+
*/
|
|
130
|
+
export function parseRouteCloseReason(reason) {
|
|
131
|
+
switch (reason) {
|
|
132
|
+
case "reload":
|
|
133
|
+
case "restart":
|
|
134
|
+
case "disable":
|
|
135
|
+
case "crash":
|
|
136
|
+
case "capability_denied":
|
|
137
|
+
return reason;
|
|
138
|
+
default:
|
|
139
|
+
return "unknown";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Unknown close reasons take the strictest action and must not trigger a reopen. */
|
|
143
|
+
export function classifyRouteCloseReason(reason) {
|
|
144
|
+
switch (parseRouteCloseReason(reason)) {
|
|
145
|
+
case "reload":
|
|
146
|
+
case "restart":
|
|
147
|
+
return "may_reopen";
|
|
148
|
+
case "disable":
|
|
149
|
+
case "crash":
|
|
150
|
+
case "capability_denied":
|
|
151
|
+
case "unknown":
|
|
152
|
+
return "must_not_reopen";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Decode a close reason from a route lifecycle control push, if present. */
|
|
156
|
+
export function routeCloseReason(push) {
|
|
157
|
+
if (push.op !== "route.closing" && push.op !== "route.closed")
|
|
158
|
+
return undefined;
|
|
159
|
+
return parseRouteCloseReason(push.body.reason);
|
|
160
|
+
}
|
|
100
161
|
export class SubcClient {
|
|
101
162
|
sock;
|
|
102
163
|
currentConn;
|
|
@@ -139,6 +200,33 @@ export class SubcClient {
|
|
|
139
200
|
const parsed = this.parseJson(reply);
|
|
140
201
|
return parsed.modules ?? [];
|
|
141
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Resolve the sole catalog claimant for a capability.
|
|
205
|
+
*
|
|
206
|
+
* This expresses singular intent even when the fleet permits plural claims;
|
|
207
|
+
* ambiguity is returned instead of selecting an arbitrary module.
|
|
208
|
+
*/
|
|
209
|
+
async resolveProvider(capability) {
|
|
210
|
+
const claimants = await this.resolveProviders(capability);
|
|
211
|
+
if (claimants.length === 0) {
|
|
212
|
+
throw new SubcError(`no catalog claimant for capability ${capability}`, "capability_unprovided");
|
|
213
|
+
}
|
|
214
|
+
if (claimants.length > 1) {
|
|
215
|
+
throw new SubcError(`multiple catalog claimants for capability ${capability}: ${claimants.join(", ")}`, "capability_ambiguous");
|
|
216
|
+
}
|
|
217
|
+
return claimants[0];
|
|
218
|
+
}
|
|
219
|
+
/** Resolve every catalog claimant in deterministic module-id order. */
|
|
220
|
+
async resolveProviders(capability) {
|
|
221
|
+
if (!isValidCapabilityIdentifier(capability)) {
|
|
222
|
+
throw new SubcError(`malformed capability identifier ${JSON.stringify(capability)}`, "invalid_capability_identifier");
|
|
223
|
+
}
|
|
224
|
+
const modules = await this.catalogList();
|
|
225
|
+
return modules
|
|
226
|
+
.filter((module) => module.capabilities?.provides.includes(capability) ?? false)
|
|
227
|
+
.map((module) => module.module_id)
|
|
228
|
+
.sort();
|
|
229
|
+
}
|
|
142
230
|
/** Open a route and return its connection-bound immutable handle. */
|
|
143
231
|
async routeOpen(target, identity, opts = {}) {
|
|
144
232
|
const consumerIdentity = routeOpenConsumerIdentity(opts);
|
|
@@ -185,23 +273,40 @@ export class SubcClient {
|
|
|
185
273
|
/** Send a data-plane request on exactly the supplied route generation. */
|
|
186
274
|
async request(handle, body, opts = {}) {
|
|
187
275
|
this.assertLiveHandle(handle);
|
|
188
|
-
const
|
|
276
|
+
const binary = opts.binary ?? false;
|
|
277
|
+
const bytes = binary ? requireBinaryBody(body) : body instanceof Uint8Array ? body : this.encode(body);
|
|
189
278
|
const priority = opts.priority ?? Priority.Interactive;
|
|
190
279
|
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
191
|
-
const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
|
|
192
|
-
return this.
|
|
280
|
+
const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress, undefined, undefined, binary);
|
|
281
|
+
return this.decodeReply(reply);
|
|
193
282
|
}
|
|
194
283
|
/**
|
|
195
284
|
* Managed route + request convenience. Opens and caches a route for the module,
|
|
196
285
|
* reconnecting and re-opening cached routes after connection drops.
|
|
197
286
|
*/
|
|
198
287
|
async call(moduleId, method, params, opts = {}) {
|
|
288
|
+
if (opts.binary) {
|
|
289
|
+
throw new SubcError("call() builds a JSON body and cannot send a binary request; use callBinary(moduleId, body, opts) with a Uint8Array", "binary_call_requires_call_binary");
|
|
290
|
+
}
|
|
199
291
|
const body = params === undefined ? { method } : { method, params };
|
|
292
|
+
return (await this.managedCall(moduleId, body, opts));
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Managed route + raw opaque-body request convenience. Unlike call(), this
|
|
296
|
+
* sends bytes without a JSON method envelope because the BINARY flag is the
|
|
297
|
+
* receiver's only signal that the body is not JSON. A BINARY reply resolves
|
|
298
|
+
* to Uint8Array; a non-binary reply is decoded as JSON.
|
|
299
|
+
*/
|
|
300
|
+
async callBinary(moduleId, body, opts = {}) {
|
|
301
|
+
const bytes = requireBinaryBody(body);
|
|
302
|
+
return this.managedCall(moduleId, bytes, { ...opts, binary: true });
|
|
303
|
+
}
|
|
304
|
+
async managedCall(moduleId, body, opts) {
|
|
200
305
|
let retriedUnknownChannel = false;
|
|
201
306
|
for (;;) {
|
|
202
307
|
const routeHandle = await this.cachedRouteHandle(moduleId, opts);
|
|
203
308
|
try {
|
|
204
|
-
return
|
|
309
|
+
return await this.managedRequest(routeHandle, body, opts);
|
|
205
310
|
}
|
|
206
311
|
catch (err) {
|
|
207
312
|
if (!(err instanceof SubcCallError))
|
|
@@ -408,7 +513,7 @@ export class SubcClient {
|
|
|
408
513
|
async controlRpc(body, acceptFrame, onLateResponse) {
|
|
409
514
|
return this.send(null, body, Priority.Interactive, AdmissionClass.Normal, undefined, undefined, acceptFrame, onLateResponse);
|
|
410
515
|
}
|
|
411
|
-
send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse) {
|
|
516
|
+
send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse, binary = false) {
|
|
412
517
|
if (handle)
|
|
413
518
|
this.assertLiveHandle(handle);
|
|
414
519
|
if (this.closedErr)
|
|
@@ -423,7 +528,7 @@ export class SubcClient {
|
|
|
423
528
|
const key = pendingKey(handle, corr);
|
|
424
529
|
const channel = handle?.channel ?? 0;
|
|
425
530
|
const epoch = handle?.epoch ?? 0;
|
|
426
|
-
const frame = buildFrame(FrameType.Request, buildFlags(
|
|
531
|
+
const frame = buildFrame(FrameType.Request, buildFlags(binary, priority, false, admission), channel, epoch, corr, body);
|
|
427
532
|
return new Promise((resolve, reject) => {
|
|
428
533
|
const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
429
534
|
const pending = {
|
|
@@ -479,12 +584,13 @@ export class SubcClient {
|
|
|
479
584
|
setImmediate(arbitrate);
|
|
480
585
|
}
|
|
481
586
|
async managedRequest(handle, body, opts) {
|
|
482
|
-
const
|
|
587
|
+
const binary = opts.binary ?? false;
|
|
588
|
+
const bytes = binary ? requireBinaryBody(body) : body instanceof Uint8Array ? body : this.encode(body);
|
|
483
589
|
const priority = opts.priority ?? Priority.Interactive;
|
|
484
590
|
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
485
591
|
try {
|
|
486
|
-
const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
|
|
487
|
-
return this.
|
|
592
|
+
const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress, binary);
|
|
593
|
+
return this.decodeReply(reply);
|
|
488
594
|
}
|
|
489
595
|
catch (error) {
|
|
490
596
|
if (error instanceof SubcCallError)
|
|
@@ -492,7 +598,7 @@ export class SubcClient {
|
|
|
492
598
|
throw this.terminalCallError("managed call failed", error);
|
|
493
599
|
}
|
|
494
600
|
}
|
|
495
|
-
sendManaged(handle, body, priority, admission, timeoutMs, onProgress) {
|
|
601
|
+
sendManaged(handle, body, priority, admission, timeoutMs, onProgress, binary = false) {
|
|
496
602
|
try {
|
|
497
603
|
this.assertLiveHandle(handle);
|
|
498
604
|
}
|
|
@@ -510,7 +616,7 @@ export class SubcClient {
|
|
|
510
616
|
return Promise.reject(this.notSentCallError("request correlation allocator was exhausted", error));
|
|
511
617
|
}
|
|
512
618
|
const key = pendingKey(handle, corr);
|
|
513
|
-
const frame = buildFrame(FrameType.Request, buildFlags(
|
|
619
|
+
const frame = buildFrame(FrameType.Request, buildFlags(binary, priority, false, admission), handle.channel, handle.epoch, corr, body);
|
|
514
620
|
let handedToSocket = false;
|
|
515
621
|
const classifyFailure = (error) => {
|
|
516
622
|
if (!handedToSocket)
|
|
@@ -572,7 +678,7 @@ export class SubcClient {
|
|
|
572
678
|
return cached.opening;
|
|
573
679
|
}
|
|
574
680
|
async openCachedRoute(cached) {
|
|
575
|
-
const routeRetryDeadline = Date.now() +
|
|
681
|
+
const routeRetryDeadline = Date.now() + this.opts.routeOpenRetryDeadlineMs;
|
|
576
682
|
let routeRetryDelay = this.opts.reconnectBackoff.baseMs;
|
|
577
683
|
let routeRetryAttempt = 0;
|
|
578
684
|
for (;;) {
|
|
@@ -611,13 +717,20 @@ export class SubcClient {
|
|
|
611
717
|
continue;
|
|
612
718
|
}
|
|
613
719
|
if (!this.closeStarted && error instanceof SubcError && isRetryableRouteOpenCode(error.code)) {
|
|
720
|
+
// The deadline is the ONLY binder here. An attempt cap used to share
|
|
721
|
+
// this condition, and because the capped backoff sums to ~3.1s it
|
|
722
|
+
// strictly dominated the 30s deadline — the advertised reload
|
|
723
|
+
// patience was never delivered, so every module restart whose reload
|
|
724
|
+
// exceeded ~3s failed managed callers with module_reloading. Module
|
|
725
|
+
// reloads legitimately take tens of seconds (drain alone defaults to
|
|
726
|
+
// 30s); the backoff cap below bounds retry pressure instead.
|
|
614
727
|
routeRetryAttempt += 1;
|
|
615
|
-
if (
|
|
728
|
+
if (Date.now() < routeRetryDeadline) {
|
|
616
729
|
await this.opts.sleep(routeRetryDelay);
|
|
617
730
|
routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
|
|
618
731
|
continue;
|
|
619
732
|
}
|
|
620
|
-
throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${error.code} (retry
|
|
733
|
+
throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${error.code} (retry deadline exhausted after ${routeRetryAttempt} attempts)`, error);
|
|
621
734
|
}
|
|
622
735
|
throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, error);
|
|
623
736
|
}
|
|
@@ -786,20 +899,34 @@ export class SubcClient {
|
|
|
786
899
|
void this.readLoop(opened.sock, this.generation);
|
|
787
900
|
}
|
|
788
901
|
async reopenCachedRoutes() {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (cached
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (cached.closed)
|
|
798
|
-
this.liveRoutes.delete(handle.channel);
|
|
799
|
-
this.sendRouteGoodbye(handle);
|
|
902
|
+
const routeKeys = [...this.routes.keys()];
|
|
903
|
+
for (const key of routeKeys) {
|
|
904
|
+
const cached = this.routes.get(key);
|
|
905
|
+
if (cached)
|
|
906
|
+
cached.handle = null;
|
|
907
|
+
}
|
|
908
|
+
for (const key of routeKeys) {
|
|
909
|
+
const cached = this.routes.get(key);
|
|
910
|
+
if (!cached || cached.closed)
|
|
800
911
|
continue;
|
|
912
|
+
try {
|
|
913
|
+
const handle = await this.routeOpen(cached.target, cached.identity, {
|
|
914
|
+
consumerIdentity: cached.consumerIdentity ?? null,
|
|
915
|
+
});
|
|
916
|
+
if (cached.closed) {
|
|
917
|
+
this.liveRoutes.delete(handle.channel);
|
|
918
|
+
this.sendRouteGoodbye(handle);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
cached.handle = handle;
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
if (isRouteOpenRefusal(error)) {
|
|
925
|
+
this.routes.delete(key);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
throw error;
|
|
801
929
|
}
|
|
802
|
-
cached.handle = handle;
|
|
803
930
|
}
|
|
804
931
|
}
|
|
805
932
|
// A request timeout carries the local socket port and (channel, corr) so a
|
|
@@ -841,6 +968,13 @@ export class SubcClient {
|
|
|
841
968
|
// post-deadline liveness probe reads this stamp. Stamped after readLoop's
|
|
842
969
|
// generation gate, so a stale socket's late frames never vouch for the
|
|
843
970
|
// live one.
|
|
971
|
+
//
|
|
972
|
+
// PLACEMENT IS LOAD-BEARING: every inbound frame passes this one point
|
|
973
|
+
// before demux, which is the whole reason a single stamp works. A future
|
|
974
|
+
// fast path or drain-and-dispatch refactor that routes frames around
|
|
975
|
+
// dispatch() makes the stamp skippable, and the liveness watermark
|
|
976
|
+
// quietly stops meaning "the link delivered bytes" -- the cheapest
|
|
977
|
+
// correctness property here is also the easiest to lose in a refactor.
|
|
844
978
|
this.lastInboundAtMs = Date.now();
|
|
845
979
|
if (frame.header.channel === 0 && frame.header.ty === FrameType.Push) {
|
|
846
980
|
// Daemon-originated control push (route.closing / route.closed / future
|
|
@@ -976,7 +1110,7 @@ export class SubcClient {
|
|
|
976
1110
|
errorFromFrame(frame) {
|
|
977
1111
|
try {
|
|
978
1112
|
const parsed = JSON.parse(Buffer.from(frame.body).toString("utf8"));
|
|
979
|
-
return new SubcError(parsed.message ?? "subc error", parsed.code);
|
|
1113
|
+
return new SubcError(parsed.message ?? "subc error", parsed.code, parsed.detail);
|
|
980
1114
|
}
|
|
981
1115
|
catch {
|
|
982
1116
|
return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
|
|
@@ -1061,6 +1195,10 @@ export class SubcClient {
|
|
|
1061
1195
|
encode(value) {
|
|
1062
1196
|
return Buffer.from(JSON.stringify(value), "utf8");
|
|
1063
1197
|
}
|
|
1198
|
+
/** Decode a terminal response according to the representation on the wire. */
|
|
1199
|
+
decodeReply(frame) {
|
|
1200
|
+
return hasBinary(frame.header.flags) ? frame.body : this.parseJson(frame);
|
|
1201
|
+
}
|
|
1064
1202
|
parseJson(frame) {
|
|
1065
1203
|
const b = frame.body;
|
|
1066
1204
|
return JSON.parse(Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString("utf8"));
|
|
@@ -1089,19 +1227,31 @@ export function isConsumerReconnectTransient(err) {
|
|
|
1089
1227
|
const code = errorCode(err);
|
|
1090
1228
|
return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
|
|
1091
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* A coded SubcError came from a complete wire Error frame, so the connection
|
|
1232
|
+
* survived long enough to refuse only this route. An uncoded SubcError can mean
|
|
1233
|
+
* the socket or protocol exchange failed mid-open and must still fail reconnect.
|
|
1234
|
+
*/
|
|
1235
|
+
function isRouteOpenRefusal(err) {
|
|
1236
|
+
return err instanceof SubcError && typeof err.code === "string";
|
|
1237
|
+
}
|
|
1092
1238
|
/**
|
|
1093
1239
|
* The closed set of route.open rejection codes that mean "the target is
|
|
1094
1240
|
* momentarily unavailable but the request could succeed on retry" — the target
|
|
1095
1241
|
* is booting, mid-reload, transiently absent, or the bind relay timed out. A
|
|
1096
1242
|
* daemon-rejected route.open is provably pre-send (no data frame ever left the
|
|
1097
1243
|
* client), so these classify as not_sent; the managed path retries them in-place
|
|
1098
|
-
* within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (
|
|
1099
|
-
* config_divergence, unknown_target, ...) are excluded — they are pre-send but
|
|
1100
|
-
* would never succeed, so retrying them would only storm the daemon.
|
|
1101
|
-
*
|
|
1244
|
+
* within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (module_removed,
|
|
1245
|
+
* bad_consumer_identity, config_divergence, unknown_target, ...) are excluded — they are pre-send but
|
|
1246
|
+
* would never succeed, so retrying them would only storm the daemon. In
|
|
1247
|
+
* particular, capability_forbidden is an explicit non-retryable policy refusal.
|
|
1248
|
+
* Kept byte-identical to subc-client-rs is_retryable_route_open_code for cross-client
|
|
1102
1249
|
* classification parity.
|
|
1103
1250
|
*/
|
|
1104
1251
|
export function isRetryableRouteOpenCode(code) {
|
|
1252
|
+
// A capability deny is policy, never a transient target-availability failure.
|
|
1253
|
+
if (code === "capability_forbidden")
|
|
1254
|
+
return false;
|
|
1105
1255
|
return (code === "unknown_module" ||
|
|
1106
1256
|
code === "module_reloading" ||
|
|
1107
1257
|
code === "module_warming" ||
|
|
@@ -1125,11 +1275,32 @@ function normalizeConnectOptions(opts) {
|
|
|
1125
1275
|
targetKind: opts.targetKind ?? DEFAULT_MANAGED_TARGET_KIND,
|
|
1126
1276
|
reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
|
|
1127
1277
|
sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
1278
|
+
routeOpenRetryDeadlineMs: opts.routeOpenRetryDeadlineMs ?? ROUTE_OPEN_RETRY_DEADLINE_MS,
|
|
1128
1279
|
timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS,
|
|
1129
1280
|
livenessProbeWindowMs: opts.livenessProbeWindowMs ?? LIVENESS_PROBE_WINDOW_MS,
|
|
1130
1281
|
onControlPush: opts.onControlPush,
|
|
1131
1282
|
};
|
|
1132
1283
|
}
|
|
1284
|
+
/** Validate the exact capability-grammar identifier before catalog I/O. */
|
|
1285
|
+
export function isValidCapabilityIdentifier(identifier) {
|
|
1286
|
+
if (/\s/u.test(identifier))
|
|
1287
|
+
return false;
|
|
1288
|
+
const separator = identifier.indexOf("/v");
|
|
1289
|
+
if (separator <= 0 || identifier.indexOf("/v", separator + 1) !== -1)
|
|
1290
|
+
return false;
|
|
1291
|
+
const name = identifier.slice(0, separator);
|
|
1292
|
+
const version = identifier.slice(separator + 2);
|
|
1293
|
+
if (new TextEncoder().encode(name).byteLength > 64 || version.length === 0)
|
|
1294
|
+
return false;
|
|
1295
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(name) && !/^[a-z]$/u.test(name))
|
|
1296
|
+
return false;
|
|
1297
|
+
if (name.includes("--"))
|
|
1298
|
+
return false;
|
|
1299
|
+
if (!/^\d+$/u.test(version) || (version.length > 1 && version.startsWith("0")))
|
|
1300
|
+
return false;
|
|
1301
|
+
const numericVersion = Number(version);
|
|
1302
|
+
return Number.isSafeInteger(numericVersion) && numericVersion >= 1 && numericVersion <= 0xffff_ffff;
|
|
1303
|
+
}
|
|
1133
1304
|
function routeCacheKey(target, identity, consumerIdentity) {
|
|
1134
1305
|
const consumerPart = consumerIdentity
|
|
1135
1306
|
? `${consumerIdentity.module_id}\0${consumerIdentity.launch_nonce}`
|