@cortexkit/subc-client 0.12.0 → 0.13.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/dist/client.d.ts +13 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +93 -12
- package/dist/client.js.map +1 -1
- package/dist/envelope.d.ts +1 -0
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +2 -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/route-handle.d.ts +32 -1
- package/dist/route-handle.d.ts.map +1 -1
- package/dist/route-handle.js +61 -5
- package/dist/route-handle.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +158 -12
- package/src/envelope.ts +2 -1
- package/src/index.ts +9 -1
- package/src/route-handle.ts +89 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { SubcClient, SubcError, SubcCallError, DEFAULT_RECONNECT_BACKOFF, SUBC_MODULE_ID_ENV, SUBC_LAUNCH_NONCE_ENV, isConsumerReconnectTransient, parseRouteCloseReason, classifyRouteCloseReason, routeCloseReason, isValidCapabilityIdentifier, connectionFileExists, } from "./client.js";
|
|
2
|
-
export { RouteHandle, StaleRouteHandleError } from "./route-handle.js";
|
|
2
|
+
export { ReverseRequestRegistry, ReverseRequestRegistrationError, RouteHandle, StaleRouteHandleError, } from "./route-handle.js";
|
|
3
3
|
export { readConnectionFile, ConnectionFileError, } from "./connection-file.js";
|
|
4
|
-
export { FrameType, Priority, AdmissionClass, DAEMON_ORIGIN_FLAG, PROTOCOL_VERSION, HEADER_LEN, buildFrame, buildFrameWithVersion, buildFlags, encodeFrame, decodeHeader, hasBinary, hasDaemonOrigin, encodeHeader, DecodeError, } from "./envelope.js";
|
|
4
|
+
export { FrameType, Priority, AdmissionClass, DAEMON_ORIGIN_FLAG, SUBSCRIPTION_FLAG, PROTOCOL_VERSION, HEADER_LEN, buildFrame, buildFrameWithVersion, buildFlags, encodeFrame, decodeHeader, hasBinary, hasDaemonOrigin, encodeHeader, DecodeError, } from "./envelope.js";
|
|
5
5
|
export { authenticateClient, computeProof, AuthError, NONCE_LEN, PROOF_LEN, SERVER_PROOF_DOMAIN, CLIENT_AUTH_DOMAIN, } from "./auth.js";
|
|
6
6
|
export { SubcSocket, SocketClosedError, SocketTimeoutError } from "./socket.js";
|
|
7
7
|
export { SubcProvider, SubcProviderError, HELLO_CORR, managementSurfaceManifest, jsonProviderHandler, } from "./provider.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,aAAa,EACb,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EACrB,4BAA4B,EAC5B,qBAAqB,EACrB,wBAAwB,EACxB,gBAAgB,EAChB,2BAA2B,EAC3B,oBAAoB,GAwBrB,MAAM,aAAa,CAAC;AACrB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,aAAa,EACb,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EACrB,4BAA4B,EAC5B,qBAAqB,EACrB,wBAAwB,EACxB,gBAAgB,EAChB,2BAA2B,EAC3B,oBAAoB,GAwBrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,+BAA+B,EAC/B,WAAW,EACX,qBAAqB,GAGtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,SAAS,EACT,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,qBAAqB,EACrB,UAAU,EACV,WAAW,EACX,YAAY,EACZ,SAAS,EACT,eAAe,EACf,YAAY,EACZ,WAAW,GAIZ,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,EACT,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,yBAAyB,EACzB,mBAAmB,GAmCpB,MAAM,eAAe,CAAC"}
|
package/dist/route-handle.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
export interface ReverseRequestContext {
|
|
2
|
+
readonly corr: bigint;
|
|
3
|
+
readonly method: string;
|
|
4
|
+
}
|
|
5
|
+
export type ReverseRequestHandler = (body: Uint8Array, ctx: ReverseRequestContext) => Promise<Uint8Array> | Uint8Array;
|
|
6
|
+
/** A reverse-request registration cannot add a capability after route.open declared its set. */
|
|
7
|
+
export declare class ReverseRequestRegistrationError extends Error {
|
|
8
|
+
readonly methodFamily: string;
|
|
9
|
+
readonly code = "reverse_request_capability_not_declared";
|
|
10
|
+
constructor(methodFamily: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mutable handler registry prepared before route.open. The client derives
|
|
14
|
+
* consumer_capabilities from its method families and seals that set when the
|
|
15
|
+
* route opens; handlers for declared families may still be replaced afterward.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ReverseRequestRegistry {
|
|
18
|
+
private readonly handlers;
|
|
19
|
+
private declaredFamilies;
|
|
20
|
+
onRequest(methodFamily: string, handler: ReverseRequestHandler): void;
|
|
21
|
+
/** @internal */
|
|
22
|
+
capabilities(): string[];
|
|
23
|
+
/** @internal */
|
|
24
|
+
handler(methodFamily: string): ReverseRequestHandler | undefined;
|
|
25
|
+
/** @internal */
|
|
26
|
+
seal(): void;
|
|
27
|
+
}
|
|
1
28
|
/**
|
|
2
29
|
* Immutable identity for one route binding on one live socket. Only channel and
|
|
3
30
|
* epoch cross the wire; the connection token remains private to this SDK.
|
|
@@ -6,10 +33,14 @@ export declare class RouteHandle {
|
|
|
6
33
|
readonly channel: number;
|
|
7
34
|
readonly epoch: number;
|
|
8
35
|
private constructor();
|
|
36
|
+
/** Replace the handler for a capability declared when this route opened. */
|
|
37
|
+
onRequest(methodFamily: string, handler: ReverseRequestHandler): void;
|
|
9
38
|
private static create;
|
|
10
39
|
}
|
|
11
40
|
/** @internal SDK factory; not re-exported from the package surface. */
|
|
12
|
-
export declare function createRouteHandle(channel: number, epoch: number, token: object): RouteHandle;
|
|
41
|
+
export declare function createRouteHandle(channel: number, epoch: number, token: object, registry?: ReverseRequestRegistry): RouteHandle;
|
|
42
|
+
/** @internal */
|
|
43
|
+
export declare function reverseRequestHandler(handle: RouteHandle, methodFamily: string): ReverseRequestHandler | undefined;
|
|
13
44
|
/** A route handle belongs to another connection or is no longer installed. */
|
|
14
45
|
export declare class StaleRouteHandleError extends Error {
|
|
15
46
|
readonly handle: RouteHandle;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-handle.d.ts","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"route-handle.d.ts","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,qBAAqB,KACvB,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC;AAEtC,gGAAgG;AAChG,qBAAa,+BAAgC,SAAQ,KAAK;IAG5C,QAAQ,CAAC,YAAY,EAAE,MAAM;IAFzC,QAAQ,CAAC,IAAI,6CAA6C;gBAErC,YAAY,EAAE,MAAM;CAI1C;AAED;;;;GAIG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IACrE,OAAO,CAAC,gBAAgB,CAAoC;IAE5D,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAWrE,gBAAgB;IAChB,YAAY,IAAI,MAAM,EAAE;IAIxB,gBAAgB;IAChB,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS;IAIhE,gBAAgB;IAChB,IAAI,IAAI,IAAI;CAGb;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,OAAO;IAeP,4EAA4E;IAC5E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAIrE,OAAO,CAAC,MAAM,CAAC,MAAM;CAQtB;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,QAAQ,yBAA+B,GACtC,WAAW,CAKb;AAED,gBAAgB;AAChB,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAElH;AAMD,8EAA8E;AAC9E,qBAAa,qBAAsB,SAAQ,KAAK;IAGlC,QAAQ,CAAC,MAAM,EAAE,WAAW;IAFxC,QAAQ,CAAC,IAAI,wBAAwB;gBAEhB,MAAM,EAAE,WAAW;CAIzC;AAED,gBAAgB;AAChB,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,gBAAgB;AAChB,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/E;AAED,gBAAgB;AAChB,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAE9E"}
|
package/dist/route-handle.js
CHANGED
|
@@ -1,4 +1,47 @@
|
|
|
1
1
|
const connectionToken = new WeakMap();
|
|
2
|
+
const reverseRequests = new WeakMap();
|
|
3
|
+
/** A reverse-request registration cannot add a capability after route.open declared its set. */
|
|
4
|
+
export class ReverseRequestRegistrationError extends Error {
|
|
5
|
+
methodFamily;
|
|
6
|
+
code = "reverse_request_capability_not_declared";
|
|
7
|
+
constructor(methodFamily) {
|
|
8
|
+
super(`reverse-request capability ${JSON.stringify(methodFamily)} was not registered before route.open`);
|
|
9
|
+
this.methodFamily = methodFamily;
|
|
10
|
+
this.name = "ReverseRequestRegistrationError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Mutable handler registry prepared before route.open. The client derives
|
|
15
|
+
* consumer_capabilities from its method families and seals that set when the
|
|
16
|
+
* route opens; handlers for declared families may still be replaced afterward.
|
|
17
|
+
*/
|
|
18
|
+
export class ReverseRequestRegistry {
|
|
19
|
+
handlers = new Map();
|
|
20
|
+
declaredFamilies = null;
|
|
21
|
+
onRequest(methodFamily, handler) {
|
|
22
|
+
if (!isMethodFamily(methodFamily)) {
|
|
23
|
+
throw new TypeError(`reverse-request method family must be a capability identifier, got ${JSON.stringify(methodFamily)}`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof handler !== "function")
|
|
26
|
+
throw new TypeError("reverse-request handler must be a function");
|
|
27
|
+
if (this.declaredFamilies && !this.declaredFamilies.has(methodFamily)) {
|
|
28
|
+
throw new ReverseRequestRegistrationError(methodFamily);
|
|
29
|
+
}
|
|
30
|
+
this.handlers.set(methodFamily, handler);
|
|
31
|
+
}
|
|
32
|
+
/** @internal */
|
|
33
|
+
capabilities() {
|
|
34
|
+
return [...this.handlers.keys()].sort();
|
|
35
|
+
}
|
|
36
|
+
/** @internal */
|
|
37
|
+
handler(methodFamily) {
|
|
38
|
+
return this.handlers.get(methodFamily);
|
|
39
|
+
}
|
|
40
|
+
/** @internal */
|
|
41
|
+
seal() {
|
|
42
|
+
this.declaredFamilies ??= new Set(this.handlers.keys());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
2
45
|
/**
|
|
3
46
|
* Immutable identity for one route binding on one live socket. Only channel and
|
|
4
47
|
* epoch cross the wire; the connection token remains private to this SDK.
|
|
@@ -6,7 +49,7 @@ const connectionToken = new WeakMap();
|
|
|
6
49
|
export class RouteHandle {
|
|
7
50
|
channel;
|
|
8
51
|
epoch;
|
|
9
|
-
constructor(channel, epoch, token) {
|
|
52
|
+
constructor(channel, epoch, token, registry) {
|
|
10
53
|
if (!Number.isInteger(channel) || channel <= 0 || channel > 0xffff) {
|
|
11
54
|
throw new RangeError(`route channel must be an integer in 1..65535, got ${channel}`);
|
|
12
55
|
}
|
|
@@ -15,17 +58,30 @@ export class RouteHandle {
|
|
|
15
58
|
}
|
|
16
59
|
this.channel = channel;
|
|
17
60
|
this.epoch = epoch;
|
|
61
|
+
registry.seal();
|
|
18
62
|
connectionToken.set(this, token);
|
|
63
|
+
reverseRequests.set(this, registry);
|
|
19
64
|
Object.freeze(this);
|
|
20
65
|
}
|
|
21
|
-
|
|
22
|
-
|
|
66
|
+
/** Replace the handler for a capability declared when this route opened. */
|
|
67
|
+
onRequest(methodFamily, handler) {
|
|
68
|
+
reverseRequests.get(this).onRequest(methodFamily, handler);
|
|
69
|
+
}
|
|
70
|
+
static create(channel, epoch, token, registry) {
|
|
71
|
+
return new RouteHandle(channel, epoch, token, registry);
|
|
23
72
|
}
|
|
24
73
|
}
|
|
25
74
|
/** @internal SDK factory; not re-exported from the package surface. */
|
|
26
|
-
export function createRouteHandle(channel, epoch, token) {
|
|
75
|
+
export function createRouteHandle(channel, epoch, token, registry = new ReverseRequestRegistry()) {
|
|
27
76
|
const factory = RouteHandle;
|
|
28
|
-
return factory.create(channel, epoch, token);
|
|
77
|
+
return factory.create(channel, epoch, token, registry);
|
|
78
|
+
}
|
|
79
|
+
/** @internal */
|
|
80
|
+
export function reverseRequestHandler(handle, methodFamily) {
|
|
81
|
+
return reverseRequests.get(handle)?.handler(methodFamily);
|
|
82
|
+
}
|
|
83
|
+
function isMethodFamily(value) {
|
|
84
|
+
return /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(value);
|
|
29
85
|
}
|
|
30
86
|
/** A route handle belongs to another connection or is no longer installed. */
|
|
31
87
|
export class StaleRouteHandleError extends Error {
|
package/dist/route-handle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-handle.js","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAG,IAAI,OAAO,EAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"route-handle.js","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/subc-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "TypeScript client for the subc daemon. Wire-compatible (byte-for-byte) with the Rust subc-transport handshake and subc-protocol envelope.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/client.ts
CHANGED
|
@@ -22,12 +22,15 @@ import {
|
|
|
22
22
|
FrameType,
|
|
23
23
|
hasBinary,
|
|
24
24
|
Priority,
|
|
25
|
+
SUBSCRIPTION_FLAG,
|
|
25
26
|
type Frame,
|
|
26
27
|
} from "./envelope.js";
|
|
27
28
|
import {
|
|
28
29
|
belongsToConnection,
|
|
29
30
|
createRouteHandle,
|
|
30
31
|
newConnectionToken,
|
|
32
|
+
reverseRequestHandler,
|
|
33
|
+
ReverseRequestRegistry,
|
|
31
34
|
RouteHandle,
|
|
32
35
|
sameRouteHandle,
|
|
33
36
|
StaleRouteHandleError,
|
|
@@ -109,8 +112,11 @@ export interface ConsumerIdentity {
|
|
|
109
112
|
export interface RouteOpenOptions {
|
|
110
113
|
/** Optional override for the consumer identity; by default the SUBC_MODULE_ID and SUBC_LAUNCH_NONCE environment variables are used when both are non-empty. Set null to send route.open without consumer_identity. */
|
|
111
114
|
consumerIdentity?: ConsumerIdentity | null;
|
|
112
|
-
/**
|
|
113
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Handlers prepared before route.open. The client derives consumer_capabilities
|
|
117
|
+
* from this registry; callers cannot declare a capability without a handler.
|
|
118
|
+
*/
|
|
119
|
+
reverseRequests?: ReverseRequestRegistry;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
export interface CatalogCapabilityRequirement {
|
|
@@ -193,6 +199,8 @@ export interface ManagedCallOptions extends RequestOptions {
|
|
|
193
199
|
targetKind?: ManagedRouteKind;
|
|
194
200
|
/** Optional override for the consumer identity; by default the SUBC_MODULE_ID and SUBC_LAUNCH_NONCE environment variables are used when both are non-empty. Set null to send route.open without consumer_identity. */
|
|
195
201
|
consumerIdentity?: ConsumerIdentity | null;
|
|
202
|
+
/** Reverse-request handlers used by the managed route and all of its reconnect reopens. */
|
|
203
|
+
reverseRequests?: ReverseRequestRegistry;
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
export interface SubscribeOptions {
|
|
@@ -221,6 +229,8 @@ export interface CloseRouteOptions {
|
|
|
221
229
|
export interface ManagedCloseRouteOptions extends CloseRouteOptions {
|
|
222
230
|
/** Consumer identity used by the cached managed route. */
|
|
223
231
|
consumerIdentity?: ConsumerIdentity | null;
|
|
232
|
+
/** The registry whose derived capability set identifies the cached route. */
|
|
233
|
+
reverseRequests?: ReverseRequestRegistry;
|
|
224
234
|
}
|
|
225
235
|
|
|
226
236
|
/**
|
|
@@ -468,6 +478,7 @@ interface CachedRoute {
|
|
|
468
478
|
target: Extract<RouteTarget, { kind: ManagedRouteKind }>;
|
|
469
479
|
identity: BindIdentity;
|
|
470
480
|
consumerIdentity?: ConsumerIdentity;
|
|
481
|
+
reverseRequests: ReverseRequestRegistry;
|
|
471
482
|
handle: RouteHandle | null;
|
|
472
483
|
opening: Promise<RouteHandle> | null;
|
|
473
484
|
/**
|
|
@@ -566,13 +577,15 @@ export class SubcClient {
|
|
|
566
577
|
/** Open a route and return its connection-bound immutable handle. */
|
|
567
578
|
async routeOpen(target: RouteTarget, identity: BindIdentity, opts: RouteOpenOptions = {}): Promise<RouteHandle> {
|
|
568
579
|
const consumerIdentity = routeOpenConsumerIdentity(opts);
|
|
569
|
-
const
|
|
580
|
+
const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry();
|
|
581
|
+
reverseRequests.seal();
|
|
582
|
+
const consumerCapabilities = reverseRequests.capabilities();
|
|
570
583
|
const body = this.encode({
|
|
571
584
|
op: "route.open",
|
|
572
585
|
target,
|
|
573
586
|
identity,
|
|
574
587
|
...(consumerIdentity ? { consumer_identity: consumerIdentity } : {}),
|
|
575
|
-
...(consumerCapabilities
|
|
588
|
+
...(consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {}),
|
|
576
589
|
});
|
|
577
590
|
|
|
578
591
|
let installed: RouteHandle | null = null;
|
|
@@ -582,7 +595,7 @@ export class SubcClient {
|
|
|
582
595
|
if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number") {
|
|
583
596
|
throw new SubcError(`route.open returned no route handle: ${JSON.stringify(parsed)}`);
|
|
584
597
|
}
|
|
585
|
-
installed = this.installRoute(parsed.route_channel, parsed.route_epoch);
|
|
598
|
+
installed = this.installRoute(parsed.route_channel, parsed.route_epoch, reverseRequests);
|
|
586
599
|
return true;
|
|
587
600
|
};
|
|
588
601
|
const closeLateRoute = (frame: Frame): void => {
|
|
@@ -749,7 +762,7 @@ export class SubcClient {
|
|
|
749
762
|
this.pending.set(key, subscriptionPending);
|
|
750
763
|
const frame = buildFrame(
|
|
751
764
|
FrameType.Request,
|
|
752
|
-
buildFlags(false, priority, false, admission),
|
|
765
|
+
buildFlags(false, priority, false, admission) | SUBSCRIPTION_FLAG,
|
|
753
766
|
handle.channel,
|
|
754
767
|
handle.epoch,
|
|
755
768
|
corr,
|
|
@@ -857,7 +870,12 @@ export class SubcClient {
|
|
|
857
870
|
identity: BindIdentity,
|
|
858
871
|
opts: ManagedCloseRouteOptions = {},
|
|
859
872
|
): Promise<void> {
|
|
860
|
-
const key = routeCacheKey(
|
|
873
|
+
const key = routeCacheKey(
|
|
874
|
+
target,
|
|
875
|
+
identity,
|
|
876
|
+
routeOpenConsumerIdentity(opts),
|
|
877
|
+
(opts.reverseRequests ?? new ReverseRequestRegistry()).capabilities(),
|
|
878
|
+
);
|
|
861
879
|
const cached = this.routes.get(key);
|
|
862
880
|
if (!cached) return;
|
|
863
881
|
cached.closed = true;
|
|
@@ -1139,7 +1157,9 @@ export class SubcClient {
|
|
|
1139
1157
|
{ kind: ManagedRouteKind }
|
|
1140
1158
|
>;
|
|
1141
1159
|
const consumerIdentity = routeOpenConsumerIdentity(opts);
|
|
1142
|
-
const
|
|
1160
|
+
const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry();
|
|
1161
|
+
reverseRequests.seal();
|
|
1162
|
+
const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities());
|
|
1143
1163
|
let cached = this.routes.get(key);
|
|
1144
1164
|
if (!cached) {
|
|
1145
1165
|
cached = {
|
|
@@ -1148,6 +1168,7 @@ export class SubcClient {
|
|
|
1148
1168
|
target,
|
|
1149
1169
|
identity,
|
|
1150
1170
|
consumerIdentity,
|
|
1171
|
+
reverseRequests,
|
|
1151
1172
|
handle: null,
|
|
1152
1173
|
opening: null,
|
|
1153
1174
|
};
|
|
@@ -1178,6 +1199,7 @@ export class SubcClient {
|
|
|
1178
1199
|
try {
|
|
1179
1200
|
const handle = await this.routeOpen(cached.target, cached.identity, {
|
|
1180
1201
|
consumerIdentity: cached.consumerIdentity ?? null,
|
|
1202
|
+
reverseRequests: cached.reverseRequests,
|
|
1181
1203
|
});
|
|
1182
1204
|
if (cached.closed) {
|
|
1183
1205
|
this.liveRoutes.delete(handle.channel);
|
|
@@ -1401,6 +1423,7 @@ export class SubcClient {
|
|
|
1401
1423
|
try {
|
|
1402
1424
|
const handle = await this.routeOpen(cached.target, cached.identity, {
|
|
1403
1425
|
consumerIdentity: cached.consumerIdentity ?? null,
|
|
1426
|
+
reverseRequests: cached.reverseRequests,
|
|
1404
1427
|
});
|
|
1405
1428
|
if (cached.closed) {
|
|
1406
1429
|
this.liveRoutes.delete(handle.channel);
|
|
@@ -1504,6 +1527,11 @@ export class SubcClient {
|
|
|
1504
1527
|
}
|
|
1505
1528
|
}
|
|
1506
1529
|
|
|
1530
|
+
if (frame.header.ty === FrameType.Request && handle) {
|
|
1531
|
+
this.dispatchReverseRequest(frame, handle);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1507
1535
|
const key = pendingKey(handle, frame.header.corr);
|
|
1508
1536
|
const pending = this.pending.get(key);
|
|
1509
1537
|
if (pending) {
|
|
@@ -1607,6 +1635,109 @@ export class SubcClient {
|
|
|
1607
1635
|
this.settle(key, pending, () => pending.reject(pending.classifyFailure?.(err) ?? err));
|
|
1608
1636
|
}
|
|
1609
1637
|
|
|
1638
|
+
private dispatchReverseRequest(frame: Frame, handle: RouteHandle): void {
|
|
1639
|
+
const method = reverseRequestMethod(frame.body);
|
|
1640
|
+
const methodFamily = method?.split("/", 1)[0];
|
|
1641
|
+
const handler = methodFamily ? reverseRequestHandler(handle, methodFamily) : undefined;
|
|
1642
|
+
const sock = this.sock;
|
|
1643
|
+
const generation = this.generation;
|
|
1644
|
+
|
|
1645
|
+
// ONE CODE, TWO SITES, ON PURPOSE. `reverse_request_unhandled` is emitted
|
|
1646
|
+
// both here (no handler registered) and below (a handler that threw), with
|
|
1647
|
+
// different MESSAGES and the same CODE. That is a ruling, not an oversight:
|
|
1648
|
+
// the distinction between "nobody listening" and "the listener broke" is
|
|
1649
|
+
// ours to log, and every provider's decision is identical for both -- not
|
|
1650
|
+
// granted, retry later. A second code would be a second thing every
|
|
1651
|
+
// consumer's decoder must learn for a branch none of them takes. If one ever
|
|
1652
|
+
// needs it, the message carries it. (Settled with prefrontal, the lane's
|
|
1653
|
+
// first consumer, before anyone pinned on it.)
|
|
1654
|
+
//
|
|
1655
|
+
// What must NOT join them: a handler that RETURNS bytes declining the
|
|
1656
|
+
// request answers with an ordinary Response in the provider's own
|
|
1657
|
+
// vocabulary. Error means "the consumer did not answer", never "the consumer
|
|
1658
|
+
// said no" -- that separation is the reason this lane exists.
|
|
1659
|
+
const reply = async (): Promise<void> => {
|
|
1660
|
+
if (!handler || !method) {
|
|
1661
|
+
await this.sendReverseError(
|
|
1662
|
+
frame,
|
|
1663
|
+
handle,
|
|
1664
|
+
"reverse_request_unhandled",
|
|
1665
|
+
methodFamily
|
|
1666
|
+
? `no reverse-request handler is registered for ${methodFamily}`
|
|
1667
|
+
: "reverse request has no valid method",
|
|
1668
|
+
sock,
|
|
1669
|
+
generation,
|
|
1670
|
+
);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
let body: Uint8Array;
|
|
1675
|
+
try {
|
|
1676
|
+
body = await handler(frame.body, { corr: frame.header.corr, method });
|
|
1677
|
+
if (!(body instanceof Uint8Array)) {
|
|
1678
|
+
throw new TypeError("reverse-request handler must return a Uint8Array");
|
|
1679
|
+
}
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
await this.sendReverseError(
|
|
1682
|
+
frame,
|
|
1683
|
+
handle,
|
|
1684
|
+
"reverse_request_unhandled",
|
|
1685
|
+
error instanceof Error ? error.message : String(error),
|
|
1686
|
+
sock,
|
|
1687
|
+
generation,
|
|
1688
|
+
);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
await this.sendReverseReply(frame, handle, FrameType.Response, body, sock, generation);
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
void reply().catch((error) => {
|
|
1696
|
+
if (this.sock === sock && this.generation === generation && !this.closeStarted) {
|
|
1697
|
+
debug("failed to answer reverse request: %s", causeMessage(error));
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
private async sendReverseError(
|
|
1703
|
+
request: Frame,
|
|
1704
|
+
handle: RouteHandle,
|
|
1705
|
+
code: string,
|
|
1706
|
+
message: string,
|
|
1707
|
+
sock: SubcSocket,
|
|
1708
|
+
generation: number,
|
|
1709
|
+
): Promise<void> {
|
|
1710
|
+
await this.sendReverseReply(request, handle, FrameType.Error, this.encode({ code, message }), sock, generation);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
private async sendReverseReply(
|
|
1714
|
+
request: Frame,
|
|
1715
|
+
handle: RouteHandle,
|
|
1716
|
+
type: FrameType.Response | FrameType.Error,
|
|
1717
|
+
body: Uint8Array,
|
|
1718
|
+
sock: SubcSocket,
|
|
1719
|
+
generation: number,
|
|
1720
|
+
): Promise<void> {
|
|
1721
|
+
if (
|
|
1722
|
+
this.sock !== sock ||
|
|
1723
|
+
this.generation !== generation ||
|
|
1724
|
+
!this.isLiveHandle(handle) ||
|
|
1725
|
+
this.closeStarted ||
|
|
1726
|
+
this.closedErr
|
|
1727
|
+
) {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const frame = buildFrame(
|
|
1731
|
+
type,
|
|
1732
|
+
buildFlags(false, Priority.Interactive, false),
|
|
1733
|
+
handle.channel,
|
|
1734
|
+
handle.epoch,
|
|
1735
|
+
request.header.corr,
|
|
1736
|
+
body,
|
|
1737
|
+
);
|
|
1738
|
+
await writeBorrowed(sock, encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1610
1741
|
private errorFromFrame(frame: Frame): SubcError {
|
|
1611
1742
|
try {
|
|
1612
1743
|
const parsed = JSON.parse(Buffer.from(frame.body).toString("utf8")) as {
|
|
@@ -1663,8 +1794,12 @@ export class SubcClient {
|
|
|
1663
1794
|
return this.ingressEpochDropCount;
|
|
1664
1795
|
}
|
|
1665
1796
|
|
|
1666
|
-
private installRoute(
|
|
1667
|
-
|
|
1797
|
+
private installRoute(
|
|
1798
|
+
channel: number,
|
|
1799
|
+
epoch: number,
|
|
1800
|
+
reverseRequests = new ReverseRequestRegistry(),
|
|
1801
|
+
): RouteHandle {
|
|
1802
|
+
const handle = createRouteHandle(channel, epoch, this.connectionToken, reverseRequests);
|
|
1668
1803
|
this.liveRoutes.set(channel, handle);
|
|
1669
1804
|
return handle;
|
|
1670
1805
|
}
|
|
@@ -1813,12 +1948,14 @@ export function isValidCapabilityIdentifier(identifier: string): boolean {
|
|
|
1813
1948
|
function routeCacheKey(
|
|
1814
1949
|
target: Extract<RouteTarget, { kind: ManagedRouteKind }>,
|
|
1815
1950
|
identity: BindIdentity,
|
|
1816
|
-
consumerIdentity
|
|
1951
|
+
consumerIdentity: ConsumerIdentity | undefined,
|
|
1952
|
+
consumerCapabilities: readonly string[],
|
|
1817
1953
|
): string {
|
|
1818
1954
|
const consumerPart = consumerIdentity
|
|
1819
1955
|
? `${consumerIdentity.module_id}\0${consumerIdentity.launch_nonce}`
|
|
1820
1956
|
: "";
|
|
1821
|
-
|
|
1957
|
+
const capabilitiesPart = [...consumerCapabilities].sort().join("\0");
|
|
1958
|
+
return `${target.kind}\0${target.module_id}\0${identity.project_root}\0${identity.harness}\0${identity.session}\0${consumerPart}\0${capabilitiesPart}`;
|
|
1822
1959
|
}
|
|
1823
1960
|
|
|
1824
1961
|
function routeOpenConsumerIdentity(opts: RouteOpenOptions = {}): ConsumerIdentity | undefined {
|
|
@@ -1845,3 +1982,12 @@ function causeMessage(cause: unknown): string {
|
|
|
1845
1982
|
function pendingKey(handle: RouteHandle | null, corr: bigint): string {
|
|
1846
1983
|
return handle ? `${handle.channel}:${handle.epoch}:${corr}` : `0:0:${corr}`;
|
|
1847
1984
|
}
|
|
1985
|
+
|
|
1986
|
+
function reverseRequestMethod(body: Uint8Array): string | undefined {
|
|
1987
|
+
try {
|
|
1988
|
+
const parsed = JSON.parse(Buffer.from(body).toString("utf8")) as { method?: unknown };
|
|
1989
|
+
return typeof parsed.method === "string" && parsed.method.length > 0 ? parsed.method : undefined;
|
|
1990
|
+
} catch {
|
|
1991
|
+
return undefined;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
package/src/envelope.ts
CHANGED
|
@@ -51,7 +51,8 @@ const FLAG_LAST = 0b0000_1000;
|
|
|
51
51
|
const FLAG_ADMISSION_MASK = 0b0011_0000;
|
|
52
52
|
const FLAG_ADMISSION_SHIFT = 4;
|
|
53
53
|
export const DAEMON_ORIGIN_FLAG = 0x40;
|
|
54
|
-
const
|
|
54
|
+
export const SUBSCRIPTION_FLAG = 0x80;
|
|
55
|
+
const FLAG_RESERVED_MASK = 0;
|
|
55
56
|
|
|
56
57
|
/** Build flags from typed components. Admission defaults to NORMAL. */
|
|
57
58
|
export function buildFlags(
|
package/src/index.ts
CHANGED
|
@@ -35,7 +35,14 @@ export {
|
|
|
35
35
|
type RoutePollKind,
|
|
36
36
|
type RoutePollResult,
|
|
37
37
|
} from "./client.js";
|
|
38
|
-
export {
|
|
38
|
+
export {
|
|
39
|
+
ReverseRequestRegistry,
|
|
40
|
+
ReverseRequestRegistrationError,
|
|
41
|
+
RouteHandle,
|
|
42
|
+
StaleRouteHandleError,
|
|
43
|
+
type ReverseRequestContext,
|
|
44
|
+
type ReverseRequestHandler,
|
|
45
|
+
} from "./route-handle.js";
|
|
39
46
|
export {
|
|
40
47
|
readConnectionFile,
|
|
41
48
|
ConnectionFileError,
|
|
@@ -47,6 +54,7 @@ export {
|
|
|
47
54
|
Priority,
|
|
48
55
|
AdmissionClass,
|
|
49
56
|
DAEMON_ORIGIN_FLAG,
|
|
57
|
+
SUBSCRIPTION_FLAG,
|
|
50
58
|
PROTOCOL_VERSION,
|
|
51
59
|
HEADER_LEN,
|
|
52
60
|
buildFrame,
|
package/src/route-handle.ts
CHANGED
|
@@ -1,4 +1,61 @@
|
|
|
1
1
|
const connectionToken = new WeakMap<RouteHandle, object>();
|
|
2
|
+
const reverseRequests = new WeakMap<RouteHandle, ReverseRequestRegistry>();
|
|
3
|
+
|
|
4
|
+
export interface ReverseRequestContext {
|
|
5
|
+
readonly corr: bigint;
|
|
6
|
+
readonly method: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ReverseRequestHandler = (
|
|
10
|
+
body: Uint8Array,
|
|
11
|
+
ctx: ReverseRequestContext,
|
|
12
|
+
) => Promise<Uint8Array> | Uint8Array;
|
|
13
|
+
|
|
14
|
+
/** A reverse-request registration cannot add a capability after route.open declared its set. */
|
|
15
|
+
export class ReverseRequestRegistrationError extends Error {
|
|
16
|
+
readonly code = "reverse_request_capability_not_declared";
|
|
17
|
+
|
|
18
|
+
constructor(readonly methodFamily: string) {
|
|
19
|
+
super(`reverse-request capability ${JSON.stringify(methodFamily)} was not registered before route.open`);
|
|
20
|
+
this.name = "ReverseRequestRegistrationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mutable handler registry prepared before route.open. The client derives
|
|
26
|
+
* consumer_capabilities from its method families and seals that set when the
|
|
27
|
+
* route opens; handlers for declared families may still be replaced afterward.
|
|
28
|
+
*/
|
|
29
|
+
export class ReverseRequestRegistry {
|
|
30
|
+
private readonly handlers = new Map<string, ReverseRequestHandler>();
|
|
31
|
+
private declaredFamilies: ReadonlySet<string> | null = null;
|
|
32
|
+
|
|
33
|
+
onRequest(methodFamily: string, handler: ReverseRequestHandler): void {
|
|
34
|
+
if (!isMethodFamily(methodFamily)) {
|
|
35
|
+
throw new TypeError(`reverse-request method family must be a capability identifier, got ${JSON.stringify(methodFamily)}`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof handler !== "function") throw new TypeError("reverse-request handler must be a function");
|
|
38
|
+
if (this.declaredFamilies && !this.declaredFamilies.has(methodFamily)) {
|
|
39
|
+
throw new ReverseRequestRegistrationError(methodFamily);
|
|
40
|
+
}
|
|
41
|
+
this.handlers.set(methodFamily, handler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @internal */
|
|
45
|
+
capabilities(): string[] {
|
|
46
|
+
return [...this.handlers.keys()].sort();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
handler(methodFamily: string): ReverseRequestHandler | undefined {
|
|
51
|
+
return this.handlers.get(methodFamily);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @internal */
|
|
55
|
+
seal(): void {
|
|
56
|
+
this.declaredFamilies ??= new Set(this.handlers.keys());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
2
59
|
|
|
3
60
|
/**
|
|
4
61
|
* Immutable identity for one route binding on one live socket. Only channel and
|
|
@@ -8,7 +65,7 @@ export class RouteHandle {
|
|
|
8
65
|
readonly channel: number;
|
|
9
66
|
readonly epoch: number;
|
|
10
67
|
|
|
11
|
-
private constructor(channel: number, epoch: number, token: object) {
|
|
68
|
+
private constructor(channel: number, epoch: number, token: object, registry: ReverseRequestRegistry) {
|
|
12
69
|
if (!Number.isInteger(channel) || channel <= 0 || channel > 0xffff) {
|
|
13
70
|
throw new RangeError(`route channel must be an integer in 1..65535, got ${channel}`);
|
|
14
71
|
}
|
|
@@ -17,21 +74,47 @@ export class RouteHandle {
|
|
|
17
74
|
}
|
|
18
75
|
this.channel = channel;
|
|
19
76
|
this.epoch = epoch;
|
|
77
|
+
registry.seal();
|
|
20
78
|
connectionToken.set(this, token);
|
|
79
|
+
reverseRequests.set(this, registry);
|
|
21
80
|
Object.freeze(this);
|
|
22
81
|
}
|
|
23
82
|
|
|
24
|
-
|
|
25
|
-
|
|
83
|
+
/** Replace the handler for a capability declared when this route opened. */
|
|
84
|
+
onRequest(methodFamily: string, handler: ReverseRequestHandler): void {
|
|
85
|
+
reverseRequests.get(this)!.onRequest(methodFamily, handler);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private static create(
|
|
89
|
+
channel: number,
|
|
90
|
+
epoch: number,
|
|
91
|
+
token: object,
|
|
92
|
+
registry: ReverseRequestRegistry,
|
|
93
|
+
): RouteHandle {
|
|
94
|
+
return new RouteHandle(channel, epoch, token, registry);
|
|
26
95
|
}
|
|
27
96
|
}
|
|
28
97
|
|
|
29
98
|
/** @internal SDK factory; not re-exported from the package surface. */
|
|
30
|
-
export function createRouteHandle(
|
|
99
|
+
export function createRouteHandle(
|
|
100
|
+
channel: number,
|
|
101
|
+
epoch: number,
|
|
102
|
+
token: object,
|
|
103
|
+
registry = new ReverseRequestRegistry(),
|
|
104
|
+
): RouteHandle {
|
|
31
105
|
const factory = RouteHandle as unknown as {
|
|
32
|
-
create(channel: number, epoch: number, token: object): RouteHandle;
|
|
106
|
+
create(channel: number, epoch: number, token: object, registry: ReverseRequestRegistry): RouteHandle;
|
|
33
107
|
};
|
|
34
|
-
return factory.create(channel, epoch, token);
|
|
108
|
+
return factory.create(channel, epoch, token, registry);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @internal */
|
|
112
|
+
export function reverseRequestHandler(handle: RouteHandle, methodFamily: string): ReverseRequestHandler | undefined {
|
|
113
|
+
return reverseRequests.get(handle)?.handler(methodFamily);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isMethodFamily(value: string): boolean {
|
|
117
|
+
return /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(value);
|
|
35
118
|
}
|
|
36
119
|
|
|
37
120
|
/** A route handle belongs to another connection or is no longer installed. */
|