@cortexkit/subc-client 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/client.d.ts +49 -54
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +303 -262
- package/dist/client.js.map +1 -1
- package/dist/envelope.d.ts +24 -17
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +91 -72
- package/dist/envelope.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +35 -8
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +221 -71
- package/dist/provider.js.map +1 -1
- package/dist/route-handle.d.ts +25 -0
- package/dist/route-handle.d.ts.map +1 -0
- package/dist/route-handle.js +52 -0
- package/dist/route-handle.js.map +1 -0
- package/dist/socket.d.ts +7 -0
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +23 -0
- package/dist/socket.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +374 -277
- package/src/envelope.ts +136 -74
- package/src/index.ts +7 -0
- package/src/provider.ts +361 -94
- package/src/route-handle.ts +60 -0
- package/src/socket.ts +37 -0
package/dist/provider.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type BindIdentity, type ReconnectBackoff, type RouteTarget } from "./client.js";
|
|
1
|
+
import { type BindIdentity, type ReconnectBackoff, type RequestOptions, type RouteTarget } from "./client.js";
|
|
2
2
|
import { type ConnectionInfo } from "./connection-file.js";
|
|
3
|
+
import { AdmissionClass, Priority } from "./envelope.js";
|
|
4
|
+
import { RouteHandle } from "./route-handle.js";
|
|
3
5
|
export declare const HELLO_CORR = 1n;
|
|
4
6
|
export type TrustTier = "first_party" | "reviewed" | "untrusted";
|
|
5
7
|
export type ExecutionMode = "pure" | "mutating" | "unfenceable";
|
|
@@ -135,12 +137,14 @@ export interface ManagementSurfaceManifestOptions {
|
|
|
135
137
|
* events and `signal` to learn when the consumer cancelled or the route went away.
|
|
136
138
|
*/
|
|
137
139
|
export interface ProviderRequestContext {
|
|
140
|
+
/** The immutable route generation that received this request. */
|
|
141
|
+
readonly handle: RouteHandle;
|
|
138
142
|
/**
|
|
139
143
|
* Emit an interim event as a StreamData frame on this request's (channel, corr).
|
|
140
144
|
* The consumer receives it via its subscription `onEvent`. A no-op once the
|
|
141
145
|
* request has been aborted (cancelled or route-gone).
|
|
142
146
|
*/
|
|
143
|
-
emit(body: Uint8Array): Promise<void>;
|
|
147
|
+
emit(body: Uint8Array, opts?: ProviderEmitOptions): Promise<void>;
|
|
144
148
|
/** Aborts when the consumer sends Cancel for this request, or the route is torn down. */
|
|
145
149
|
signal: AbortSignal;
|
|
146
150
|
/**
|
|
@@ -159,7 +163,7 @@ export interface ProviderRequestContext {
|
|
|
159
163
|
* `void` to end a streaming subscription with a StreamEnd terminal (after emitting
|
|
160
164
|
* events via `ctx.emit`). Throwing produces an Error terminal.
|
|
161
165
|
*/
|
|
162
|
-
export type ProviderHandler = (
|
|
166
|
+
export type ProviderHandler = (handle: RouteHandle, body: Uint8Array, ctx: ProviderRequestContext) => Promise<Uint8Array | void> | Uint8Array | void;
|
|
163
167
|
export type ProviderHealthHandler = () => Promise<HealthReport> | HealthReport;
|
|
164
168
|
export type Principal = {
|
|
165
169
|
kind: "reserved";
|
|
@@ -170,10 +174,16 @@ export type Principal = {
|
|
|
170
174
|
kind: "unverified";
|
|
171
175
|
};
|
|
172
176
|
export interface RouteBindRequest {
|
|
173
|
-
|
|
177
|
+
handle: RouteHandle;
|
|
174
178
|
target: RouteTarget;
|
|
175
179
|
identity: BindIdentity;
|
|
176
180
|
principal?: Principal;
|
|
181
|
+
/** Consumer-declared reverse-request capabilities for this bind. 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". */
|
|
182
|
+
consumer_capabilities?: string[];
|
|
183
|
+
}
|
|
184
|
+
export interface ProviderEmitOptions {
|
|
185
|
+
priority?: Priority;
|
|
186
|
+
admissionClass?: AdmissionClass;
|
|
177
187
|
}
|
|
178
188
|
export type BindDecision = boolean | {
|
|
179
189
|
accept: boolean;
|
|
@@ -201,7 +211,9 @@ export interface SubcProviderConnectOptions {
|
|
|
201
211
|
handshakeTimeoutMs?: number;
|
|
202
212
|
controlOps?: string[] | null;
|
|
203
213
|
onBind?: (request: RouteBindRequest) => Promise<BindDecision> | BindDecision;
|
|
204
|
-
|
|
214
|
+
/** Runs only after an accepted bind ack is queued and the handle is installed. */
|
|
215
|
+
onBound?: (handle: RouteHandle) => void | Promise<void>;
|
|
216
|
+
onRouteGone?: (handle: RouteHandle) => void | Promise<void>;
|
|
205
217
|
/** Backoff for provider reconnect after an unexpected socket drop. */
|
|
206
218
|
reconnectBackoff?: ReconnectBackoff;
|
|
207
219
|
/** Injectable sleep for timer-free reconnect and debounce tests. */
|
|
@@ -234,7 +246,7 @@ export declare class SubcProviderError extends Error {
|
|
|
234
246
|
constructor(message: string, code?: string | undefined);
|
|
235
247
|
}
|
|
236
248
|
export declare function managementSurfaceManifest(opts: ManagementSurfaceManifestOptions): ManifestInput;
|
|
237
|
-
export declare function jsonProviderHandler<Request = unknown, Response = unknown>(handler: (
|
|
249
|
+
export declare function jsonProviderHandler<Request = unknown, Response = unknown>(handler: (handle: RouteHandle, request: Request) => Promise<Response> | Response): ProviderHandler;
|
|
238
250
|
export declare class SubcProvider {
|
|
239
251
|
private sock;
|
|
240
252
|
private currentConn;
|
|
@@ -244,6 +256,11 @@ export declare class SubcProvider {
|
|
|
244
256
|
private closeStarted;
|
|
245
257
|
private closedErr;
|
|
246
258
|
private readonly inflight;
|
|
259
|
+
private readonly pending;
|
|
260
|
+
private readonly liveRoutes;
|
|
261
|
+
private connectionToken;
|
|
262
|
+
private nextCorr;
|
|
263
|
+
private ingressEpochDropCount;
|
|
247
264
|
private readonly requestGate;
|
|
248
265
|
private reconnecting;
|
|
249
266
|
private generation;
|
|
@@ -258,16 +275,23 @@ export declare class SubcProvider {
|
|
|
258
275
|
*/
|
|
259
276
|
storage: unknown;
|
|
260
277
|
private constructor();
|
|
278
|
+
/** Number of nonzero-channel ingress frames rejected by endpoint validation. */
|
|
279
|
+
get droppedIngressFrames(): number;
|
|
261
280
|
get conn(): ConnectionInfo;
|
|
262
281
|
currentEpoch(): number;
|
|
282
|
+
/** Send a reverse request on exactly one installed route generation. */
|
|
283
|
+
request(handle: RouteHandle, body: Uint8Array, opts?: RequestOptions): Promise<Uint8Array>;
|
|
284
|
+
/** Emit an unsolicited Push after onBound has published the route. */
|
|
285
|
+
push(handle: RouteHandle, body: Uint8Array, opts?: ProviderEmitOptions): Promise<void>;
|
|
286
|
+
cancel(handle: RouteHandle, corr: bigint): void;
|
|
287
|
+
closeRoute(handle: RouteHandle): void;
|
|
263
288
|
/** Read the connection file, authenticate as a client, register the manifest with HELLO, and serve frames. */
|
|
264
289
|
static connect(opts: SubcProviderConnectOptions): Promise<SubcProvider>;
|
|
265
290
|
close(): Promise<void>;
|
|
266
291
|
private static openConnection;
|
|
267
292
|
private readLoop;
|
|
268
293
|
private dispatch;
|
|
269
|
-
|
|
270
|
-
private abortChannel;
|
|
294
|
+
private abortHandle;
|
|
271
295
|
private abortGeneration;
|
|
272
296
|
private abortAllInflight;
|
|
273
297
|
private handleControlRequest;
|
|
@@ -283,6 +307,9 @@ export declare class SubcProvider {
|
|
|
283
307
|
private cancelRestoredDebounce;
|
|
284
308
|
private enqueueConnectionState;
|
|
285
309
|
private drainConnectionStateQueue;
|
|
310
|
+
private isLiveHandle;
|
|
311
|
+
private assertLiveHandle;
|
|
312
|
+
private allocateCorr;
|
|
286
313
|
private failFatal;
|
|
287
314
|
private finishClosed;
|
|
288
315
|
}
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
|
package/dist/provider.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { AuthError, authenticateClient } from "./auth.js";
|
|
3
|
-
import { DEFAULT_RECONNECT_BACKOFF } from "./client.js";
|
|
3
|
+
import { DEFAULT_RECONNECT_BACKOFF, } from "./client.js";
|
|
4
4
|
import { ConnectionFileError, readConnectionFile } from "./connection-file.js";
|
|
5
|
-
import { buildFlags, buildFrame, buildFrameWithVersion,
|
|
5
|
+
import { AdmissionClass, buildFlags, buildFrame, buildFrameWithVersion, encodeFrame, FrameType, Priority, PROTOCOL_VERSION, } from "./envelope.js";
|
|
6
|
+
import { belongsToConnection, createRouteHandle, newConnectionToken, StaleRouteHandleError, } from "./route-handle.js";
|
|
6
7
|
import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, } from "./socket.js";
|
|
7
8
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
8
9
|
const BODY_READ_TIMEOUT_MS = 30_000;
|
|
@@ -88,9 +89,9 @@ export function managementSurfaceManifest(opts) {
|
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
export function jsonProviderHandler(handler) {
|
|
91
|
-
return async (
|
|
92
|
+
return async (handle, body) => {
|
|
92
93
|
const request = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
93
|
-
const response = await handler(
|
|
94
|
+
const response = await handler(handle, request);
|
|
94
95
|
return encodeJson(response);
|
|
95
96
|
};
|
|
96
97
|
}
|
|
@@ -106,6 +107,11 @@ export class SubcProvider {
|
|
|
106
107
|
// A socket drop only makes the reply path stale; handlers may still finish their
|
|
107
108
|
// durable work, and their late sends are ignored by the generation guard.
|
|
108
109
|
inflight = new Map();
|
|
110
|
+
pending = new Map();
|
|
111
|
+
liveRoutes = new Map();
|
|
112
|
+
connectionToken = newConnectionToken();
|
|
113
|
+
nextCorr = 1n;
|
|
114
|
+
ingressEpochDropCount = 0;
|
|
109
115
|
requestGate = new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY);
|
|
110
116
|
reconnecting = null;
|
|
111
117
|
generation = 1;
|
|
@@ -130,12 +136,58 @@ export class SubcProvider {
|
|
|
130
136
|
void this.readLoop(sock, this.generation);
|
|
131
137
|
this.enqueueConnectionState({ state: "connected", epoch: this.connectionEpoch });
|
|
132
138
|
}
|
|
139
|
+
/** Number of nonzero-channel ingress frames rejected by endpoint validation. */
|
|
140
|
+
get droppedIngressFrames() {
|
|
141
|
+
return this.ingressEpochDropCount;
|
|
142
|
+
}
|
|
133
143
|
get conn() {
|
|
134
144
|
return this.currentConn;
|
|
135
145
|
}
|
|
136
146
|
currentEpoch() {
|
|
137
147
|
return this.connectionEpoch;
|
|
138
148
|
}
|
|
149
|
+
/** Send a reverse request on exactly one installed route generation. */
|
|
150
|
+
async request(handle, body, opts = {}) {
|
|
151
|
+
this.assertLiveHandle(handle);
|
|
152
|
+
const corr = this.allocateCorr();
|
|
153
|
+
const key = routeKey(handle, corr);
|
|
154
|
+
const timeoutMs = opts.timeoutMs ?? WRITE_TIMEOUT_MS;
|
|
155
|
+
const frame = buildFrame(FrameType.Request, buildFlags(false, opts.priority ?? Priority.Interactive, false, opts.admissionClass ?? AdmissionClass.Normal), handle.channel, handle.epoch, corr, body);
|
|
156
|
+
return await new Promise((resolve, reject) => {
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
if (this.pending.delete(key))
|
|
159
|
+
reject(new SubcProviderError("reverse request timed out", "request_timeout"));
|
|
160
|
+
}, timeoutMs);
|
|
161
|
+
this.pending.set(key, {
|
|
162
|
+
resolve: (response) => resolve(response.body),
|
|
163
|
+
reject,
|
|
164
|
+
timer,
|
|
165
|
+
});
|
|
166
|
+
this.sendOn(this.sock, this.generation, frame).catch((error) => {
|
|
167
|
+
const pending = this.pending.get(key);
|
|
168
|
+
if (!pending)
|
|
169
|
+
return;
|
|
170
|
+
this.pending.delete(key);
|
|
171
|
+
clearTimeout(pending.timer);
|
|
172
|
+
reject(error instanceof Error ? error : new SubcProviderError(String(error)));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Emit an unsolicited Push after onBound has published the route. */
|
|
177
|
+
async push(handle, body, opts = {}) {
|
|
178
|
+
this.assertLiveHandle(handle);
|
|
179
|
+
await this.sendOn(this.sock, this.generation, buildFrame(FrameType.Push, buildFlags(false, opts.priority ?? Priority.Interactive, false, opts.admissionClass ?? AdmissionClass.Normal), handle.channel, handle.epoch, 0n, body));
|
|
180
|
+
}
|
|
181
|
+
cancel(handle, corr) {
|
|
182
|
+
this.assertLiveHandle(handle);
|
|
183
|
+
void this.sendOn(this.sock, this.generation, buildFrame(FrameType.Cancel, controlFlags(), handle.channel, handle.epoch, corr, new Uint8Array(0)));
|
|
184
|
+
}
|
|
185
|
+
closeRoute(handle) {
|
|
186
|
+
this.assertLiveHandle(handle);
|
|
187
|
+
this.liveRoutes.delete(handle.channel);
|
|
188
|
+
this.abortHandle(handle);
|
|
189
|
+
void this.sendOn(this.sock, this.generation, buildFrame(FrameType.Goodbye, controlFlags(), handle.channel, handle.epoch, 0n, new Uint8Array(0)));
|
|
190
|
+
}
|
|
139
191
|
/** Read the connection file, authenticate as a client, register the manifest with HELLO, and serve frames. */
|
|
140
192
|
static async connect(opts) {
|
|
141
193
|
if (opts.manifest.protocol_ver !== PROTOCOL_VERSION) {
|
|
@@ -151,7 +203,7 @@ export class SubcProvider {
|
|
|
151
203
|
this.cancelRestoredDebounce();
|
|
152
204
|
const sock = this.sock;
|
|
153
205
|
try {
|
|
154
|
-
await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0n, new Uint8Array(0)));
|
|
206
|
+
await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0, 0n, new Uint8Array(0)));
|
|
155
207
|
}
|
|
156
208
|
catch {
|
|
157
209
|
// The daemon may already have closed the connection; close() remains best-effort.
|
|
@@ -182,13 +234,8 @@ export class SubcProvider {
|
|
|
182
234
|
async readLoop(sock, generation) {
|
|
183
235
|
try {
|
|
184
236
|
for (;;) {
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const header = decodeHeader(headerBytes);
|
|
188
|
-
const body = header.len === 0
|
|
189
|
-
? new Uint8Array(0)
|
|
190
|
-
: await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
|
|
191
|
-
const keepGoing = await this.dispatch({ header, body }, sock, generation);
|
|
237
|
+
const frame = await sock.readFrame(Number.POSITIVE_INFINITY, Date.now() + BODY_READ_TIMEOUT_MS);
|
|
238
|
+
const keepGoing = await this.dispatch(frame, sock, generation);
|
|
192
239
|
if (!keepGoing) {
|
|
193
240
|
if (this.sock === sock && this.generation === generation)
|
|
194
241
|
this.closeStarted = true;
|
|
@@ -196,9 +243,9 @@ export class SubcProvider {
|
|
|
196
243
|
}
|
|
197
244
|
}
|
|
198
245
|
}
|
|
199
|
-
catch (
|
|
246
|
+
catch (error) {
|
|
200
247
|
if (this.sock === sock && this.generation === generation && !this.closeStarted) {
|
|
201
|
-
this.handleUnexpectedDrop(sock, generation,
|
|
248
|
+
this.handleUnexpectedDrop(sock, generation, error instanceof Error ? error : new SubcProviderError(String(error)));
|
|
202
249
|
return;
|
|
203
250
|
}
|
|
204
251
|
}
|
|
@@ -211,33 +258,59 @@ export class SubcProvider {
|
|
|
211
258
|
}
|
|
212
259
|
}
|
|
213
260
|
async dispatch(frame, sock, generation) {
|
|
261
|
+
let handle = null;
|
|
262
|
+
if (frame.header.channel !== 0) {
|
|
263
|
+
handle = this.liveRoutes.get(frame.header.channel) ?? null;
|
|
264
|
+
if (!handle || handle.epoch !== frame.header.epoch) {
|
|
265
|
+
this.ingressEpochDropCount += 1;
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (handle) {
|
|
270
|
+
const pendingKey = routeKey(handle, frame.header.corr);
|
|
271
|
+
const pending = this.pending.get(pendingKey);
|
|
272
|
+
if (pending) {
|
|
273
|
+
if (frame.header.ty === FrameType.Push || frame.header.ty === FrameType.StreamData)
|
|
274
|
+
return true;
|
|
275
|
+
if (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.StreamEnd) {
|
|
276
|
+
this.pending.delete(pendingKey);
|
|
277
|
+
clearTimeout(pending.timer);
|
|
278
|
+
pending.resolve(frame);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (frame.header.ty === FrameType.Error) {
|
|
282
|
+
this.pending.delete(pendingKey);
|
|
283
|
+
clearTimeout(pending.timer);
|
|
284
|
+
pending.reject(providerErrorFromFrame(frame));
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
214
289
|
switch (frame.header.ty) {
|
|
215
290
|
case FrameType.Ping:
|
|
216
291
|
if (frame.header.channel === 0) {
|
|
217
|
-
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Pong, frame.header.flags, 0, frame.header.corr, new Uint8Array(0)));
|
|
292
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Pong, frame.header.flags, 0, 0, frame.header.corr, new Uint8Array(0)));
|
|
218
293
|
}
|
|
219
294
|
return true;
|
|
220
295
|
case FrameType.Goodbye:
|
|
221
|
-
if (
|
|
296
|
+
if (!handle)
|
|
222
297
|
return false;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
await this.opts.onRouteGone?.(frame.header.channel);
|
|
298
|
+
this.liveRoutes.delete(handle.channel);
|
|
299
|
+
this.abortHandle(handle);
|
|
300
|
+
await this.opts.onRouteGone?.(handle);
|
|
227
301
|
return true;
|
|
228
302
|
case FrameType.Cancel:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.inflight.get(routeKey(generation, frame.header.channel, frame.header.corr))?.abort();
|
|
303
|
+
if (handle)
|
|
304
|
+
this.inflight.get(routeKey(handle, frame.header.corr))?.abort();
|
|
232
305
|
return true;
|
|
233
306
|
case FrameType.Request:
|
|
234
307
|
if (frame.header.channel === 0) {
|
|
235
308
|
await this.handleControlRequest(frame, sock, generation);
|
|
236
309
|
}
|
|
237
|
-
else {
|
|
238
|
-
void this.handleDataRequest(frame, sock, generation).catch((
|
|
310
|
+
else if (handle) {
|
|
311
|
+
void this.handleDataRequest(frame, handle, sock, generation).catch((error) => {
|
|
239
312
|
if (!this.closeStarted && this.sock === sock && this.generation === generation) {
|
|
240
|
-
console.warn("SubcProvider handler failed after its request was dispatched",
|
|
313
|
+
console.warn("SubcProvider handler failed after its request was dispatched", error);
|
|
241
314
|
}
|
|
242
315
|
});
|
|
243
316
|
}
|
|
@@ -246,20 +319,28 @@ export class SubcProvider {
|
|
|
246
319
|
return true;
|
|
247
320
|
}
|
|
248
321
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const prefix = `${generation}:${channel}:`;
|
|
322
|
+
abortHandle(handle) {
|
|
323
|
+
const prefix = `${handle.channel}:${handle.epoch}:`;
|
|
252
324
|
for (const [key, controller] of this.inflight) {
|
|
253
325
|
if (key.startsWith(prefix))
|
|
254
326
|
controller.abort();
|
|
255
327
|
}
|
|
328
|
+
for (const [key, pending] of this.pending) {
|
|
329
|
+
if (!key.startsWith(prefix))
|
|
330
|
+
continue;
|
|
331
|
+
this.pending.delete(key);
|
|
332
|
+
clearTimeout(pending.timer);
|
|
333
|
+
pending.reject(new StaleRouteHandleError(handle));
|
|
334
|
+
}
|
|
256
335
|
}
|
|
257
|
-
abortGeneration(
|
|
258
|
-
|
|
259
|
-
for (const [key,
|
|
260
|
-
|
|
261
|
-
|
|
336
|
+
abortGeneration(_generation) {
|
|
337
|
+
this.abortAllInflight();
|
|
338
|
+
for (const [key, pending] of this.pending) {
|
|
339
|
+
this.pending.delete(key);
|
|
340
|
+
clearTimeout(pending.timer);
|
|
341
|
+
pending.reject(new SubcProviderError("provider connection dropped", "connection_dropped"));
|
|
262
342
|
}
|
|
343
|
+
this.liveRoutes.clear();
|
|
263
344
|
}
|
|
264
345
|
abortAllInflight() {
|
|
265
346
|
for (const controller of this.inflight.values())
|
|
@@ -268,9 +349,9 @@ export class SubcProvider {
|
|
|
268
349
|
async handleControlRequest(frame, sock, generation) {
|
|
269
350
|
const request = parseJson(frame.body);
|
|
270
351
|
if (request.op === HEALTH_CHECK_OP) {
|
|
271
|
-
void this.handleHealthRequest(frame, sock, generation).catch((
|
|
352
|
+
void this.handleHealthRequest(frame, sock, generation).catch((error) => {
|
|
272
353
|
if (!this.closeStarted && this.sock === sock && this.generation === generation) {
|
|
273
|
-
console.warn("SubcProvider health handler failed after its request was dispatched",
|
|
354
|
+
console.warn("SubcProvider health handler failed after its request was dispatched", error);
|
|
274
355
|
}
|
|
275
356
|
});
|
|
276
357
|
return;
|
|
@@ -278,53 +359,88 @@ export class SubcProvider {
|
|
|
278
359
|
if (request.op !== "route.bind") {
|
|
279
360
|
throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
|
|
280
361
|
}
|
|
362
|
+
const tentative = createRouteHandle(numberField(request.route_channel, "route_channel"), numberField(request.epoch, "epoch"), this.connectionToken);
|
|
281
363
|
const bindRequest = {
|
|
282
|
-
|
|
364
|
+
handle: tentative,
|
|
283
365
|
target: request.target,
|
|
284
366
|
identity: request.identity,
|
|
285
367
|
principal: request.principal,
|
|
368
|
+
consumer_capabilities: request.consumer_capabilities,
|
|
286
369
|
};
|
|
287
|
-
|
|
370
|
+
let decision;
|
|
371
|
+
try {
|
|
372
|
+
decision = await this.opts.onBind?.(bindRequest);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
try {
|
|
376
|
+
await this.sendError(frame, "route_rejected", error instanceof Error ? error.message : String(error), controlFlags(), sock, generation);
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
await this.opts.onRouteGone?.(tentative);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
288
383
|
const rejection = bindRejection(decision);
|
|
289
384
|
if (rejection) {
|
|
290
|
-
|
|
385
|
+
try {
|
|
386
|
+
await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
await this.opts.onRouteGone?.(tentative);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Response, controlFlags(), 0, 0, frame.header.corr, encodeJson({ op: "route.bind" })));
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
await this.opts.onRouteGone?.(tentative);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr) {
|
|
401
|
+
await this.opts.onRouteGone?.(tentative);
|
|
291
402
|
return;
|
|
292
403
|
}
|
|
293
|
-
|
|
404
|
+
this.liveRoutes.set(tentative.channel, tentative);
|
|
405
|
+
await this.opts.onBound?.(tentative);
|
|
294
406
|
}
|
|
295
|
-
async handleDataRequest(frame, sock, generation) {
|
|
296
|
-
const {
|
|
297
|
-
const key = routeKey(
|
|
407
|
+
async handleDataRequest(frame, handle, sock, generation) {
|
|
408
|
+
const { corr, ver } = frame.header;
|
|
409
|
+
const key = routeKey(handle, corr);
|
|
298
410
|
const controller = new AbortController();
|
|
299
411
|
this.inflight.set(key, controller);
|
|
300
|
-
const
|
|
301
|
-
|
|
412
|
+
const context = {
|
|
413
|
+
handle,
|
|
302
414
|
signal: controller.signal,
|
|
303
415
|
currentEpoch: () => this.connectionEpoch,
|
|
304
|
-
emit: async (eventBody) => {
|
|
305
|
-
|
|
416
|
+
emit: async (eventBody, options = {}) => {
|
|
417
|
+
this.assertLiveHandle(handle);
|
|
306
418
|
if (controller.signal.aborted)
|
|
307
419
|
return;
|
|
308
|
-
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamData,
|
|
420
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamData, buildFlags(false, options.priority ?? Priority.Interactive, false, options.admissionClass ?? AdmissionClass.Normal), handle.channel, handle.epoch, corr, eventBody));
|
|
309
421
|
},
|
|
310
422
|
};
|
|
311
423
|
const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
|
|
424
|
+
const dataFlags = buildFlags(false, Priority.Interactive, false);
|
|
312
425
|
try {
|
|
313
|
-
const body = await this.opts.handler(
|
|
426
|
+
const body = await this.opts.handler(handle, frame.body, context);
|
|
427
|
+
if (controller.signal.aborted)
|
|
428
|
+
return;
|
|
429
|
+
this.assertLiveHandle(handle);
|
|
314
430
|
if (body === undefined) {
|
|
315
|
-
|
|
316
|
-
// StreamEnd terminal (the consumer's subscription resolves).
|
|
317
|
-
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamEnd, dataFlags, channel, corr, new Uint8Array(0)));
|
|
431
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamEnd, dataFlags, handle.channel, handle.epoch, corr, new Uint8Array(0)));
|
|
318
432
|
}
|
|
319
433
|
else if (body instanceof Uint8Array) {
|
|
320
|
-
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, channel, corr, body));
|
|
434
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, handle.channel, handle.epoch, corr, body));
|
|
321
435
|
}
|
|
322
436
|
else {
|
|
323
437
|
throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
|
|
324
438
|
}
|
|
325
439
|
}
|
|
326
|
-
catch (
|
|
327
|
-
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (error instanceof StaleRouteHandleError || controller.signal.aborted)
|
|
442
|
+
return;
|
|
443
|
+
await this.sendError(frame, error instanceof SubcProviderError && error.code ? error.code : "handler_error", error instanceof Error ? error.message : String(error), dataFlags, sock, generation);
|
|
328
444
|
}
|
|
329
445
|
finally {
|
|
330
446
|
releasePermit();
|
|
@@ -333,8 +449,8 @@ export class SubcProvider {
|
|
|
333
449
|
}
|
|
334
450
|
}
|
|
335
451
|
async handleHealthRequest(frame, sock, generation) {
|
|
336
|
-
const {
|
|
337
|
-
const key =
|
|
452
|
+
const { corr, ver } = frame.header;
|
|
453
|
+
const key = `control:${generation}:${corr}`;
|
|
338
454
|
const controller = new AbortController();
|
|
339
455
|
this.inflight.set(key, controller);
|
|
340
456
|
const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
|
|
@@ -342,15 +458,15 @@ export class SubcProvider {
|
|
|
342
458
|
if (controller.signal.aborted)
|
|
343
459
|
return;
|
|
344
460
|
const report = await this.opts.health();
|
|
345
|
-
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(),
|
|
461
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(), 0, 0, corr, encodeJson({
|
|
346
462
|
op: HEALTH_CHECK_OP,
|
|
347
463
|
status: report.status,
|
|
348
464
|
...(report.detail === undefined ? {} : { detail: report.detail }),
|
|
349
465
|
...(report.metrics === undefined ? {} : { metrics: report.metrics }),
|
|
350
466
|
})));
|
|
351
467
|
}
|
|
352
|
-
catch (
|
|
353
|
-
await this.sendError(frame,
|
|
468
|
+
catch (error) {
|
|
469
|
+
await this.sendError(frame, error instanceof SubcProviderError && error.code ? error.code : "health_error", error instanceof Error ? error.message : String(error), controlFlags(), sock, generation);
|
|
354
470
|
}
|
|
355
471
|
finally {
|
|
356
472
|
releasePermit();
|
|
@@ -359,7 +475,7 @@ export class SubcProvider {
|
|
|
359
475
|
}
|
|
360
476
|
}
|
|
361
477
|
async sendError(frame, code, message, flags, sock, generation) {
|
|
362
|
-
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Error, flags, frame.header.channel, frame.header.corr, encodeJson({ code, message })));
|
|
478
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Error, flags, frame.header.channel, frame.header.epoch, frame.header.corr, encodeJson({ code, message })));
|
|
363
479
|
}
|
|
364
480
|
async sendOn(sock, generation, frame) {
|
|
365
481
|
if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr)
|
|
@@ -426,6 +542,9 @@ export class SubcProvider {
|
|
|
426
542
|
this.storage = opened.ack.storage;
|
|
427
543
|
this.closedErr = null;
|
|
428
544
|
this.connectionEpoch += 1;
|
|
545
|
+
this.connectionToken = newConnectionToken();
|
|
546
|
+
this.liveRoutes.clear();
|
|
547
|
+
this.nextCorr = 1n;
|
|
429
548
|
const generation = this.generation;
|
|
430
549
|
void this.readLoop(opened.sock, generation);
|
|
431
550
|
this.scheduleRestored(generation, this.connectionEpoch);
|
|
@@ -489,6 +608,24 @@ export class SubcProvider {
|
|
|
489
608
|
void this.drainConnectionStateQueue();
|
|
490
609
|
}
|
|
491
610
|
}
|
|
611
|
+
isLiveHandle(handle) {
|
|
612
|
+
return belongsToConnection(handle, this.connectionToken) && this.liveRoutes.get(handle.channel) === handle;
|
|
613
|
+
}
|
|
614
|
+
assertLiveHandle(handle) {
|
|
615
|
+
if (!this.isLiveHandle(handle))
|
|
616
|
+
throw new StaleRouteHandleError(handle);
|
|
617
|
+
}
|
|
618
|
+
allocateCorr() {
|
|
619
|
+
const maximum = 0xffffffffffffffffn;
|
|
620
|
+
if (this.nextCorr > maximum) {
|
|
621
|
+
const error = new SubcProviderError("channel-0 correlation id allocator exhausted", "corr_exhausted");
|
|
622
|
+
this.handleUnexpectedDrop(this.sock, this.generation, error);
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
const corr = this.nextCorr;
|
|
626
|
+
this.nextCorr += 1n;
|
|
627
|
+
return corr;
|
|
628
|
+
}
|
|
492
629
|
failFatal(err) {
|
|
493
630
|
if (!this.closedErr)
|
|
494
631
|
this.closedErr = err;
|
|
@@ -502,8 +639,17 @@ export class SubcProvider {
|
|
|
502
639
|
this.resolveClosed();
|
|
503
640
|
}
|
|
504
641
|
}
|
|
505
|
-
function routeKey(
|
|
506
|
-
return `${
|
|
642
|
+
function routeKey(handle, corr) {
|
|
643
|
+
return `${handle.channel}:${handle.epoch}:${corr}`;
|
|
644
|
+
}
|
|
645
|
+
function providerErrorFromFrame(frame) {
|
|
646
|
+
try {
|
|
647
|
+
const body = parseJson(frame.body);
|
|
648
|
+
return new SubcProviderError(body.message ?? "subc error", body.code);
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
return new SubcProviderError(Buffer.from(frame.body).toString("utf8") || "subc error");
|
|
652
|
+
}
|
|
507
653
|
}
|
|
508
654
|
function launchNonce(opts) {
|
|
509
655
|
const nonce = opts.launchNonce ?? process.env[SUBC_LAUNCH_NONCE_ENV];
|
|
@@ -519,6 +665,7 @@ function normalizeProviderConnectOptions(opts) {
|
|
|
519
665
|
handshakeTimeoutMs: opts.handshakeTimeoutMs,
|
|
520
666
|
controlOps: opts.controlOps,
|
|
521
667
|
onBind: opts.onBind,
|
|
668
|
+
onBound: opts.onBound,
|
|
522
669
|
onRouteGone: opts.onRouteGone,
|
|
523
670
|
reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
|
|
524
671
|
sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
@@ -536,7 +683,7 @@ function normalizedControlOps(controlOps) {
|
|
|
536
683
|
}
|
|
537
684
|
function buildHelloFrame(opts) {
|
|
538
685
|
const nonce = launchNonce(opts);
|
|
539
|
-
return buildFrame(FrameType.Hello, controlFlags(), 0, HELLO_CORR, encodeJson({
|
|
686
|
+
return buildFrame(FrameType.Hello, controlFlags(), 0, 0, HELLO_CORR, encodeJson({
|
|
540
687
|
manifest: normalizeManifest(opts.manifest),
|
|
541
688
|
protocol_ver: PROTOCOL_VERSION,
|
|
542
689
|
control_ops: normalizedControlOps(opts.controlOps),
|
|
@@ -584,14 +731,17 @@ async function sendFrame(sock, frame) {
|
|
|
584
731
|
await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
|
|
585
732
|
}
|
|
586
733
|
async function expectHelloAck(sock, deadline) {
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
734
|
+
const frame = await sock.readFrame(deadline, deadline);
|
|
735
|
+
switch (frame.header.ty) {
|
|
736
|
+
case FrameType.HelloAck: {
|
|
737
|
+
const ack = parseJson(frame.body);
|
|
738
|
+
if (ack.negotiated_ver !== PROTOCOL_VERSION) {
|
|
739
|
+
throw new SubcProviderError(`subc negotiated protocol ${ack.negotiated_ver}; expected exactly ${PROTOCOL_VERSION}`, "unsupported_version");
|
|
740
|
+
}
|
|
741
|
+
return ack;
|
|
742
|
+
}
|
|
593
743
|
case FrameType.Error: {
|
|
594
|
-
const error = parseJson(body);
|
|
744
|
+
const error = parseJson(frame.body);
|
|
595
745
|
throw new SubcProviderError(`subc rejected HELLO: ${error.code ?? "unknown"} — ${error.message ?? "subc error"}`, error.code);
|
|
596
746
|
}
|
|
597
747
|
default:
|