@grackle-ai/ahp-transport 0.132.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.
Files changed (46) hide show
  1. package/dist/ahp-client-socket.d.ts +109 -0
  2. package/dist/ahp-client-socket.d.ts.map +1 -0
  3. package/dist/ahp-client-socket.js +364 -0
  4. package/dist/ahp-client-socket.js.map +1 -0
  5. package/dist/ahp-server-socket.d.ts +111 -0
  6. package/dist/ahp-server-socket.d.ts.map +1 -0
  7. package/dist/ahp-server-socket.js +260 -0
  8. package/dist/ahp-server-socket.js.map +1 -0
  9. package/dist/backoff.d.ts +28 -0
  10. package/dist/backoff.d.ts.map +1 -0
  11. package/dist/backoff.js +31 -0
  12. package/dist/backoff.js.map +1 -0
  13. package/dist/client-id-store.d.ts +41 -0
  14. package/dist/client-id-store.d.ts.map +1 -0
  15. package/dist/client-id-store.js +66 -0
  16. package/dist/client-id-store.js.map +1 -0
  17. package/dist/error-codes.d.ts +45 -0
  18. package/dist/error-codes.d.ts.map +1 -0
  19. package/dist/error-codes.js +32 -0
  20. package/dist/error-codes.js.map +1 -0
  21. package/dist/examples/echo-subscriber.d.ts +29 -0
  22. package/dist/examples/echo-subscriber.d.ts.map +1 -0
  23. package/dist/examples/echo-subscriber.js +102 -0
  24. package/dist/examples/echo-subscriber.js.map +1 -0
  25. package/dist/heartbeat.d.ts +67 -0
  26. package/dist/heartbeat.d.ts.map +1 -0
  27. package/dist/heartbeat.js +79 -0
  28. package/dist/heartbeat.js.map +1 -0
  29. package/dist/index.d.ts +22 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +16 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/json-rpc-session.d.ts +106 -0
  34. package/dist/json-rpc-session.d.ts.map +1 -0
  35. package/dist/json-rpc-session.js +294 -0
  36. package/dist/json-rpc-session.js.map +1 -0
  37. package/dist/mocks/fake-websocket.d.ts +53 -0
  38. package/dist/mocks/fake-websocket.d.ts.map +1 -0
  39. package/dist/mocks/fake-websocket.js +86 -0
  40. package/dist/mocks/fake-websocket.js.map +1 -0
  41. package/dist/mocks/test-driver.d.ts +47 -0
  42. package/dist/mocks/test-driver.d.ts.map +1 -0
  43. package/dist/mocks/test-driver.js +122 -0
  44. package/dist/mocks/test-driver.js.map +1 -0
  45. package/dist/tsdoc-metadata.json +11 -0
  46. package/package.json +51 -0
@@ -0,0 +1,294 @@
1
+ /**
2
+ * JSON-RPC 2.0 framing engine over a single WebSocket. Owns:
3
+ * - outbound request/response correlation by numeric id
4
+ * - inbound dispatch to `onRequest` / `onNotification`
5
+ * - per-request timeouts
6
+ * - close-rejects-all-pending-with-ConnectionLost
7
+ *
8
+ * Channels (AHP routing) are deliberately NOT understood here — that's
9
+ * `MultiHostClient`'s job. This class only does framing.
10
+ */
11
+ import { JsonRpcErrorCodes } from "@grackle-ai/ahp";
12
+ import { TransportError, WsCloseCode } from "./error-codes.js";
13
+ /**
14
+ * Bidirectional JSON-RPC session over one WebSocket.
15
+ *
16
+ * @example Wrap an already-OPEN ws connection and exchange a typed request:
17
+ * ```ts
18
+ * import { WebSocket } from "ws";
19
+ * const socket = new WebSocket(url);
20
+ * await new Promise((r) => socket.once("open", r));
21
+ *
22
+ * const session = new JsonRpcSession({
23
+ * socket,
24
+ * onNotification: (n) => console.log("inbound:", n.method),
25
+ * });
26
+ * const result = await session.request("ping", { channel: "ahp-root://" });
27
+ * ```
28
+ *
29
+ * @example Send a response that closes the session after the frame flushes:
30
+ * ```ts
31
+ * const session = new JsonRpcSession({
32
+ * socket,
33
+ * onRequest: async (req) => ({
34
+ * response: { jsonrpc: "2.0", id: req.id, error: { code: -32600, message: "bad" } },
35
+ * afterSend: () => session.close(1000, "bye"),
36
+ * }),
37
+ * });
38
+ * ```
39
+ */
40
+ export class JsonRpcSession {
41
+ socket;
42
+ onRequest;
43
+ onNotification;
44
+ onClose;
45
+ requestTimeoutMs;
46
+ nextRequestId = 1;
47
+ pendingRequests = new Map();
48
+ closed = false;
49
+ constructor(opts) {
50
+ this.socket = opts.socket;
51
+ this.onRequest = opts.onRequest;
52
+ this.onNotification = opts.onNotification;
53
+ this.onClose = opts.onClose;
54
+ this.requestTimeoutMs = opts.requestTimeoutMs;
55
+ this.socket.on("message", this.handleMessage);
56
+ this.socket.on("close", this.handleClose);
57
+ this.socket.on("error", this.handleError);
58
+ }
59
+ /** Sends a request and resolves with the result (or rejects with the error). */
60
+ request(method, params) {
61
+ if (this.closed) {
62
+ return Promise.reject(new TransportError("connection-lost", `request ${String(method)} after close`));
63
+ }
64
+ const id = this.nextRequestId++;
65
+ return new Promise((resolve, reject) => {
66
+ const entry = {
67
+ method: String(method),
68
+ resolve: resolve,
69
+ reject,
70
+ timeoutHandle: undefined,
71
+ };
72
+ if (this.requestTimeoutMs !== undefined) {
73
+ entry.timeoutHandle = setTimeout(() => {
74
+ if (this.pendingRequests.delete(id)) {
75
+ reject(new TransportError("request-timeout", `request ${entry.method} timed out after ${this.requestTimeoutMs}ms`));
76
+ }
77
+ }, this.requestTimeoutMs);
78
+ }
79
+ this.pendingRequests.set(id, entry);
80
+ // If sending fails (e.g., socket closed mid-call), reject immediately.
81
+ try {
82
+ this.socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
83
+ }
84
+ catch (err) {
85
+ this.pendingRequests.delete(id);
86
+ if (entry.timeoutHandle !== undefined) {
87
+ clearTimeout(entry.timeoutHandle);
88
+ }
89
+ reject(new TransportError("connection-lost", `request ${entry.method} failed to send: ${err.message}`));
90
+ }
91
+ });
92
+ }
93
+ /** Sends a notification (fire-and-forget). Silently drops if already closed. */
94
+ notify(method, params) {
95
+ if (this.closed) {
96
+ return;
97
+ }
98
+ try {
99
+ this.socket.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
100
+ }
101
+ catch {
102
+ // Socket closed between the check and send; drop silently.
103
+ }
104
+ }
105
+ /** Closes the underlying socket. Idempotent. */
106
+ close(code, reason) {
107
+ if (this.closed) {
108
+ return;
109
+ }
110
+ this.socket.close(code, reason);
111
+ }
112
+ /** True between construction and the socket's "close" event firing. */
113
+ get isOpen() {
114
+ return !this.closed;
115
+ }
116
+ // ─── Inbound message dispatch ──────────────────────────────────────
117
+ handleMessage = (data, isBinary) => {
118
+ if (isBinary) {
119
+ this.socket.close(WsCloseCode.UnsupportedData, "binary frames not supported");
120
+ return;
121
+ }
122
+ const text = typeof data === "string"
123
+ ? data
124
+ : Buffer.isBuffer(data)
125
+ ? data.toString("utf8")
126
+ : Buffer.concat(Array.isArray(data) ? data : [Buffer.from(data)]).toString("utf8");
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(text);
130
+ }
131
+ catch {
132
+ // Malformed JSON: if it looked like a request with an id, we can't
133
+ // recover the id, so just drop. JSON-RPC spec says respond with
134
+ // ParseError + id=null, but a null id is rarely actionable.
135
+ this.tryWriteParseError();
136
+ return;
137
+ }
138
+ if (!isObject(parsed) || parsed["jsonrpc"] !== "2.0") {
139
+ // Not a JSON-RPC envelope; drop.
140
+ return;
141
+ }
142
+ const idValue = parsed["id"];
143
+ const hasId = typeof idValue === "number";
144
+ if (hasId) {
145
+ if ("result" in parsed) {
146
+ this.resolvePending(idValue, parsed["result"]);
147
+ return;
148
+ }
149
+ if ("error" in parsed) {
150
+ this.rejectPending(idValue, parsed["error"]);
151
+ return;
152
+ }
153
+ if (typeof parsed["method"] === "string") {
154
+ void this.handleInboundRequest(parsed);
155
+ return;
156
+ }
157
+ // Has an id but neither result/error/method: invalid.
158
+ this.writeError(idValue, JsonRpcErrorCodes.InvalidRequest, "malformed envelope");
159
+ return;
160
+ }
161
+ if (typeof parsed["method"] === "string") {
162
+ this.handleInboundNotification(parsed);
163
+ return;
164
+ }
165
+ // No id and no method: drop silently.
166
+ };
167
+ resolvePending(id, result) {
168
+ const entry = this.pendingRequests.get(id);
169
+ if (entry === undefined) {
170
+ return;
171
+ }
172
+ this.pendingRequests.delete(id);
173
+ if (entry.timeoutHandle !== undefined) {
174
+ clearTimeout(entry.timeoutHandle);
175
+ }
176
+ entry.resolve(result);
177
+ }
178
+ rejectPending(id, error) {
179
+ const entry = this.pendingRequests.get(id);
180
+ if (entry === undefined) {
181
+ return;
182
+ }
183
+ this.pendingRequests.delete(id);
184
+ if (entry.timeoutHandle !== undefined) {
185
+ clearTimeout(entry.timeoutHandle);
186
+ }
187
+ entry.reject(error);
188
+ }
189
+ async handleInboundRequest(req) {
190
+ if (this.onRequest === undefined) {
191
+ this.writeError(req.id, JsonRpcErrorCodes.MethodNotFound, `no handler for ${req.method}`);
192
+ return;
193
+ }
194
+ try {
195
+ const result = await this.onRequest(req);
196
+ const isWrapped = isObject(result) && "response" in result && "afterSend" in result;
197
+ if (isWrapped) {
198
+ const { response, afterSend } = result;
199
+ this.sendAndThen(response, afterSend);
200
+ }
201
+ else {
202
+ this.socket.send(JSON.stringify(result));
203
+ }
204
+ }
205
+ catch (err) {
206
+ this.writeError(req.id, JsonRpcErrorCodes.InternalError, err.message || "handler threw");
207
+ }
208
+ }
209
+ /**
210
+ * Sends `frame` and invokes `after` once the data has been flushed to the
211
+ * OS socket buffer. Use this when a side effect (e.g., closing the session)
212
+ * MUST follow the frame on the wire.
213
+ *
214
+ * `after` runs even if `socket.send` errors — the typical close-after-error
215
+ * caller wants the close to proceed regardless.
216
+ */
217
+ sendAndThen(frame, after) {
218
+ if (this.closed) {
219
+ // Fire `after` immediately if the socket is already gone; otherwise the
220
+ // callback would leak.
221
+ after();
222
+ return;
223
+ }
224
+ try {
225
+ this.socket.send(JSON.stringify(frame), (err) => {
226
+ // The callback fires once the data is flushed to the kernel buffer
227
+ // (or immediately with an error if the socket is no longer writable).
228
+ // We always run `after` regardless.
229
+ void err;
230
+ after();
231
+ });
232
+ }
233
+ catch {
234
+ // Synchronous send failure (socket already closed mid-call). Run after.
235
+ after();
236
+ }
237
+ }
238
+ handleInboundNotification(notif) {
239
+ if (this.onNotification === undefined) {
240
+ return;
241
+ }
242
+ try {
243
+ this.onNotification(notif);
244
+ }
245
+ catch {
246
+ // Notification handler errors are swallowed to keep the session alive.
247
+ }
248
+ }
249
+ writeError(id, code, message) {
250
+ try {
251
+ this.socket.send(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }));
252
+ }
253
+ catch {
254
+ // Socket likely closing; ignore.
255
+ }
256
+ }
257
+ tryWriteParseError() {
258
+ try {
259
+ this.socket.send(JSON.stringify({
260
+ jsonrpc: "2.0",
261
+ id: null,
262
+ error: { code: JsonRpcErrorCodes.ParseError, message: "parse error" },
263
+ }));
264
+ }
265
+ catch {
266
+ // Ignore.
267
+ }
268
+ }
269
+ // ─── Lifecycle ─────────────────────────────────────────────────────
270
+ handleClose = (code, reason) => {
271
+ if (this.closed) {
272
+ return;
273
+ }
274
+ this.closed = true;
275
+ const reasonStr = reason.toString("utf8");
276
+ for (const [, entry] of this.pendingRequests) {
277
+ if (entry.timeoutHandle !== undefined) {
278
+ clearTimeout(entry.timeoutHandle);
279
+ }
280
+ entry.reject(new TransportError("connection-lost", `connection lost while ${entry.method} pending (code=${code})`));
281
+ }
282
+ this.pendingRequests.clear();
283
+ this.onClose?.(code, reasonStr);
284
+ };
285
+ handleError = (_err) => {
286
+ // ws emits "error" before "close" on transport failures. The "close"
287
+ // handler does the work; this listener exists so the EventEmitter doesn't
288
+ // crash the process on unhandled "error" events.
289
+ };
290
+ }
291
+ function isObject(v) {
292
+ return typeof v === "object" && v !== null && !Array.isArray(v);
293
+ }
294
+ //# sourceMappingURL=json-rpc-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-rpc-session.js","sourceRoot":"","sources":["../src/json-rpc-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * EventEmitter-shaped WebSocket double for unit tests. Implements just the
3
+ * subset of `ws.WebSocket` that `JsonRpcSession` and `AhpClientSocket`
4
+ * actually call, plus test-only helpers (`open`, `receive`, `receivePong`,
5
+ * `remoteClose`) for driving inbound events.
6
+ *
7
+ * Tests pass instances to consumers via `as unknown as WebSocket` casts.
8
+ * Excluded from coverage by the rig's `src/**\/mocks/**` glob.
9
+ */
10
+ import { EventEmitter } from "node:events";
11
+ /** Mirrors `ws.WebSocket` `readyState` constants. */
12
+ export declare const FakeReadyState: {
13
+ readonly Connecting: 0;
14
+ readonly Open: 1;
15
+ readonly Closing: 2;
16
+ readonly Closed: 3;
17
+ };
18
+ export type FakeReadyState = (typeof FakeReadyState)[keyof typeof FakeReadyState];
19
+ /** Test-only WebSocket double. */
20
+ export declare class FakeWebSocket extends EventEmitter {
21
+ readyState: FakeReadyState;
22
+ /** Frames the SUT sent. UTF-8 strings appear as strings; binary frames as Buffer. */
23
+ readonly sent: Array<string | Buffer>;
24
+ /** Counts of `ping()` calls made by the SUT. */
25
+ pingCount: number;
26
+ /** Captured close calls from the SUT. */
27
+ closedBy: {
28
+ code?: number;
29
+ reason?: string;
30
+ } | undefined;
31
+ /**
32
+ * Mirrors `ws.WebSocket.send(data[, cb])`. The callback (when provided) is
33
+ * invoked on the next microtask with `undefined` as the error parameter,
34
+ * matching ws's "flushed cleanly" semantics. Tests can override the
35
+ * delivery timing by stubbing this method.
36
+ */
37
+ send(data: string | Buffer, cb?: (err?: Error) => void): void;
38
+ close(code?: number, reason?: string): void;
39
+ ping(): void;
40
+ /** Transitions to OPEN and fires the "open" event. */
41
+ open(): void;
42
+ /** Fires a "message" event with a text frame. */
43
+ receive(text: string): void;
44
+ /** Fires a "message" event with a binary frame. */
45
+ receiveBinary(data: Buffer): void;
46
+ /** Fires a "pong" event. */
47
+ receivePong(): void;
48
+ /** Simulates a remote close: fires "close" without going through close(). */
49
+ remoteClose(code: number, reason?: string): void;
50
+ /** Simulates a transport error. */
51
+ emitError(err: Error): void;
52
+ }
53
+ //# sourceMappingURL=fake-websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-websocket.d.ts","sourceRoot":"","sources":["../../src/mocks/fake-websocket.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,qDAAqD;AACrD,eAAO,MAAM,cAAc;;;;;CAKjB,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,OAAO,cAAc,CAAC,CAAC;AAElF,kCAAkC;AAClC,qBAAa,aAAc,SAAQ,YAAY;IACtC,UAAU,EAAE,cAAc,CAA6B;IAC9D,qFAAqF;IACrF,SAAgB,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAM;IAClD,gDAAgD;IACzC,SAAS,SAAK;IACrB,yCAAyC;IAClC,QAAQ,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAIhE;;;;;OAKG;IACI,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI;IAU7D,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAa3C,IAAI,IAAI,IAAI;IAMnB,sDAAsD;IAC/C,IAAI,IAAI,IAAI;IAKnB,iDAAiD;IAC1C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIlC,mDAAmD;IAC5C,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIxC,4BAA4B;IACrB,WAAW,IAAI,IAAI;IAI1B,6EAA6E;IACtE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,IAAI;IAKnD,mCAAmC;IAC5B,SAAS,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI;CAGnC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * EventEmitter-shaped WebSocket double for unit tests. Implements just the
3
+ * subset of `ws.WebSocket` that `JsonRpcSession` and `AhpClientSocket`
4
+ * actually call, plus test-only helpers (`open`, `receive`, `receivePong`,
5
+ * `remoteClose`) for driving inbound events.
6
+ *
7
+ * Tests pass instances to consumers via `as unknown as WebSocket` casts.
8
+ * Excluded from coverage by the rig's `src/**\/mocks/**` glob.
9
+ */
10
+ import { EventEmitter } from "node:events";
11
+ /** Mirrors `ws.WebSocket` `readyState` constants. */
12
+ export const FakeReadyState = {
13
+ Connecting: 0,
14
+ Open: 1,
15
+ Closing: 2,
16
+ Closed: 3,
17
+ };
18
+ /** Test-only WebSocket double. */
19
+ export class FakeWebSocket extends EventEmitter {
20
+ readyState = FakeReadyState.Connecting;
21
+ /** Frames the SUT sent. UTF-8 strings appear as strings; binary frames as Buffer. */
22
+ sent = [];
23
+ /** Counts of `ping()` calls made by the SUT. */
24
+ pingCount = 0;
25
+ /** Captured close calls from the SUT. */
26
+ closedBy;
27
+ // ─── ws.WebSocket surface used by JsonRpcSession ───────────────────
28
+ /**
29
+ * Mirrors `ws.WebSocket.send(data[, cb])`. The callback (when provided) is
30
+ * invoked on the next microtask with `undefined` as the error parameter,
31
+ * matching ws's "flushed cleanly" semantics. Tests can override the
32
+ * delivery timing by stubbing this method.
33
+ */
34
+ send(data, cb) {
35
+ if (this.readyState !== FakeReadyState.Open) {
36
+ throw new Error(`FakeWebSocket: send() called in readyState=${this.readyState}`);
37
+ }
38
+ this.sent.push(data);
39
+ if (cb !== undefined) {
40
+ queueMicrotask(() => cb());
41
+ }
42
+ }
43
+ close(code, reason) {
44
+ if (this.readyState === FakeReadyState.Closed) {
45
+ return;
46
+ }
47
+ this.closedBy = { code, reason };
48
+ this.readyState = FakeReadyState.Closing;
49
+ // Synthesize the close event asynchronously, matching ws behavior.
50
+ queueMicrotask(() => {
51
+ this.readyState = FakeReadyState.Closed;
52
+ this.emit("close", code ?? 1000, Buffer.from(reason ?? ""));
53
+ });
54
+ }
55
+ ping() {
56
+ this.pingCount += 1;
57
+ }
58
+ // ─── Test-only inbound drivers ─────────────────────────────────────
59
+ /** Transitions to OPEN and fires the "open" event. */
60
+ open() {
61
+ this.readyState = FakeReadyState.Open;
62
+ this.emit("open");
63
+ }
64
+ /** Fires a "message" event with a text frame. */
65
+ receive(text) {
66
+ this.emit("message", Buffer.from(text, "utf8"), false);
67
+ }
68
+ /** Fires a "message" event with a binary frame. */
69
+ receiveBinary(data) {
70
+ this.emit("message", data, true);
71
+ }
72
+ /** Fires a "pong" event. */
73
+ receivePong() {
74
+ this.emit("pong", Buffer.alloc(0));
75
+ }
76
+ /** Simulates a remote close: fires "close" without going through close(). */
77
+ remoteClose(code, reason = "") {
78
+ this.readyState = FakeReadyState.Closed;
79
+ this.emit("close", code, Buffer.from(reason));
80
+ }
81
+ /** Simulates a transport error. */
82
+ emitError(err) {
83
+ this.emit("error", err);
84
+ }
85
+ }
86
+ //# sourceMappingURL=fake-websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-websocket.js","sourceRoot":"","sources":["../../src/mocks/fake-websocket.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,qDAAqD;AACrD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,UAAU,EAAE,CAAC;IACb,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;CACD,CAAC;AAIX,kCAAkC;AAClC,MAAM,OAAO,aAAc,SAAQ,YAAY;IACtC,UAAU,GAAmB,cAAc,CAAC,UAAU,CAAC;IAC9D,qFAAqF;IACrE,IAAI,GAA2B,EAAE,CAAC;IAClD,gDAAgD;IACzC,SAAS,GAAG,CAAC,CAAC;IACrB,yCAAyC;IAClC,QAAQ,CAAiD;IAEhE,sEAAsE;IAEtE;;;;;OAKG;IACI,IAAI,CAAC,IAAqB,EAAE,EAA0B;QAC3D,IAAI,IAAI,CAAC,UAAU,KAAK,cAAc,CAAC,IAAI,EAAE,CAAC;YAC5C,MAAM,IAAI,KAAK,CAAC,8CAA8C,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,cAAc,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,IAAa,EAAE,MAAe;QACzC,IAAI,IAAI,CAAC,UAAU,KAAK,cAAc,CAAC,MAAM,EAAE,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QACjC,IAAI,CAAC,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC;QACzC,mEAAmE;QACnE,cAAc,CAAC,GAAG,EAAE;YAClB,IAAI,CAAC,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC;YACxC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;IACL,CAAC;IAEM,IAAI;QACT,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;IACtB,CAAC;IAED,sEAAsE;IAEtE,sDAAsD;IAC/C,IAAI;QACT,IAAI,CAAC,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpB,CAAC;IAED,iDAAiD;IAC1C,OAAO,CAAC,IAAY;QACzB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;IACzD,CAAC;IAED,mDAAmD;IAC5C,aAAa,CAAC,IAAY;QAC/B,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,4BAA4B;IACrB,WAAW;QAChB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,6EAA6E;IACtE,WAAW,CAAC,IAAY,EAAE,MAAM,GAAG,EAAE;QAC1C,IAAI,CAAC,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC;QACxC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,mCAAmC;IAC5B,SAAS,CAAC,GAAU;QACzB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Test harness that lets a property-based fuzzer (or any test) drive an
3
+ * `AhpServerSocket` + `AhpClientSocket` pair through arbitrary sequences
4
+ * of operations and observe the resulting state. Excluded from coverage.
5
+ */
6
+ import { AhpClientSocket, type AhpConnectionState } from "../ahp-client-socket.js";
7
+ import { type AhpServerConnection, type AhpServerSocketOptions } from "../ahp-server-socket.js";
8
+ /** Records every state transition for invariant checks. */
9
+ export interface StateTransition {
10
+ readonly at: number;
11
+ readonly state: AhpConnectionState;
12
+ }
13
+ /** Per-driver state used by tests/fuzzers. */
14
+ export interface TestDriver {
15
+ readonly client: AhpClientSocket;
16
+ /** Set after the server has been booted. Cleared on serverDown. */
17
+ serverConnections: AhpServerConnection[];
18
+ /** Full history of client state transitions. */
19
+ readonly transitions: StateTransition[];
20
+ /** Settle-history for every request the driver issued. */
21
+ readonly requestOutcomes: Array<"resolved" | "rejected">;
22
+ /**
23
+ * Set if any request promise settled MORE than once. The fuzzer asserts
24
+ * this stays empty as invariant I3 (exactly-once settle).
25
+ */
26
+ readonly doubleSettleErrors: string[];
27
+ /** Start (or restart) the server on the original port. */
28
+ startServer(opts?: Partial<AhpServerSocketOptions>): Promise<void>;
29
+ /** Stop the server. Existing client connections drop. */
30
+ stopServer(): Promise<void>;
31
+ /** Kick the most-recently-established server session. No-op if no session. */
32
+ kickSession(): void;
33
+ /** Issue a typed request through the client. Tracks the settle. */
34
+ request(): Promise<void>;
35
+ /** Send a notification through the client. */
36
+ notify(): void;
37
+ /** Sleep `ms` of real time. */
38
+ wait(ms: number): Promise<void>;
39
+ /** Close + dispose the driver. */
40
+ dispose(): Promise<void>;
41
+ }
42
+ export interface TestDriverOptions {
43
+ /** Backoff for the client. Defaults to tight 5ms reconnects for fuzz speed. */
44
+ readonly backoffMs?: number;
45
+ }
46
+ export declare function createTestDriver(options?: TestDriverOptions): Promise<TestDriver>;
47
+ //# sourceMappingURL=test-driver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-driver.d.ts","sourceRoot":"","sources":["../../src/mocks/test-driver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,eAAe,EAAE,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGnF,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC5B,MAAM,yBAAyB,CAAC;AAQjC,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;CACpC;AAED,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,mEAAmE;IACnE,iBAAiB,EAAE,mBAAmB,EAAE,CAAC;IACzC,gDAAgD;IAChD,QAAQ,CAAC,WAAW,EAAE,eAAe,EAAE,CAAC;IACxC,0DAA0D;IAC1D,QAAQ,CAAC,eAAe,EAAE,KAAK,CAAC,UAAU,GAAG,UAAU,CAAC,CAAC;IACzD;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAEtC,0DAA0D;IAC1D,WAAW,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,yDAAyD;IACzD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,8EAA8E;IAC9E,WAAW,IAAI,IAAI,CAAC;IACpB,mEAAmE;IACnE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,8CAA8C;IAC9C,MAAM,IAAI,IAAI,CAAC;IACf,+BAA+B;IAC/B,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,kCAAkC;IAClC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CA2G3F"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Test harness that lets a property-based fuzzer (or any test) drive an
3
+ * `AhpServerSocket` + `AhpClientSocket` pair through arbitrary sequences
4
+ * of operations and observe the resulting state. Excluded from coverage.
5
+ */
6
+ import { createServer } from "node:http";
7
+ import { AhpClientSocket } from "../ahp-client-socket.js";
8
+ import { exponentialBackoff } from "../backoff.js";
9
+ import { InMemoryClientIdStore } from "../client-id-store.js";
10
+ import { AhpServerSocket, } from "../ahp-server-socket.js";
11
+ const INIT_RESULT = {
12
+ protocolVersion: "0.1.0",
13
+ serverSeq: 0,
14
+ snapshots: [],
15
+ };
16
+ export async function createTestDriver(options = {}) {
17
+ const backoffMs = options.backoffMs ?? 5;
18
+ // Grab a free port via a brief listen-then-close. There's a small
19
+ // theoretical race against other vitest workers that could grab the
20
+ // same port between close and the next startServer, but in practice
21
+ // operating systems hold recently-freed ports in TIME_WAIT long enough
22
+ // for the test to re-bind. We tolerate the rare flake rather than pay
23
+ // the complexity of port-keeping with a fallback upgrade handler.
24
+ const initialServer = createServer();
25
+ await new Promise((r) => initialServer.listen(0, "127.0.0.1", r));
26
+ const port = initialServer.address().port;
27
+ await new Promise((r) => initialServer.close(() => r()));
28
+ let server;
29
+ let ahp;
30
+ const serverConnections = [];
31
+ const transitions = [];
32
+ const requestOutcomes = [];
33
+ const doubleSettleErrors = [];
34
+ let startedAt = Date.now();
35
+ let requestSeq = 0;
36
+ const client = new AhpClientSocket({
37
+ url: `ws://127.0.0.1:${port}/ahp`,
38
+ powerlineToken: "tok",
39
+ clientIdStore: new InMemoryClientIdStore(),
40
+ clientIdKey: "fuzz",
41
+ backoff: exponentialBackoff({ initialMs: backoffMs, maxMs: backoffMs, jitter: 0 }),
42
+ onStateChange: (s) => transitions.push({ at: Date.now() - startedAt, state: s }),
43
+ });
44
+ const driver = {
45
+ client,
46
+ serverConnections,
47
+ transitions,
48
+ requestOutcomes,
49
+ doubleSettleErrors,
50
+ async startServer(opts) {
51
+ if (server !== undefined) {
52
+ return;
53
+ }
54
+ server = createServer();
55
+ await new Promise((r) => server.listen(port, "127.0.0.1", r));
56
+ ahp = new AhpServerSocket({
57
+ server,
58
+ powerlineToken: "tok",
59
+ onInitialize: () => INIT_RESULT,
60
+ onConnection: (c) => serverConnections.push(c),
61
+ onRequest: async (req) => ({
62
+ jsonrpc: "2.0",
63
+ id: req.id,
64
+ result: null,
65
+ }),
66
+ ...opts,
67
+ });
68
+ },
69
+ async stopServer() {
70
+ if (server === undefined) {
71
+ return;
72
+ }
73
+ await ahp.close();
74
+ await new Promise((r) => server.close(() => r()));
75
+ server = undefined;
76
+ ahp = undefined;
77
+ serverConnections.length = 0;
78
+ },
79
+ kickSession() {
80
+ const last = serverConnections[serverConnections.length - 1];
81
+ if (last !== undefined) {
82
+ last.session.close(1011, "kicked");
83
+ }
84
+ },
85
+ async request() {
86
+ const id = requestSeq++;
87
+ let settled = false;
88
+ const recordSettle = (outcome) => {
89
+ if (settled) {
90
+ doubleSettleErrors.push(`request ${id} settled twice (last: ${outcome})`);
91
+ return;
92
+ }
93
+ settled = true;
94
+ requestOutcomes.push(outcome);
95
+ };
96
+ try {
97
+ await client.request("ping", { channel: "ahp-root://" });
98
+ recordSettle("resolved");
99
+ }
100
+ catch {
101
+ recordSettle("rejected");
102
+ }
103
+ },
104
+ notify() {
105
+ try {
106
+ client.notify("unsubscribe", { channel: "ahp-session:/x" });
107
+ }
108
+ catch {
109
+ // notify is fire-and-forget; swallow
110
+ }
111
+ },
112
+ async wait(ms) {
113
+ await new Promise((r) => setTimeout(r, ms));
114
+ },
115
+ async dispose() {
116
+ await client.close();
117
+ await this.stopServer();
118
+ },
119
+ };
120
+ return driver;
121
+ }
122
+ //# sourceMappingURL=test-driver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-driver.js","sourceRoot":"","sources":["../../src/mocks/test-driver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.57.7"
9
+ }
10
+ ]
11
+ }