@cortexkit/subc-client 0.4.1 → 0.8.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/auth.d.ts.map +1 -1
- package/dist/auth.js +17 -5
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +57 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +168 -14
- package/dist/client.js.map +1 -1
- package/dist/connection-file.d.ts.map +1 -1
- package/dist/connection-file.js +11 -1
- package/dist/connection-file.js.map +1 -1
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +36 -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.map +1 -1
- package/dist/provider.d.ts +8 -25
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +123 -53
- package/dist/provider.js.map +1 -1
- package/dist/socket.d.ts +12 -0
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +33 -3
- package/dist/socket.js.map +1 -1
- package/package.json +1 -1
- package/src/auth.ts +21 -9
- package/src/client.ts +210 -20
- package/src/connection-file.ts +15 -1
- package/src/envelope.ts +54 -1
- package/src/index.ts +1 -5
- package/src/provider.ts +153 -86
- package/src/socket.ts +41 -3
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAiB,MAAM,aAAa,CAAC;AAExD,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AACnD,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAE5C,qBAAa,SAAU,SAAQ,KAAK;CAAG;AAEvC,yEAAyE;AACzE,wBAAgB,YAAY,CAC1B,GAAG,EAAE,UAAU,EACf,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,UAAU,EACvB,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,UAAU,GACnB,UAAU,CAOZ;AAuDD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAwBf"}
|
package/dist/auth.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// the JSON body. Byte arrays (nonces, proofs, ids) serialize as JSON arrays of
|
|
14
14
|
// numbers, matching serde's default for [u8; N].
|
|
15
15
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
16
|
+
import { writeBorrowed } from "./socket.js";
|
|
16
17
|
export const NONCE_LEN = 32;
|
|
17
18
|
export const PROOF_LEN = 32;
|
|
18
19
|
export const MAX_AUTH_MESSAGE_LEN = 4096;
|
|
@@ -42,8 +43,8 @@ async function writeMessage(sock, value, deadlineMs) {
|
|
|
42
43
|
}
|
|
43
44
|
const lenPrefix = new Uint8Array(4);
|
|
44
45
|
new DataView(lenPrefix.buffer).setUint32(0, json.length, true);
|
|
45
|
-
await sock
|
|
46
|
-
await sock
|
|
46
|
+
await writeBorrowed(sock, lenPrefix, deadlineMs);
|
|
47
|
+
await writeBorrowed(sock, json, deadlineMs);
|
|
47
48
|
}
|
|
48
49
|
async function readMessage(sock, deadlineMs) {
|
|
49
50
|
const lenBytes = await sock.readExact(4, deadlineMs);
|
|
@@ -59,6 +60,17 @@ async function readMessage(sock, deadlineMs) {
|
|
|
59
60
|
throw new AuthError(`auth message JSON decode failed: ${String(err)}`);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
function authBytes(value, field) {
|
|
64
|
+
if (!Array.isArray(value)) {
|
|
65
|
+
throw new AuthError(`auth field '${field}' must be a byte array`);
|
|
66
|
+
}
|
|
67
|
+
for (const byte of value) {
|
|
68
|
+
if (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255) {
|
|
69
|
+
throw new AuthError(`auth field '${field}' has invalid byte ${String(byte)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return Uint8Array.from(value);
|
|
73
|
+
}
|
|
62
74
|
/**
|
|
63
75
|
* Run the client handshake over an already-connected socket. Resolves on
|
|
64
76
|
* success; throws AuthError on any proof/identity mismatch or framing fault.
|
|
@@ -68,9 +80,9 @@ export async function authenticateClient(sock, conn, deadlineMs) {
|
|
|
68
80
|
const clientNonce = new Uint8Array(randomBytes(NONCE_LEN));
|
|
69
81
|
await writeMessage(sock, { client_nonce: Array.from(clientNonce), role: DEFAULT_CLIENT_ROLE }, deadlineMs);
|
|
70
82
|
const proof = await readMessage(sock, deadlineMs);
|
|
71
|
-
const serverNonce =
|
|
72
|
-
const daemonId =
|
|
73
|
-
const serverProof =
|
|
83
|
+
const serverNonce = authBytes(proof.server_nonce, "server_nonce");
|
|
84
|
+
const daemonId = authBytes(proof.daemon_id, "daemon_id");
|
|
85
|
+
const serverProof = authBytes(proof.server_proof, "server_proof");
|
|
74
86
|
const expected = computeProof(conn.key, SERVER_PROOF_DOMAIN, clientNonce, serverNonce, daemonId);
|
|
75
87
|
if (!constantTimeEq(expected, serverProof)) {
|
|
76
88
|
throw new AuthError("server proof mismatch — wrong key or impostor daemon");
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,8EAA8E;AAC9E,uEAAuE;AACvE,EAAE;AACF,2BAA2B;AAC3B,+CAA+C;AAC/C,iFAAiF;AACjF,2FAA2F;AAC3F,wDAAwD;AACxD,qFAAqF;AACrF,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,iDAAiD;AAEjD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,8EAA8E;AAC9E,uEAAuE;AACvE,EAAE;AACF,2BAA2B;AAC3B,+CAA+C;AAC/C,iFAAiF;AACjF,2FAA2F;AAC3F,wDAAwD;AACxD,qFAAqF;AACrF,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,iDAAiD;AAEjD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGvE,OAAO,EAAc,aAAa,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAC5B,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAC5B,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AACnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC;AAE5C,MAAM,OAAO,SAAU,SAAQ,KAAK;CAAG;AAEvC,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAC1B,GAAe,EACf,MAAc,EACd,WAAuB,EACvB,WAAuB,EACvB,QAAoB;IAEpB,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACxC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IACrC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IACrC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClC,OAAO,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,CAAa,EAAE,CAAa;IAClD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,IAAgB,EAChB,KAAc,EACd,UAAkB;IAElB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;IACxD,IAAI,IAAI,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QACvC,MAAM,IAAI,SAAS,CAAC,2BAA2B,IAAI,CAAC,MAAM,MAAM,oBAAoB,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IACpC,IAAI,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC/D,MAAM,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IACjD,MAAM,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;AAC9C,CAAC;AAED,KAAK,UAAU,WAAW,CAAI,IAAgB,EAAE,UAAkB;IAChE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACrF,IAAI,GAAG,GAAG,oBAAoB,EAAE,CAAC;QAC/B,MAAM,IAAI,SAAS,CAAC,2BAA2B,GAAG,MAAM,oBAAoB,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACnF,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAM,CAAC;IAC7D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,SAAS,CAAC,oCAAoC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;AACH,CAAC;AASD,SAAS,SAAS,CAAC,KAAc,EAAE,KAAa;IAC9C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,SAAS,CAAC,eAAe,KAAK,wBAAwB,CAAC,CAAC;IACpE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,GAAG,EAAE,CAAC;YAClF,MAAM,IAAI,SAAS,CAAC,eAAe,KAAK,sBAAsB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,KAAiB,CAAC,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAAgB,EAChB,IAAoB,EACpB,UAAkB;IAElB,MAAM,WAAW,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;IAE3D,MAAM,YAAY,CAChB,IAAI,EACJ,EAAE,YAAY,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,EACpE,UAAU,CACX,CAAC;IAEF,MAAM,KAAK,GAAG,MAAM,WAAW,CAAqB,IAAI,EAAE,UAAU,CAAC,CAAC;IACtE,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAElE,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,mBAAmB,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;IACjG,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,SAAS,CAAC,sDAAsD,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,SAAS,CAAC,mEAAmE,CAAC,CAAC;IAC3F,CAAC;IAED,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;IAClG,MAAM,YAAY,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;AAChF,CAAC"}
|
package/dist/client.d.ts
CHANGED
|
@@ -110,13 +110,14 @@ export declare class SubcCallError extends Error {
|
|
|
110
110
|
/**
|
|
111
111
|
* A live subscription to a provider's event stream, riding a single held-open
|
|
112
112
|
* request. `onEvent` fires for each StreamData frame; `closed` resolves when the
|
|
113
|
-
* provider ends the stream (StreamEnd)
|
|
114
|
-
* GOODBYE. `unsubscribe` cancels the held-open request
|
|
113
|
+
* provider ends the stream (StreamEnd) or the consumer unsubscribes, and rejects on
|
|
114
|
+
* an Error terminal or a route GOODBYE. `unsubscribe` cancels the held-open request
|
|
115
|
+
* so the provider unwinds.
|
|
115
116
|
*/
|
|
116
117
|
export interface Subscription {
|
|
117
118
|
/** Cancel the subscription: sends Cancel on the held-open request; idempotent. */
|
|
118
119
|
unsubscribe(): void;
|
|
119
|
-
/** Resolves on StreamEnd; rejects on an Error terminal or route close. */
|
|
120
|
+
/** Resolves on StreamEnd or local unsubscribe; rejects on an Error terminal or route close. */
|
|
120
121
|
readonly closed: Promise<void>;
|
|
121
122
|
}
|
|
122
123
|
export declare class SubcError extends Error {
|
|
@@ -134,6 +135,14 @@ export interface ConnectOptions {
|
|
|
134
135
|
reconnectBackoff?: ReconnectBackoff;
|
|
135
136
|
/** Injectable sleep for timer-free reconnect tests. */
|
|
136
137
|
sleep?: (ms: number) => Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Window the post-deadline liveness probe waits for ANY inbound frame after
|
|
140
|
+
* sending its channel-0 Ping before convicting the socket as half-open.
|
|
141
|
+
* Genuine event-loop starvation self-protects at any setting: a loop starved
|
|
142
|
+
* enough to delay the probe's check also delays the read loop, and when it
|
|
143
|
+
* wakes the buffered Pong dispatches first. Exposed for deterministic tests.
|
|
144
|
+
*/
|
|
145
|
+
livenessProbeWindowMs?: number;
|
|
137
146
|
/**
|
|
138
147
|
* Hard cap on the timeout-arbitration grace window (see
|
|
139
148
|
* TIMEOUT_ARBITRATION_GRACE_MS). A reply whose bytes are actively arriving when
|
|
@@ -142,6 +151,25 @@ export interface ConnectOptions {
|
|
|
142
151
|
* exposed mainly so tests can prove the arbitration deterministically.
|
|
143
152
|
*/
|
|
144
153
|
timeoutArbitrationGraceMs?: number;
|
|
154
|
+
/**
|
|
155
|
+
* Observer for daemon-originated channel-0 control pushes (`route.closing`,
|
|
156
|
+
* `route.closed`, and any op added later). Purely advisory: the daemon's
|
|
157
|
+
* load-bearing route-death signal remains the GOODBYE frame, and nothing in
|
|
158
|
+
* the client's own route lifecycle consumes these. The contract is the wire
|
|
159
|
+
* contract verbatim -- unrecognized ops still arrive here (callers ignore
|
|
160
|
+
* what they don't know, per the MUST-ignore clause), a push that fails to
|
|
161
|
+
* parse as JSON is dropped without surfacing, and an observer throw is
|
|
162
|
+
* swallowed like every other caller callback so it cannot fail the read loop
|
|
163
|
+
* or unrelated requests.
|
|
164
|
+
*/
|
|
165
|
+
onControlPush?: (push: ControlPush) => void;
|
|
166
|
+
}
|
|
167
|
+
/** A parsed daemon-originated channel-0 control push. */
|
|
168
|
+
export interface ControlPush {
|
|
169
|
+
/** The push discriminator, e.g. "route.closing" | "route.closed". */
|
|
170
|
+
op: string;
|
|
171
|
+
/** The full parsed body, `op` included, for op-specific fields. */
|
|
172
|
+
body: Record<string, unknown>;
|
|
145
173
|
}
|
|
146
174
|
export declare class SubcClient {
|
|
147
175
|
private sock;
|
|
@@ -209,6 +237,32 @@ export declare class SubcClient {
|
|
|
209
237
|
private sendManaged;
|
|
210
238
|
private cachedRouteHandle;
|
|
211
239
|
private openCachedRoute;
|
|
240
|
+
private lastInboundAtMs;
|
|
241
|
+
private livenessProbe;
|
|
242
|
+
/**
|
|
243
|
+
* Discriminate event-loop starvation from a half-open socket after a
|
|
244
|
+
* deadline-no-drop settle. The arbitration deliberately keeps the socket on
|
|
245
|
+
* that verdict (a late-read reply under load must not cost a healthy
|
|
246
|
+
* connection), but a half-open socket — peer vanished with no FIN/RST, as
|
|
247
|
+
* after host sleep/wake — yields the SAME verdict forever, so the keep
|
|
248
|
+
* becomes a permanent pin to a dead transport. The discriminator is
|
|
249
|
+
* socket-level liveness evidence: send a channel-0 Ping (the daemon always
|
|
250
|
+
* answers — subc-core control.rs) and require ANY inbound frame within the
|
|
251
|
+
* window; the Pong suffices, and a busy socket's other traffic proves the
|
|
252
|
+
* link just as well. Silence convicts: the socket is failed with a named
|
|
253
|
+
* cause and closed, so the ordinary drop path (pending rejection now,
|
|
254
|
+
* reconnect on the next call) takes over.
|
|
255
|
+
*
|
|
256
|
+
* Starvation cannot be falsely convicted by a short window: a loop starved
|
|
257
|
+
* enough to delay the read loop delays this probe's own timer identically,
|
|
258
|
+
* and when both wake the buffered Pong dispatches before the check runs.
|
|
259
|
+
*
|
|
260
|
+
* Scope: hooked from the managed-call path only. Control-plane requests
|
|
261
|
+
* (route.open, catalog.list) are short-lived and their failures already
|
|
262
|
+
* escalate through reconnect classification; the managed path is where a
|
|
263
|
+
* plugin lives for hours and where the pin was observed in production.
|
|
264
|
+
*/
|
|
265
|
+
private probeLivenessAfterDeadline;
|
|
212
266
|
private ensureConnectedForManaged;
|
|
213
267
|
private scheduleReconnectAfterDrop;
|
|
214
268
|
private reconnectAfterDrop;
|
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,EAKd,QAAQ,EAET,MAAM,eAAe,CAAC;AACvB,OAAO,EAIL,WAAW,EAGZ,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -14,7 +14,7 @@ import { AuthError, authenticateClient } from "./auth.js";
|
|
|
14
14
|
import { ConnectionFileError, readConnectionFile } from "./connection-file.js";
|
|
15
15
|
import { AdmissionClass, buildFrame, buildFlags, encodeFrame, FrameType, Priority, } from "./envelope.js";
|
|
16
16
|
import { belongsToConnection, createRouteHandle, newConnectionToken, sameRouteHandle, StaleRouteHandleError, } from "./route-handle.js";
|
|
17
|
-
import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, } from "./socket.js";
|
|
17
|
+
import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, writeBorrowed, writeTrackedBorrowed, } from "./socket.js";
|
|
18
18
|
const debug = debuglog("subc-client");
|
|
19
19
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
20
20
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
@@ -29,6 +29,7 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
29
29
|
// NOT a deadline extension: an absent reply still settles right after the check
|
|
30
30
|
// phase. Capped so it can never approach BODY_READ_TIMEOUT_MS.
|
|
31
31
|
const TIMEOUT_ARBITRATION_GRACE_MS = 50;
|
|
32
|
+
const LIVENESS_PROBE_WINDOW_MS = 2000;
|
|
32
33
|
// Internal marker set as the `code` on the SubcError a request-deadline timeout
|
|
33
34
|
// rejects with, so the managed classifier can tell a deadline (reply may simply
|
|
34
35
|
// not have been read in time) from an actual connection drop. Never surfaced to
|
|
@@ -210,11 +211,20 @@ export class SubcClient {
|
|
|
210
211
|
// double-execute anything. The cached bind is dead (module restarted and
|
|
211
212
|
// its route-gone GOODBYE raced or was missed); evict it so the retry
|
|
212
213
|
// re-opens the route instead of resending into the same dead channel.
|
|
213
|
-
|
|
214
|
+
// stale_route_epoch is the same class with a sharper cause (subconscious
|
|
215
|
+
// issue #39): the channel is known but its epoch was released while this
|
|
216
|
+
// request was in flight. The daemon's contract for the code is
|
|
217
|
+
// NOT-FORWARDED — dropped before delivery — so the retry is safe by
|
|
218
|
+
// construction, and the remedy is identical: evict, re-open, resend once.
|
|
219
|
+
const deadBindCode = err.code === "unknown_channel" || err.code === "stale_route_epoch";
|
|
220
|
+
if (deadBindCode && !retriedUnknownChannel && !this.closeStarted) {
|
|
214
221
|
retriedUnknownChannel = true;
|
|
215
222
|
this.evictRouteHandle(routeHandle);
|
|
216
223
|
continue;
|
|
217
224
|
}
|
|
225
|
+
if (deadBindCode && retriedUnknownChannel) {
|
|
226
|
+
this.evictRouteHandle(routeHandle);
|
|
227
|
+
}
|
|
218
228
|
if (err.kind === "not_sent") {
|
|
219
229
|
try {
|
|
220
230
|
await this.reconnectAfterDrop(err);
|
|
@@ -231,6 +241,15 @@ export class SubcClient {
|
|
|
231
241
|
// and its other in-flight routes for nothing.
|
|
232
242
|
this.scheduleReconnectAfterDrop(err);
|
|
233
243
|
}
|
|
244
|
+
else if (err.kind === "outcome_unknown" && err.code === DEADLINE_NO_DROP_CODE) {
|
|
245
|
+
// Keeping the socket is only correct when the socket is actually
|
|
246
|
+
// alive. A HALF-OPEN socket (peer gone with no FIN/RST — the
|
|
247
|
+
// post-sleep/wake shape) produces the identical observable, and
|
|
248
|
+
// keeping it pins every future call to a corpse: this exact keep
|
|
249
|
+
// turned one host sleep into a whole session's tool outage. The
|
|
250
|
+
// probe supplies the discriminator neither timer can.
|
|
251
|
+
this.probeLivenessAfterDeadline();
|
|
252
|
+
}
|
|
234
253
|
throw err;
|
|
235
254
|
}
|
|
236
255
|
}
|
|
@@ -243,21 +262,25 @@ export class SubcClient {
|
|
|
243
262
|
const admission = opts.admissionClass ?? AdmissionClass.Normal;
|
|
244
263
|
const corr = this.allocateCorr();
|
|
245
264
|
const key = pendingKey(handle, corr);
|
|
265
|
+
let subscriptionPending = null;
|
|
266
|
+
let resolveClosed = null;
|
|
246
267
|
const closed = new Promise((resolve, reject) => {
|
|
247
268
|
if (this.closedErr) {
|
|
248
269
|
reject(this.closedErr);
|
|
249
270
|
return;
|
|
250
271
|
}
|
|
251
|
-
|
|
272
|
+
resolveClosed = resolve;
|
|
273
|
+
subscriptionPending = {
|
|
252
274
|
handle,
|
|
253
275
|
resolve: () => resolve(),
|
|
254
276
|
reject,
|
|
255
277
|
onProgress: onEvent,
|
|
256
278
|
timer: null,
|
|
257
279
|
subscription: true,
|
|
258
|
-
}
|
|
280
|
+
};
|
|
281
|
+
this.pending.set(key, subscriptionPending);
|
|
259
282
|
const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, bytes);
|
|
260
|
-
this.sock
|
|
283
|
+
writeBorrowed(this.sock, encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
|
|
261
284
|
const pending = this.pending.get(key);
|
|
262
285
|
if (pending)
|
|
263
286
|
this.rejectPending(key, pending, err instanceof Error ? err : new SubcError(String(err)));
|
|
@@ -268,7 +291,10 @@ export class SubcClient {
|
|
|
268
291
|
if (cancelled)
|
|
269
292
|
return;
|
|
270
293
|
cancelled = true;
|
|
271
|
-
|
|
294
|
+
if (subscriptionPending && resolveClosed)
|
|
295
|
+
this.settle(key, subscriptionPending, resolveClosed);
|
|
296
|
+
if (this.isLiveHandle(handle))
|
|
297
|
+
this.cancel(handle, corr, priority);
|
|
272
298
|
};
|
|
273
299
|
return { unsubscribe, closed };
|
|
274
300
|
}
|
|
@@ -276,7 +302,7 @@ export class SubcClient {
|
|
|
276
302
|
cancel(handle, corr, priority = Priority.Interactive) {
|
|
277
303
|
this.assertLiveHandle(handle);
|
|
278
304
|
const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), handle.channel, handle.epoch, corr, EMPTY_BODY);
|
|
279
|
-
this.sock
|
|
305
|
+
writeBorrowed(this.sock, encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => undefined);
|
|
280
306
|
}
|
|
281
307
|
/** Poll status or liveness for exactly the supplied route generation. */
|
|
282
308
|
async routePoll(handle, kind) {
|
|
@@ -357,7 +383,7 @@ export class SubcClient {
|
|
|
357
383
|
return;
|
|
358
384
|
}
|
|
359
385
|
const goodbye = buildFrame(FrameType.Goodbye, buildFlags(false, Priority.Interactive, false), handle.channel, handle.epoch, 0n, EMPTY_BODY);
|
|
360
|
-
const write = this.sock
|
|
386
|
+
const write = writeTrackedBorrowed(this.sock, encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
|
|
361
387
|
if (!write.queued && closeOnQueueFailure)
|
|
362
388
|
this.closeConnectionAfterCleanupFailure();
|
|
363
389
|
write.completed.catch(() => {
|
|
@@ -411,7 +437,7 @@ export class SubcClient {
|
|
|
411
437
|
};
|
|
412
438
|
pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
|
|
413
439
|
this.pending.set(key, pending);
|
|
414
|
-
this.sock
|
|
440
|
+
writeBorrowed(this.sock, encodeFrame(frame), Date.now() + ms).catch((error) => {
|
|
415
441
|
const current = this.pending.get(key);
|
|
416
442
|
if (current)
|
|
417
443
|
this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
|
|
@@ -506,7 +532,7 @@ export class SubcClient {
|
|
|
506
532
|
};
|
|
507
533
|
pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
|
|
508
534
|
this.pending.set(key, pending);
|
|
509
|
-
const write = this.sock
|
|
535
|
+
const write = writeTrackedBorrowed(this.sock, encodeFrame(frame), Date.now() + ms);
|
|
510
536
|
handedToSocket = write.queued;
|
|
511
537
|
write.completed.catch((error) => {
|
|
512
538
|
const current = this.pending.get(key);
|
|
@@ -597,6 +623,64 @@ export class SubcClient {
|
|
|
597
623
|
}
|
|
598
624
|
}
|
|
599
625
|
}
|
|
626
|
+
// Stamped by dispatch() on every inbound frame; read by the liveness probe.
|
|
627
|
+
lastInboundAtMs = 0;
|
|
628
|
+
// Single-flight guard: overlapping deadline-no-drop settles share one probe.
|
|
629
|
+
livenessProbe = null;
|
|
630
|
+
/**
|
|
631
|
+
* Discriminate event-loop starvation from a half-open socket after a
|
|
632
|
+
* deadline-no-drop settle. The arbitration deliberately keeps the socket on
|
|
633
|
+
* that verdict (a late-read reply under load must not cost a healthy
|
|
634
|
+
* connection), but a half-open socket — peer vanished with no FIN/RST, as
|
|
635
|
+
* after host sleep/wake — yields the SAME verdict forever, so the keep
|
|
636
|
+
* becomes a permanent pin to a dead transport. The discriminator is
|
|
637
|
+
* socket-level liveness evidence: send a channel-0 Ping (the daemon always
|
|
638
|
+
* answers — subc-core control.rs) and require ANY inbound frame within the
|
|
639
|
+
* window; the Pong suffices, and a busy socket's other traffic proves the
|
|
640
|
+
* link just as well. Silence convicts: the socket is failed with a named
|
|
641
|
+
* cause and closed, so the ordinary drop path (pending rejection now,
|
|
642
|
+
* reconnect on the next call) takes over.
|
|
643
|
+
*
|
|
644
|
+
* Starvation cannot be falsely convicted by a short window: a loop starved
|
|
645
|
+
* enough to delay the read loop delays this probe's own timer identically,
|
|
646
|
+
* and when both wake the buffered Pong dispatches before the check runs.
|
|
647
|
+
*
|
|
648
|
+
* Scope: hooked from the managed-call path only. Control-plane requests
|
|
649
|
+
* (route.open, catalog.list) are short-lived and their failures already
|
|
650
|
+
* escalate through reconnect classification; the managed path is where a
|
|
651
|
+
* plugin lives for hours and where the pin was observed in production.
|
|
652
|
+
*/
|
|
653
|
+
probeLivenessAfterDeadline() {
|
|
654
|
+
if (this.livenessProbe || this.closeStarted || this.closedErr)
|
|
655
|
+
return;
|
|
656
|
+
const sock = this.sock;
|
|
657
|
+
const generation = this.generation;
|
|
658
|
+
let corr;
|
|
659
|
+
try {
|
|
660
|
+
corr = this.allocateCorr();
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return; // allocator exhausted: the next reconnect resets it
|
|
664
|
+
}
|
|
665
|
+
const t0 = Date.now();
|
|
666
|
+
const ping = buildFrame(FrameType.Ping, buildFlags(false, Priority.Interactive, false, AdmissionClass.Normal), 0, 0, corr, new Uint8Array());
|
|
667
|
+
const probe = (async () => {
|
|
668
|
+
// A failed WRITE is not exonerating: swallow it and let the window
|
|
669
|
+
// check run — a socket that cannot carry a Ping will not produce an
|
|
670
|
+
// inbound frame either, and conviction is the right verdict.
|
|
671
|
+
await writeBorrowed(sock, encodeFrame(ping), t0 + this.opts.livenessProbeWindowMs).catch(() => { });
|
|
672
|
+
await this.opts.sleep(this.opts.livenessProbeWindowMs);
|
|
673
|
+
if (this.sock !== sock || this.generation !== generation || this.closeStarted)
|
|
674
|
+
return;
|
|
675
|
+
if (this.lastInboundAtMs >= t0)
|
|
676
|
+
return; // link proven — keep the socket
|
|
677
|
+
this.fail(new SocketClosedError(`liveness probe convicted a half-open socket: no inbound frame for ${this.opts.livenessProbeWindowMs}ms after a channel-0 Ping (deadline-no-drop settles preceded this); closing so the next call reconnects`));
|
|
678
|
+
sock.close();
|
|
679
|
+
})().finally(() => {
|
|
680
|
+
this.livenessProbe = null;
|
|
681
|
+
});
|
|
682
|
+
this.livenessProbe = probe;
|
|
683
|
+
}
|
|
600
684
|
async ensureConnectedForManaged() {
|
|
601
685
|
if (this.closeStarted)
|
|
602
686
|
throw new SubcError("client closed");
|
|
@@ -726,6 +810,40 @@ export class SubcClient {
|
|
|
726
810
|
}
|
|
727
811
|
}
|
|
728
812
|
dispatch(frame) {
|
|
813
|
+
// Every dispatched frame proves the socket delivers inbound bytes; the
|
|
814
|
+
// post-deadline liveness probe reads this stamp. Stamped after readLoop's
|
|
815
|
+
// generation gate, so a stale socket's late frames never vouch for the
|
|
816
|
+
// live one.
|
|
817
|
+
this.lastInboundAtMs = Date.now();
|
|
818
|
+
if (frame.header.channel === 0 && frame.header.ty === FrameType.Push) {
|
|
819
|
+
// Daemon-originated control push (route.closing / route.closed / future
|
|
820
|
+
// ops). Never matches a pending (corr is daemon-chosen), never an error
|
|
821
|
+
// path: unparseable bodies and absent observers both drop silently per
|
|
822
|
+
// the wire contract's MUST-ignore clause, and an observer throw is
|
|
823
|
+
// swallowed for the same reason as onProgress below -- a caller callback
|
|
824
|
+
// must not fail the read loop.
|
|
825
|
+
const observer = this.opts.onControlPush;
|
|
826
|
+
if (observer) {
|
|
827
|
+
let parsed = null;
|
|
828
|
+
try {
|
|
829
|
+
const body = this.parseJson(frame);
|
|
830
|
+
if (body && typeof body.op === "string")
|
|
831
|
+
parsed = { op: body.op, body };
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
// Unparseable control push: ignored by contract.
|
|
835
|
+
}
|
|
836
|
+
if (parsed) {
|
|
837
|
+
try {
|
|
838
|
+
observer(parsed);
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// Observer's own throw, on its own stack; the stream must survive.
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
729
847
|
let handle = null;
|
|
730
848
|
if (frame.header.channel !== 0) {
|
|
731
849
|
handle = this.liveRoutes.get(frame.header.channel) ?? null;
|
|
@@ -742,7 +860,29 @@ export class SubcClient {
|
|
|
742
860
|
switch (frame.header.ty) {
|
|
743
861
|
case FrameType.Push:
|
|
744
862
|
case FrameType.StreamData:
|
|
745
|
-
|
|
863
|
+
// A THROW FROM CALLER CODE MUST NOT REACH THE READ LOOP.
|
|
864
|
+
//
|
|
865
|
+
// `onProgress` (and `onEvent`, which is the same slot) is supplied by
|
|
866
|
+
// the caller and runs INSIDE readLoop's frame dispatch. An escaping
|
|
867
|
+
// throw unwinds into readLoop's catch, which treats it as a socket
|
|
868
|
+
// failure: it calls fail(), rejecting EVERY in-flight request on this
|
|
869
|
+
// connection -- other routes included -- with the caller's own error as
|
|
870
|
+
// the reported cause, and stops reading. One consumer's bad event
|
|
871
|
+
// handler therefore drops the whole connection, and the surfaced error
|
|
872
|
+
// names the handler rather than the transport, so the failure reads as
|
|
873
|
+
// a subc fault.
|
|
874
|
+
//
|
|
875
|
+
// Swallowing is deliberate and is NOT hiding a fault from the caller:
|
|
876
|
+
// the throw happened in their own callback, on their own stack, where
|
|
877
|
+
// they are the party able to catch it. What the client owes them is
|
|
878
|
+
// that the stream and every sibling route keep working.
|
|
879
|
+
try {
|
|
880
|
+
pending.onProgress?.(frame.body);
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// Swallow the caller's own handler error so one bad callback cannot
|
|
884
|
+
// abort the read loop or fail unrelated requests. See above.
|
|
885
|
+
}
|
|
746
886
|
return;
|
|
747
887
|
case FrameType.Response:
|
|
748
888
|
case FrameType.StreamEnd:
|
|
@@ -762,7 +902,17 @@ export class SubcClient {
|
|
|
762
902
|
return;
|
|
763
903
|
}
|
|
764
904
|
if (frame.header.ty === FrameType.Goodbye && handle) {
|
|
765
|
-
|
|
905
|
+
// Carries `route_closed` like the other two route-close sites (closeRoute,
|
|
906
|
+
// late route.open). It was the only one without a code, so a consumer
|
|
907
|
+
// branching on `route_closed` to recognise a gone route saw this one as an
|
|
908
|
+
// uncoded generic failure and fell through to whatever its default was.
|
|
909
|
+
//
|
|
910
|
+
// THE CODE MUST NOT MAKE THIS RETRYABLE. A GOODBYE arriving mid-request
|
|
911
|
+
// means the request was already forwarded and the module may have run it,
|
|
912
|
+
// so it stays kind=outcome_unknown -- the same class as a mid-flight socket
|
|
913
|
+
// drop, and never the not_sent/unknown_channel class that call() retries.
|
|
914
|
+
// The code says WHICH route ended, not that it is safe to send again.
|
|
915
|
+
this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)", "route_closed"));
|
|
766
916
|
if (this.liveRoutes.get(handle.channel) === handle)
|
|
767
917
|
this.liveRoutes.delete(handle.channel);
|
|
768
918
|
this.evictRouteHandle(handle);
|
|
@@ -882,10 +1032,11 @@ export class SubcClient {
|
|
|
882
1032
|
this.scheduleReconnectAfterDrop(error);
|
|
883
1033
|
}
|
|
884
1034
|
encode(value) {
|
|
885
|
-
return
|
|
1035
|
+
return Buffer.from(JSON.stringify(value), "utf8");
|
|
886
1036
|
}
|
|
887
1037
|
parseJson(frame) {
|
|
888
|
-
|
|
1038
|
+
const b = frame.body;
|
|
1039
|
+
return JSON.parse(Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString("utf8"));
|
|
889
1040
|
}
|
|
890
1041
|
}
|
|
891
1042
|
export function isConsumerReconnectTransient(err) {
|
|
@@ -926,6 +1077,7 @@ export function isConsumerReconnectTransient(err) {
|
|
|
926
1077
|
export function isRetryableRouteOpenCode(code) {
|
|
927
1078
|
return (code === "unknown_module" ||
|
|
928
1079
|
code === "module_reloading" ||
|
|
1080
|
+
code === "module_warming" ||
|
|
929
1081
|
code === "target_unavailable" ||
|
|
930
1082
|
code === "module_timeout");
|
|
931
1083
|
}
|
|
@@ -947,6 +1099,8 @@ function normalizeConnectOptions(opts) {
|
|
|
947
1099
|
reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
|
|
948
1100
|
sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
949
1101
|
timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS,
|
|
1102
|
+
livenessProbeWindowMs: opts.livenessProbeWindowMs ?? LIVENESS_PROBE_WINDOW_MS,
|
|
1103
|
+
onControlPush: opts.onControlPush,
|
|
950
1104
|
};
|
|
951
1105
|
}
|
|
952
1106
|
function routeCacheKey(target, identity, consumerIdentity) {
|