@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.
- package/dist/ahp-client-socket.d.ts +109 -0
- package/dist/ahp-client-socket.d.ts.map +1 -0
- package/dist/ahp-client-socket.js +364 -0
- package/dist/ahp-client-socket.js.map +1 -0
- package/dist/ahp-server-socket.d.ts +111 -0
- package/dist/ahp-server-socket.d.ts.map +1 -0
- package/dist/ahp-server-socket.js +260 -0
- package/dist/ahp-server-socket.js.map +1 -0
- package/dist/backoff.d.ts +28 -0
- package/dist/backoff.d.ts.map +1 -0
- package/dist/backoff.js +31 -0
- package/dist/backoff.js.map +1 -0
- package/dist/client-id-store.d.ts +41 -0
- package/dist/client-id-store.d.ts.map +1 -0
- package/dist/client-id-store.js +66 -0
- package/dist/client-id-store.js.map +1 -0
- package/dist/error-codes.d.ts +45 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +32 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/examples/echo-subscriber.d.ts +29 -0
- package/dist/examples/echo-subscriber.d.ts.map +1 -0
- package/dist/examples/echo-subscriber.js +102 -0
- package/dist/examples/echo-subscriber.js.map +1 -0
- package/dist/heartbeat.d.ts +67 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +79 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/json-rpc-session.d.ts +106 -0
- package/dist/json-rpc-session.d.ts.map +1 -0
- package/dist/json-rpc-session.js +294 -0
- package/dist/json-rpc-session.js.map +1 -0
- package/dist/mocks/fake-websocket.d.ts +53 -0
- package/dist/mocks/fake-websocket.d.ts.map +1 -0
- package/dist/mocks/fake-websocket.js +86 -0
- package/dist/mocks/fake-websocket.js.map +1 -0
- package/dist/mocks/test-driver.d.ts +47 -0
- package/dist/mocks/test-driver.d.ts.map +1 -0
- package/dist/mocks/test-driver.js +122 -0
- package/dist/mocks/test-driver.js.map +1 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +51 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Echo subscriber — minimal end-to-end example that exercises the public
|
|
3
|
+
* `@grackle-ai/ahp-transport` surface (`JsonRpcSession`, `AhpServerSocket`,
|
|
4
|
+
* `AhpClientSocket`, `ClientIdStore`) for a realistic workflow:
|
|
5
|
+
*
|
|
6
|
+
* - Server hosts a single "echo" session. When a client subscribes to
|
|
7
|
+
* `ahp-session:/echo`, the server fires a sequence of `action`
|
|
8
|
+
* notifications back to the client.
|
|
9
|
+
* - Client connects, subscribes (via a `subscribe` JSON-RPC request, even
|
|
10
|
+
* though the framing layer doesn't formally route by channel — `MultiHost
|
|
11
|
+
* Client` in HR8b will), and collects every received notification.
|
|
12
|
+
*
|
|
13
|
+
* This file is not exported from the package barrel. It's referenced from
|
|
14
|
+
* `examples.integration.test.ts` to confirm the public API composes
|
|
15
|
+
* without requiring consumers to reach into internals.
|
|
16
|
+
*/
|
|
17
|
+
import { JsonRpcErrorCodes } from "@grackle-ai/ahp";
|
|
18
|
+
import { createServer } from "node:http";
|
|
19
|
+
import { AhpClientSocket } from "../ahp-client-socket.js";
|
|
20
|
+
import { InMemoryClientIdStore } from "../client-id-store.js";
|
|
21
|
+
import { AhpServerSocket, } from "../ahp-server-socket.js";
|
|
22
|
+
const INIT_RESULT = {
|
|
23
|
+
protocolVersion: "0.1.0",
|
|
24
|
+
serverSeq: 0,
|
|
25
|
+
snapshots: [],
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Spawn an echo subscriber: a server that emits `count` `action`
|
|
29
|
+
* notifications to any client that issues a `subscribe` JSON-RPC request,
|
|
30
|
+
* paired with a client that connects, subscribes, and returns the
|
|
31
|
+
* collected actions.
|
|
32
|
+
*
|
|
33
|
+
* Returns a `dispose()` that tears everything down.
|
|
34
|
+
*/
|
|
35
|
+
export async function runEchoSubscriber(count) {
|
|
36
|
+
// ── Server ──────────────────────────────────────────────────────
|
|
37
|
+
const server = createServer();
|
|
38
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
39
|
+
const port = server.address().port;
|
|
40
|
+
const subscribers = [];
|
|
41
|
+
const handleRequest = async (req, conn) => {
|
|
42
|
+
if (req.method === "subscribe") {
|
|
43
|
+
// Register this connection as a subscriber and fan out `count`
|
|
44
|
+
// notifications AFTER the response is on the wire. The afterSend
|
|
45
|
+
// primitive in the framing layer is exposed via the request handler's
|
|
46
|
+
// wrapped return; here we use a plain response and a microtask
|
|
47
|
+
// because the example demonstrates the *consumer* surface.
|
|
48
|
+
subscribers.push(conn);
|
|
49
|
+
queueMicrotask(() => {
|
|
50
|
+
for (let i = 0; i < count; i++) {
|
|
51
|
+
conn.session.notify("action", {
|
|
52
|
+
channel: "ahp-session:/echo",
|
|
53
|
+
serverSeq: i + 1,
|
|
54
|
+
action: { type: "echo/tick", payload: { i } },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
jsonrpc: "2.0",
|
|
60
|
+
id: req.id,
|
|
61
|
+
result: { channel: "ahp-session:/echo", snapshot: undefined },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
jsonrpc: "2.0",
|
|
66
|
+
id: req.id,
|
|
67
|
+
error: { code: JsonRpcErrorCodes.MethodNotFound, message: `no handler for ${req.method}` },
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const ahp = new AhpServerSocket({
|
|
71
|
+
server,
|
|
72
|
+
powerlineToken: "echo-token",
|
|
73
|
+
onInitialize: (_params) => INIT_RESULT,
|
|
74
|
+
onRequest: handleRequest,
|
|
75
|
+
});
|
|
76
|
+
// ── Client ──────────────────────────────────────────────────────
|
|
77
|
+
const received = [];
|
|
78
|
+
const client = new AhpClientSocket({
|
|
79
|
+
url: `ws://127.0.0.1:${port}/ahp`,
|
|
80
|
+
powerlineToken: "echo-token",
|
|
81
|
+
clientIdStore: new InMemoryClientIdStore(),
|
|
82
|
+
clientIdKey: "echo-example",
|
|
83
|
+
onNotification: (notif) => {
|
|
84
|
+
if (notif.method === "action") {
|
|
85
|
+
received.push(notif.params);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
await client.open();
|
|
90
|
+
await client.request("subscribe", {
|
|
91
|
+
channel: "ahp-session:/echo",
|
|
92
|
+
});
|
|
93
|
+
// Give notifications a beat to arrive.
|
|
94
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
95
|
+
const dispose = async () => {
|
|
96
|
+
await client.close();
|
|
97
|
+
await ahp.close();
|
|
98
|
+
await new Promise((r) => server.close(() => r()));
|
|
99
|
+
};
|
|
100
|
+
return { received, dispose };
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=echo-subscriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"echo-subscriber.js","sourceRoot":"","sources":["../../src/examples/echo-subscriber.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat state machine. Sends WebSocket-level pings on a fixed interval
|
|
3
|
+
* and closes the connection when too many pings go un-pong'd in a row.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from {@link AhpServerSocket} so the close-on-threshold branch can
|
|
6
|
+
* be tested deterministically with a stub target (real `ws` clients auto-pong
|
|
7
|
+
* and make the close path unreachable in integration tests).
|
|
8
|
+
*/
|
|
9
|
+
/** Minimal interface the heartbeat needs from its underlying socket. */
|
|
10
|
+
export interface HeartbeatTarget {
|
|
11
|
+
/** Send a WebSocket-level PING frame. */
|
|
12
|
+
ping(): void;
|
|
13
|
+
/** Close the connection with the given code and reason. */
|
|
14
|
+
close(code: number, reason: string): void;
|
|
15
|
+
/** Subscribe to PONG events. The heartbeat only listens for "pong". */
|
|
16
|
+
on(event: "pong", listener: () => void): void;
|
|
17
|
+
}
|
|
18
|
+
/** Construction options for {@link Heartbeat}. */
|
|
19
|
+
export interface HeartbeatOptions {
|
|
20
|
+
/** Target socket the heartbeat operates on. */
|
|
21
|
+
readonly target: HeartbeatTarget;
|
|
22
|
+
/** Interval between ping ticks in milliseconds. */
|
|
23
|
+
readonly intervalMs: number;
|
|
24
|
+
/**
|
|
25
|
+
* Number of consecutive missed pongs (i.e., ticks where the previous tick's
|
|
26
|
+
* ping never got a pong before this tick) before closing with 4001.
|
|
27
|
+
*
|
|
28
|
+
* - `1` closes on the first real missed pong.
|
|
29
|
+
* - `2` (default in `AhpServerSocket`) closes on the second.
|
|
30
|
+
*/
|
|
31
|
+
readonly missedLimit: number;
|
|
32
|
+
/**
|
|
33
|
+
* Injectable timer factory for testing. Defaults to global
|
|
34
|
+
* `setInterval`/`clearInterval`. Tests use `vi.useFakeTimers()` to bypass
|
|
35
|
+
* real time without needing to inject anything here.
|
|
36
|
+
*/
|
|
37
|
+
readonly timers?: {
|
|
38
|
+
setInterval: typeof setInterval;
|
|
39
|
+
clearInterval: typeof clearInterval;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Heartbeat for a single WebSocket-like target. Owns no `WebSocket` itself;
|
|
44
|
+
* the integration with `ws.WebSocket` happens in {@link AhpServerSocket} via
|
|
45
|
+
* the {@link HeartbeatTarget} adapter.
|
|
46
|
+
*/
|
|
47
|
+
export declare class Heartbeat {
|
|
48
|
+
private readonly target;
|
|
49
|
+
private readonly intervalMs;
|
|
50
|
+
private readonly missedLimit;
|
|
51
|
+
private readonly setInterval;
|
|
52
|
+
private readonly clearInterval;
|
|
53
|
+
private timer;
|
|
54
|
+
/** True between a `ping()` and the matching `pong` event. */
|
|
55
|
+
private pingOutstanding;
|
|
56
|
+
/** Count of consecutive ticks where the previous tick's ping went un-pong'd. */
|
|
57
|
+
private missedPongs;
|
|
58
|
+
/** True once `stop()` has been called; prevents the timer body from running. */
|
|
59
|
+
private stopped;
|
|
60
|
+
constructor(opts: HeartbeatOptions);
|
|
61
|
+
/** Begin pinging on the configured interval. Idempotent. */
|
|
62
|
+
start(): void;
|
|
63
|
+
/** Stop pinging. After this, the heartbeat will not fire again. Idempotent. */
|
|
64
|
+
stop(): void;
|
|
65
|
+
private onTick;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=heartbeat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,yCAAyC;IACzC,IAAI,IAAI,IAAI,CAAC;IACb,2DAA2D;IAC3D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,uEAAuE;IACvE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAC/C;AAED,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,mDAAmD;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,WAAW,EAAE,OAAO,WAAW,CAAC;QAChC,aAAa,EAAE,OAAO,aAAa,CAAC;KACrC,CAAC;CACH;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAuB;IAErD,OAAO,CAAC,KAAK,CAA6B;IAC1C,6DAA6D;IAC7D,OAAO,CAAC,eAAe,CAAS;IAChC,gFAAgF;IAChF,OAAO,CAAC,WAAW,CAAK;IACxB,gFAAgF;IAChF,OAAO,CAAC,OAAO,CAAS;gBAEL,IAAI,EAAE,gBAAgB;IAazC,4DAA4D;IACrD,KAAK,IAAI,IAAI;IAOpB,+EAA+E;IACxE,IAAI,IAAI,IAAI;IAQnB,OAAO,CAAC,MAAM;CAuBf"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat state machine. Sends WebSocket-level pings on a fixed interval
|
|
3
|
+
* and closes the connection when too many pings go un-pong'd in a row.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from {@link AhpServerSocket} so the close-on-threshold branch can
|
|
6
|
+
* be tested deterministically with a stub target (real `ws` clients auto-pong
|
|
7
|
+
* and make the close path unreachable in integration tests).
|
|
8
|
+
*/
|
|
9
|
+
import { WsCloseCode } from "./error-codes.js";
|
|
10
|
+
/**
|
|
11
|
+
* Heartbeat for a single WebSocket-like target. Owns no `WebSocket` itself;
|
|
12
|
+
* the integration with `ws.WebSocket` happens in {@link AhpServerSocket} via
|
|
13
|
+
* the {@link HeartbeatTarget} adapter.
|
|
14
|
+
*/
|
|
15
|
+
export class Heartbeat {
|
|
16
|
+
target;
|
|
17
|
+
intervalMs;
|
|
18
|
+
missedLimit;
|
|
19
|
+
setInterval;
|
|
20
|
+
clearInterval;
|
|
21
|
+
timer;
|
|
22
|
+
/** True between a `ping()` and the matching `pong` event. */
|
|
23
|
+
pingOutstanding = false;
|
|
24
|
+
/** Count of consecutive ticks where the previous tick's ping went un-pong'd. */
|
|
25
|
+
missedPongs = 0;
|
|
26
|
+
/** True once `stop()` has been called; prevents the timer body from running. */
|
|
27
|
+
stopped = false;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
this.target = opts.target;
|
|
30
|
+
this.intervalMs = opts.intervalMs;
|
|
31
|
+
this.missedLimit = opts.missedLimit;
|
|
32
|
+
this.setInterval = opts.timers?.setInterval ?? setInterval;
|
|
33
|
+
this.clearInterval = opts.timers?.clearInterval ?? clearInterval;
|
|
34
|
+
this.target.on("pong", () => {
|
|
35
|
+
this.pingOutstanding = false;
|
|
36
|
+
this.missedPongs = 0;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Begin pinging on the configured interval. Idempotent. */
|
|
40
|
+
start() {
|
|
41
|
+
if (this.timer !== undefined || this.stopped) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.timer = this.setInterval(() => this.onTick(), this.intervalMs);
|
|
45
|
+
}
|
|
46
|
+
/** Stop pinging. After this, the heartbeat will not fire again. Idempotent. */
|
|
47
|
+
stop() {
|
|
48
|
+
this.stopped = true;
|
|
49
|
+
if (this.timer !== undefined) {
|
|
50
|
+
this.clearInterval(this.timer);
|
|
51
|
+
this.timer = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
onTick() {
|
|
55
|
+
if (this.stopped) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// If the prior tick's ping never got a pong before this tick, count it
|
|
59
|
+
// as a real miss. The initial tick (pingOutstanding=false) skips this
|
|
60
|
+
// — there's no prior ping to have missed.
|
|
61
|
+
if (this.pingOutstanding) {
|
|
62
|
+
this.missedPongs += 1;
|
|
63
|
+
if (this.missedPongs >= this.missedLimit) {
|
|
64
|
+
this.stop();
|
|
65
|
+
this.target.close(WsCloseCode.HeartbeatTimeout, "heartbeat timeout");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
this.target.ping();
|
|
71
|
+
this.pingOutstanding = true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Target already closing; the AhpServerSocket close handler will tear
|
|
75
|
+
// down the timer via stop().
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAqC/C;;;;GAIG;AACH,MAAM,OAAO,SAAS;IACH,MAAM,CAAkB;IACxB,UAAU,CAAS;IACnB,WAAW,CAAS;IACpB,WAAW,CAAqB;IAChC,aAAa,CAAuB;IAE7C,KAAK,CAA6B;IAC1C,6DAA6D;IACrD,eAAe,GAAG,KAAK,CAAC;IAChC,gFAAgF;IACxE,WAAW,GAAG,CAAC,CAAC;IACxB,gFAAgF;IACxE,OAAO,GAAG,KAAK,CAAC;IAExB,YAAmB,IAAsB;QACvC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,IAAI,WAAW,CAAC;QAC3D,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,aAAa,IAAI,aAAa,CAAC;QAEjE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YAC1B,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,4DAA4D;IACrD,KAAK;QACV,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC7C,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACtE,CAAC;IAED,+EAA+E;IACxE,IAAI;QACT,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACzB,CAAC;IACH,CAAC;IAEO,MAAM;QACZ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,uEAAuE;QACvE,sEAAsE;QACtE,0CAA0C;QAC1C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;YACtB,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACzC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACZ,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;gBACrE,OAAO;YACT,CAAC;QACH,CAAC;QACD,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,6BAA6B;QAC/B,CAAC;IACH,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@grackle-ai/ahp-transport` — WebSocket + JSON-RPC 2.0 framing primitive
|
|
3
|
+
* for the Agent Host Protocol (AHP) wire. Provides the low-level transport
|
|
4
|
+
* layer that {@link MultiHostClient} (HR8b) and the PowerLine AHP host
|
|
5
|
+
* (HR8d) build on. The package is consumer-agnostic: it ships the framing,
|
|
6
|
+
* auth handshake, and reconnect semantics; channel-scoped concerns
|
|
7
|
+
* (per-`(host, channel)` `serverSeq` tracking, subscription replay, the
|
|
8
|
+
* generation counter for stale-handle invalidation) live one layer up.
|
|
9
|
+
*/
|
|
10
|
+
export { JsonRpcSession } from "./json-rpc-session.js";
|
|
11
|
+
export type { JsonRpcSessionOptions, RequestHandler, RequestHandlerResult, NotificationHandler, } from "./json-rpc-session.js";
|
|
12
|
+
export { AhpServerSocket } from "./ahp-server-socket.js";
|
|
13
|
+
export type { AhpServerSocketOptions, AhpServerConnection } from "./ahp-server-socket.js";
|
|
14
|
+
export { AhpClientSocket } from "./ahp-client-socket.js";
|
|
15
|
+
export type { AhpClientSocketOptions, AhpConnectionState } from "./ahp-client-socket.js";
|
|
16
|
+
export type { ClientIdStore } from "./client-id-store.js";
|
|
17
|
+
export { FileClientIdStore, InMemoryClientIdStore } from "./client-id-store.js";
|
|
18
|
+
export { exponentialBackoff } from "./backoff.js";
|
|
19
|
+
export type { BackoffPolicy, ExponentialBackoffOptions } from "./backoff.js";
|
|
20
|
+
export { TransportError, WsCloseCode } from "./error-codes.js";
|
|
21
|
+
export type { TransportErrorKind } from "./error-codes.js";
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EACV,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE1F,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAE7E,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@grackle-ai/ahp-transport` — WebSocket + JSON-RPC 2.0 framing primitive
|
|
3
|
+
* for the Agent Host Protocol (AHP) wire. Provides the low-level transport
|
|
4
|
+
* layer that {@link MultiHostClient} (HR8b) and the PowerLine AHP host
|
|
5
|
+
* (HR8d) build on. The package is consumer-agnostic: it ships the framing,
|
|
6
|
+
* auth handshake, and reconnect semantics; channel-scoped concerns
|
|
7
|
+
* (per-`(host, channel)` `serverSeq` tracking, subscription replay, the
|
|
8
|
+
* generation counter for stale-handle invalidation) live one layer up.
|
|
9
|
+
*/
|
|
10
|
+
export { JsonRpcSession } from "./json-rpc-session.js";
|
|
11
|
+
export { AhpServerSocket } from "./ahp-server-socket.js";
|
|
12
|
+
export { AhpClientSocket } from "./ahp-client-socket.js";
|
|
13
|
+
export { FileClientIdStore, InMemoryClientIdStore } from "./client-id-store.js";
|
|
14
|
+
export { exponentialBackoff } from "./backoff.js";
|
|
15
|
+
export { TransportError, WsCloseCode } from "./error-codes.js";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQvD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAIzD,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
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 type { AhpNotification, AhpRequest, AhpResponse, CommandMap } from "@grackle-ai/ahp";
|
|
12
|
+
import type { WebSocket } from "ws";
|
|
13
|
+
/**
|
|
14
|
+
* Result returned by a {@link RequestHandler}. Either a bare `AhpResponse`
|
|
15
|
+
* (from `@grackle-ai/ahp`, the common case), or a wrapped result that
|
|
16
|
+
* includes an `afterSend` callback fired AFTER the response frame has been
|
|
17
|
+
* flushed to the wire. The wrapped form is the deterministic ordering
|
|
18
|
+
* primitive: use it whenever a side effect (e.g., close the session, emit
|
|
19
|
+
* a notification) must follow the response on the wire rather than precede it.
|
|
20
|
+
*/
|
|
21
|
+
export type RequestHandlerResult = AhpResponse | {
|
|
22
|
+
readonly response: AhpResponse;
|
|
23
|
+
readonly afterSend: () => void;
|
|
24
|
+
};
|
|
25
|
+
/** Handler for inbound peer-initiated requests. */
|
|
26
|
+
export type RequestHandler = (req: AhpRequest) => Promise<RequestHandlerResult>;
|
|
27
|
+
/** Handler for inbound peer-initiated notifications. */
|
|
28
|
+
export type NotificationHandler = (notif: AhpNotification) => void;
|
|
29
|
+
/** Construction options for {@link JsonRpcSession}. */
|
|
30
|
+
export interface JsonRpcSessionOptions {
|
|
31
|
+
/** An already-OPEN WebSocket. The session takes ownership of close/error events. */
|
|
32
|
+
readonly socket: WebSocket;
|
|
33
|
+
/** Called when the peer sends a JSON-RPC request. Omit to reject all inbound requests. */
|
|
34
|
+
readonly onRequest?: RequestHandler;
|
|
35
|
+
/** Called when the peer sends a JSON-RPC notification. Omit to drop silently. */
|
|
36
|
+
readonly onNotification?: NotificationHandler;
|
|
37
|
+
/** Called once when the socket closes; pending requests have already been rejected. */
|
|
38
|
+
readonly onClose?: (code: number, reason: string) => void;
|
|
39
|
+
/** If set, pending requests time out after this many milliseconds. */
|
|
40
|
+
readonly requestTimeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Bidirectional JSON-RPC session over one WebSocket.
|
|
44
|
+
*
|
|
45
|
+
* @example Wrap an already-OPEN ws connection and exchange a typed request:
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { WebSocket } from "ws";
|
|
48
|
+
* const socket = new WebSocket(url);
|
|
49
|
+
* await new Promise((r) => socket.once("open", r));
|
|
50
|
+
*
|
|
51
|
+
* const session = new JsonRpcSession({
|
|
52
|
+
* socket,
|
|
53
|
+
* onNotification: (n) => console.log("inbound:", n.method),
|
|
54
|
+
* });
|
|
55
|
+
* const result = await session.request("ping", { channel: "ahp-root://" });
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example Send a response that closes the session after the frame flushes:
|
|
59
|
+
* ```ts
|
|
60
|
+
* const session = new JsonRpcSession({
|
|
61
|
+
* socket,
|
|
62
|
+
* onRequest: async (req) => ({
|
|
63
|
+
* response: { jsonrpc: "2.0", id: req.id, error: { code: -32600, message: "bad" } },
|
|
64
|
+
* afterSend: () => session.close(1000, "bye"),
|
|
65
|
+
* }),
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare class JsonRpcSession {
|
|
70
|
+
private readonly socket;
|
|
71
|
+
private readonly onRequest;
|
|
72
|
+
private readonly onNotification;
|
|
73
|
+
private readonly onClose;
|
|
74
|
+
private readonly requestTimeoutMs;
|
|
75
|
+
private nextRequestId;
|
|
76
|
+
private readonly pendingRequests;
|
|
77
|
+
private closed;
|
|
78
|
+
constructor(opts: JsonRpcSessionOptions);
|
|
79
|
+
/** Sends a request and resolves with the result (or rejects with the error). */
|
|
80
|
+
request<M extends keyof CommandMap>(method: M, params: CommandMap[M]["params"]): Promise<CommandMap[M]["result"]>;
|
|
81
|
+
/** Sends a notification (fire-and-forget). Silently drops if already closed. */
|
|
82
|
+
notify(method: string, params: unknown): void;
|
|
83
|
+
/** Closes the underlying socket. Idempotent. */
|
|
84
|
+
close(code?: number, reason?: string): void;
|
|
85
|
+
/** True between construction and the socket's "close" event firing. */
|
|
86
|
+
get isOpen(): boolean;
|
|
87
|
+
private readonly handleMessage;
|
|
88
|
+
private resolvePending;
|
|
89
|
+
private rejectPending;
|
|
90
|
+
private handleInboundRequest;
|
|
91
|
+
/**
|
|
92
|
+
* Sends `frame` and invokes `after` once the data has been flushed to the
|
|
93
|
+
* OS socket buffer. Use this when a side effect (e.g., closing the session)
|
|
94
|
+
* MUST follow the frame on the wire.
|
|
95
|
+
*
|
|
96
|
+
* `after` runs even if `socket.send` errors — the typical close-after-error
|
|
97
|
+
* caller wants the close to proceed regardless.
|
|
98
|
+
*/
|
|
99
|
+
sendAndThen(frame: unknown, after: () => void): void;
|
|
100
|
+
private handleInboundNotification;
|
|
101
|
+
private writeError;
|
|
102
|
+
private tryWriteParseError;
|
|
103
|
+
private readonly handleClose;
|
|
104
|
+
private readonly handleError;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=json-rpc-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-rpc-session.d.ts","sourceRoot":"","sources":["../src/json-rpc-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE5F,OAAO,KAAK,EAAW,SAAS,EAAE,MAAM,IAAI,CAAC;AAI7C;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAC5B,WAAW,GACX;IAAE,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC;AAEvE,mDAAmD;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEhF,wDAAwD;AACxD,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAEnE,uDAAuD;AACvD,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC;IACpC,iFAAiF;IACjF,QAAQ,CAAC,cAAc,CAAC,EAAE,mBAAmB,CAAC;IAC9C,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAkC;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuD;IAC/E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IAEtD,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;IACrE,OAAO,CAAC,MAAM,CAAS;gBAEJ,IAAI,EAAE,qBAAqB;IAY9C,gFAAgF;IACzE,OAAO,CAAC,CAAC,SAAS,MAAM,UAAU,EACvC,MAAM,EAAE,CAAC,EACT,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAC9B,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IA6CnC,gFAAgF;IACzE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAWpD,gDAAgD;IACzC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAOlD,uEAAuE;IACvE,IAAW,MAAM,IAAI,OAAO,CAE3B;IAID,OAAO,CAAC,QAAQ,CAAC,aAAa,CAiD5B;IAEF,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,aAAa;YAYP,oBAAoB;IA0BlC;;;;;;;OAOG;IACI,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,IAAI;IAqB3D,OAAO,CAAC,yBAAyB;IAWjC,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAmB1B;IAEF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAI1B;CACH"}
|