@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,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side AHP transport. Mounts on an existing HTTP/HTTP2 server,
|
|
3
|
+
* authenticates the upgrade request via `Authorization: Bearer <token>`,
|
|
4
|
+
* runs the AHP `initialize` handshake, then surfaces an
|
|
5
|
+
* {@link AhpServerConnection} (with a ready-to-use {@link JsonRpcSession})
|
|
6
|
+
* to the application.
|
|
7
|
+
*
|
|
8
|
+
* Heartbeat: WebSocket-level pings every 30s; 2 consecutive missed pongs
|
|
9
|
+
* close with code 4001.
|
|
10
|
+
*/
|
|
11
|
+
import { JsonRpcErrorCodes } from "@grackle-ai/ahp";
|
|
12
|
+
import { timingSafeEqual } from "node:crypto";
|
|
13
|
+
import { WebSocketServer } from "ws";
|
|
14
|
+
import { WsCloseCode } from "./error-codes.js";
|
|
15
|
+
import { Heartbeat } from "./heartbeat.js";
|
|
16
|
+
import { JsonRpcSession } from "./json-rpc-session.js";
|
|
17
|
+
/** Default WS path mounted on the host HTTP server. */
|
|
18
|
+
const DEFAULT_PATH = "/ahp";
|
|
19
|
+
/** Default ping interval in milliseconds. */
|
|
20
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
21
|
+
/** Default number of consecutive missed pongs before close-4001. */
|
|
22
|
+
const DEFAULT_HEARTBEAT_MISSED_LIMIT = 2;
|
|
23
|
+
/**
|
|
24
|
+
* AHP-spec JSON-RPC/WebSocket server. Mounts on an existing
|
|
25
|
+
* `http.Server`/`http2.SecureServer` and surfaces fully-initialized
|
|
26
|
+
* `AhpServerConnection`s to the application.
|
|
27
|
+
*
|
|
28
|
+
* @example Mount on an HTTP server and accept one client:
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { createServer } from "node:http";
|
|
31
|
+
* const server = createServer();
|
|
32
|
+
* server.listen(7433, "127.0.0.1");
|
|
33
|
+
*
|
|
34
|
+
* const ahp = new AhpServerSocket({
|
|
35
|
+
* server,
|
|
36
|
+
* powerlineToken: process.env.GRACKLE_POWERLINE_TOKEN ?? "",
|
|
37
|
+
* onInitialize: (params) => ({
|
|
38
|
+
* protocolVersion: "0.1.0",
|
|
39
|
+
* serverSeq: 0,
|
|
40
|
+
* snapshots: [],
|
|
41
|
+
* }),
|
|
42
|
+
* onConnection: (conn) => {
|
|
43
|
+
* console.log("client connected:", conn.clientId);
|
|
44
|
+
* conn.session.notify("action", { channel: "ahp-session:/x", serverSeq: 1, action: {...} });
|
|
45
|
+
* },
|
|
46
|
+
* onRequest: async (req, conn) => {
|
|
47
|
+
* // typed dispatch on req.method
|
|
48
|
+
* return { jsonrpc: "2.0", id: req.id, result: null };
|
|
49
|
+
* },
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class AhpServerSocket {
|
|
54
|
+
server;
|
|
55
|
+
powerlineToken;
|
|
56
|
+
path;
|
|
57
|
+
onInitialize;
|
|
58
|
+
onRequest;
|
|
59
|
+
onNotification;
|
|
60
|
+
onConnection;
|
|
61
|
+
onDisconnect;
|
|
62
|
+
heartbeatIntervalMs;
|
|
63
|
+
heartbeatMissedLimit;
|
|
64
|
+
wss;
|
|
65
|
+
connections = new Set();
|
|
66
|
+
upgradeListener;
|
|
67
|
+
closed = false;
|
|
68
|
+
constructor(opts) {
|
|
69
|
+
this.server = opts.server;
|
|
70
|
+
this.powerlineToken = opts.powerlineToken;
|
|
71
|
+
this.path = opts.path ?? DEFAULT_PATH;
|
|
72
|
+
this.onInitialize = opts.onInitialize;
|
|
73
|
+
this.onRequest = opts.onRequest;
|
|
74
|
+
this.onNotification = opts.onNotification;
|
|
75
|
+
this.onConnection = opts.onConnection;
|
|
76
|
+
this.onDisconnect = opts.onDisconnect;
|
|
77
|
+
this.heartbeatIntervalMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
78
|
+
this.heartbeatMissedLimit = opts.heartbeatMissedLimit ?? DEFAULT_HEARTBEAT_MISSED_LIMIT;
|
|
79
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
80
|
+
this.upgradeListener = (req, socket, head) => this.handleUpgrade(req, socket, head);
|
|
81
|
+
this.server.on("upgrade", this.upgradeListener);
|
|
82
|
+
}
|
|
83
|
+
/** Stops accepting new connections and closes all existing sessions. */
|
|
84
|
+
async close() {
|
|
85
|
+
if (this.closed) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.closed = true;
|
|
89
|
+
this.server.off("upgrade", this.upgradeListener);
|
|
90
|
+
for (const conn of this.connections) {
|
|
91
|
+
conn.heartbeat.stop();
|
|
92
|
+
conn.session.close(WsCloseCode.Normal, "server shutting down");
|
|
93
|
+
}
|
|
94
|
+
this.connections.clear();
|
|
95
|
+
await new Promise((resolve) => this.wss.close(() => resolve()));
|
|
96
|
+
}
|
|
97
|
+
// ─── Upgrade + auth ────────────────────────────────────────────────
|
|
98
|
+
handleUpgrade(req, socket, head) {
|
|
99
|
+
// Only handle our path; let other listeners see other paths.
|
|
100
|
+
const requestPath = (req.url ?? "/").split("?")[0];
|
|
101
|
+
if (requestPath !== this.path) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!this.authorizeUpgrade(req)) {
|
|
105
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
106
|
+
socket.destroy();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => this.attachSocket(ws, req));
|
|
110
|
+
}
|
|
111
|
+
authorizeUpgrade(req) {
|
|
112
|
+
if (this.powerlineToken === "") {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const header = (req.headers["authorization"] ?? "").toString();
|
|
116
|
+
const bearerMatch = /^Bearer\s+(\S+)$/i.exec(header);
|
|
117
|
+
if (bearerMatch === null) {
|
|
118
|
+
// Reject anything that isn't an `Authorization: Bearer <token>` header
|
|
119
|
+
// — including a raw token with no scheme prefix.
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const supplied = bearerMatch[1] ?? "";
|
|
123
|
+
const a = Buffer.from(supplied);
|
|
124
|
+
const b = Buffer.from(this.powerlineToken);
|
|
125
|
+
if (a.length !== b.length) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return timingSafeEqual(a, b);
|
|
129
|
+
}
|
|
130
|
+
// ─── Per-connection lifecycle ──────────────────────────────────────
|
|
131
|
+
attachSocket(ws, req) {
|
|
132
|
+
const state = {
|
|
133
|
+
socket: ws,
|
|
134
|
+
session: undefined,
|
|
135
|
+
connection: undefined,
|
|
136
|
+
heartbeat: undefined,
|
|
137
|
+
};
|
|
138
|
+
state.session = new JsonRpcSession({
|
|
139
|
+
socket: ws,
|
|
140
|
+
onRequest: (innerReq) => this.handleRequest(innerReq, state, req),
|
|
141
|
+
onNotification: (notif) => this.handleNotification(notif, state),
|
|
142
|
+
onClose: (code, reason) => this.handleSocketClose(state, code, reason),
|
|
143
|
+
});
|
|
144
|
+
state.heartbeat = new Heartbeat({
|
|
145
|
+
target: {
|
|
146
|
+
ping: () => ws.ping(),
|
|
147
|
+
close: (code, reason) => state.session.close(code, reason),
|
|
148
|
+
on: (event, listener) => {
|
|
149
|
+
ws.on(event, listener);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
intervalMs: this.heartbeatIntervalMs,
|
|
153
|
+
missedLimit: this.heartbeatMissedLimit,
|
|
154
|
+
});
|
|
155
|
+
this.connections.add(state);
|
|
156
|
+
state.heartbeat.start();
|
|
157
|
+
}
|
|
158
|
+
async handleRequest(req, state, httpReq) {
|
|
159
|
+
if (req.method === "initialize") {
|
|
160
|
+
if (state.connection !== undefined) {
|
|
161
|
+
return {
|
|
162
|
+
jsonrpc: "2.0",
|
|
163
|
+
id: req.id,
|
|
164
|
+
error: {
|
|
165
|
+
code: JsonRpcErrorCodes.InvalidRequest,
|
|
166
|
+
message: "session already initialized",
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const params = req.params;
|
|
172
|
+
const result = await this.onInitialize(params);
|
|
173
|
+
const connection = {
|
|
174
|
+
clientId: params.clientId,
|
|
175
|
+
initializeParams: params,
|
|
176
|
+
session: state.session,
|
|
177
|
+
remoteAddress: httpReq.socket.remoteAddress,
|
|
178
|
+
};
|
|
179
|
+
state.connection = connection;
|
|
180
|
+
// Surface the connection AFTER the initialize response has been
|
|
181
|
+
// flushed to the wire. `afterSend` runs in JsonRpcSession's
|
|
182
|
+
// `ws.send(data, callback)` completion path — deterministic ordering,
|
|
183
|
+
// no event-loop assumptions.
|
|
184
|
+
return {
|
|
185
|
+
response: { jsonrpc: "2.0", id: req.id, result },
|
|
186
|
+
afterSend: () => this.onConnection?.(connection),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
// onInitialize failure: send the error response, then close the
|
|
191
|
+
// session. The handshake boundary requires the peer can't retry
|
|
192
|
+
// initialize on the same socket after a handshake-layer failure.
|
|
193
|
+
return {
|
|
194
|
+
response: {
|
|
195
|
+
jsonrpc: "2.0",
|
|
196
|
+
id: req.id,
|
|
197
|
+
error: {
|
|
198
|
+
code: JsonRpcErrorCodes.InternalError,
|
|
199
|
+
message: err.message || "onInitialize threw",
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
afterSend: () => state.session.close(WsCloseCode.Normal, "initialize failed"),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const connection = state.connection;
|
|
207
|
+
if (connection === undefined) {
|
|
208
|
+
// Enforce the handshake boundary: the first inbound JSON-RPC must be
|
|
209
|
+
// `initialize`. Close the session after the error response is flushed.
|
|
210
|
+
return {
|
|
211
|
+
response: {
|
|
212
|
+
jsonrpc: "2.0",
|
|
213
|
+
id: req.id,
|
|
214
|
+
error: {
|
|
215
|
+
code: JsonRpcErrorCodes.InvalidRequest,
|
|
216
|
+
message: "first request must be initialize",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
afterSend: () => state.session.close(WsCloseCode.Normal, "initialize required first"),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (this.onRequest === undefined) {
|
|
223
|
+
return {
|
|
224
|
+
jsonrpc: "2.0",
|
|
225
|
+
id: req.id,
|
|
226
|
+
error: {
|
|
227
|
+
code: JsonRpcErrorCodes.MethodNotFound,
|
|
228
|
+
message: `no handler for ${req.method}`,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return this.onRequest(req, connection);
|
|
233
|
+
}
|
|
234
|
+
handleNotification(notif, state) {
|
|
235
|
+
if (state.connection === undefined) {
|
|
236
|
+
// Pre-initialize notification violates the handshake contract.
|
|
237
|
+
// Notifications have no response to flush, so we can close
|
|
238
|
+
// immediately — no ordering concern.
|
|
239
|
+
state.session.close(WsCloseCode.Normal, "initialize required first");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (this.onNotification === undefined) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
this.onNotification(notif, state.connection);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Handler errors are swallowed to keep the session alive.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
handleSocketClose(state, code, reason) {
|
|
253
|
+
state.heartbeat.stop();
|
|
254
|
+
this.connections.delete(state);
|
|
255
|
+
if (state.connection !== undefined) {
|
|
256
|
+
this.onDisconnect?.(state.connection.clientId, code, reason);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=ahp-server-socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ahp-server-socket.js","sourceRoot":"","sources":["../src/ahp-server-socket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconnect backoff policy used by {@link AhpClientSocket}. Pure logic — no
|
|
3
|
+
* timers; callers do the actual sleeping.
|
|
4
|
+
*/
|
|
5
|
+
/** A stateful policy that yields successive delays. */
|
|
6
|
+
export interface BackoffPolicy {
|
|
7
|
+
/** Returns the next delay (in milliseconds) to wait before reconnecting. */
|
|
8
|
+
next(): number;
|
|
9
|
+
/** Resets the policy back to the initial delay (called after a successful reconnect). */
|
|
10
|
+
reset(): void;
|
|
11
|
+
}
|
|
12
|
+
/** Options for {@link exponentialBackoff}. */
|
|
13
|
+
export interface ExponentialBackoffOptions {
|
|
14
|
+
/** First delay returned by `next()`. Default 250ms. */
|
|
15
|
+
readonly initialMs?: number;
|
|
16
|
+
/** Cap on the delay. Default 30_000ms. */
|
|
17
|
+
readonly maxMs?: number;
|
|
18
|
+
/** Symmetric multiplicative jitter, e.g. 0.25 = ±25%. Default 0.25. */
|
|
19
|
+
readonly jitter?: number;
|
|
20
|
+
/** Random source, injectable for tests. Defaults to `Math.random`. */
|
|
21
|
+
readonly random?: () => number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns a {@link BackoffPolicy} that doubles each call up to `maxMs`,
|
|
25
|
+
* with symmetric jitter applied to each yielded value.
|
|
26
|
+
*/
|
|
27
|
+
export declare function exponentialBackoff(options?: ExponentialBackoffOptions): BackoffPolicy;
|
|
28
|
+
//# sourceMappingURL=backoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,IAAI,IAAI,MAAM,CAAC;IACf,yFAAyF;IACzF,KAAK,IAAI,IAAI,CAAC;CACf;AAED,8CAA8C;AAC9C,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,aAAa,CAoBzF"}
|
package/dist/backoff.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconnect backoff policy used by {@link AhpClientSocket}. Pure logic — no
|
|
3
|
+
* timers; callers do the actual sleeping.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_INITIAL_MS = 250;
|
|
6
|
+
const DEFAULT_MAX_MS = 30_000;
|
|
7
|
+
const DEFAULT_JITTER = 0.25;
|
|
8
|
+
/**
|
|
9
|
+
* Returns a {@link BackoffPolicy} that doubles each call up to `maxMs`,
|
|
10
|
+
* with symmetric jitter applied to each yielded value.
|
|
11
|
+
*/
|
|
12
|
+
export function exponentialBackoff(options = {}) {
|
|
13
|
+
const initialMs = options.initialMs ?? DEFAULT_INITIAL_MS;
|
|
14
|
+
const maxMs = options.maxMs ?? DEFAULT_MAX_MS;
|
|
15
|
+
const jitter = options.jitter ?? DEFAULT_JITTER;
|
|
16
|
+
const random = options.random ?? Math.random;
|
|
17
|
+
let current = initialMs;
|
|
18
|
+
return {
|
|
19
|
+
next() {
|
|
20
|
+
const base = Math.min(current, maxMs);
|
|
21
|
+
current = Math.min(current * 2, maxMs);
|
|
22
|
+
// Symmetric jitter: scale by [1-jitter, 1+jitter].
|
|
23
|
+
const jitterFactor = 1 + (random() * 2 - 1) * jitter;
|
|
24
|
+
return Math.max(0, Math.round(base * jitterFactor));
|
|
25
|
+
},
|
|
26
|
+
reset() {
|
|
27
|
+
current = initialMs;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,cAAc,GAAG,MAAM,CAAC;AAC9B,MAAM,cAAc,GAAG,IAAI,CAAC;AAsB5B;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAqC,EAAE;IACxE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAAC;IAC1D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC;IAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC;IAChD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;IAE7C,IAAI,OAAO,GAAG,SAAS,CAAC;IAExB,OAAO;QACL,IAAI;YACF,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACtC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YACvC,mDAAmD;YACnD,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;YACrD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC;QACtD,CAAC;QACD,KAAK;YACH,OAAO,GAAG,SAAS,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the AHP `clientId`. Per the AHP spec, the client owns
|
|
3
|
+
* its identifier — `InitializeParams.clientId` is supplied by the client
|
|
4
|
+
* on every connect. {@link AhpClientSocket} mints a fresh UUID via
|
|
5
|
+
* `randomUUID()` the first time it sees an empty store, persists it here,
|
|
6
|
+
* and replays the same id on every subsequent connect so the host can
|
|
7
|
+
* resume in-flight subscriptions.
|
|
8
|
+
*
|
|
9
|
+
* Keyed by an opaque string so a single store can serve multiple
|
|
10
|
+
* connections (e.g., one `ClientIdStore` per `MultiHostClient` keyed
|
|
11
|
+
* by host id).
|
|
12
|
+
*/
|
|
13
|
+
/** Persistent storage for AHP client identifiers. */
|
|
14
|
+
export interface ClientIdStore {
|
|
15
|
+
/** Returns the stored client id for `key`, or `undefined` if none exists. */
|
|
16
|
+
load(key: string): Promise<string | undefined>;
|
|
17
|
+
/** Persists `clientId` under `key`, replacing any prior value. */
|
|
18
|
+
save(key: string, clientId: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* In-memory store. Suitable for tests and for the lifetime of a single
|
|
22
|
+
* process where persistence across restarts is not required.
|
|
23
|
+
*/
|
|
24
|
+
export declare class InMemoryClientIdStore implements ClientIdStore {
|
|
25
|
+
private readonly entries;
|
|
26
|
+
load(key: string): Promise<string | undefined>;
|
|
27
|
+
save(key: string, clientId: string): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* On-disk store using one file per key under `rootDir`. Writes are atomic
|
|
31
|
+
* via a `.tmp`-then-`rename` pattern, so a crash mid-write can never corrupt
|
|
32
|
+
* a previously-saved value.
|
|
33
|
+
*/
|
|
34
|
+
export declare class FileClientIdStore implements ClientIdStore {
|
|
35
|
+
private readonly rootDir;
|
|
36
|
+
constructor(rootDir: string);
|
|
37
|
+
load(key: string): Promise<string | undefined>;
|
|
38
|
+
save(key: string, clientId: string): Promise<void>;
|
|
39
|
+
private fileFor;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=client-id-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-id-store.d.ts","sourceRoot":"","sources":["../src/client-id-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC/C,kEAAkE;IAClE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAExC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAI9C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhE;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,MAAM;IAEtC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAc9C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/D,OAAO,CAAC,OAAO;CAQhB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the AHP `clientId`. Per the AHP spec, the client owns
|
|
3
|
+
* its identifier — `InitializeParams.clientId` is supplied by the client
|
|
4
|
+
* on every connect. {@link AhpClientSocket} mints a fresh UUID via
|
|
5
|
+
* `randomUUID()` the first time it sees an empty store, persists it here,
|
|
6
|
+
* and replays the same id on every subsequent connect so the host can
|
|
7
|
+
* resume in-flight subscriptions.
|
|
8
|
+
*
|
|
9
|
+
* Keyed by an opaque string so a single store can serve multiple
|
|
10
|
+
* connections (e.g., one `ClientIdStore` per `MultiHostClient` keyed
|
|
11
|
+
* by host id).
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
/**
|
|
17
|
+
* In-memory store. Suitable for tests and for the lifetime of a single
|
|
18
|
+
* process where persistence across restarts is not required.
|
|
19
|
+
*/
|
|
20
|
+
export class InMemoryClientIdStore {
|
|
21
|
+
entries = new Map();
|
|
22
|
+
async load(key) {
|
|
23
|
+
return this.entries.get(key);
|
|
24
|
+
}
|
|
25
|
+
async save(key, clientId) {
|
|
26
|
+
this.entries.set(key, clientId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* On-disk store using one file per key under `rootDir`. Writes are atomic
|
|
31
|
+
* via a `.tmp`-then-`rename` pattern, so a crash mid-write can never corrupt
|
|
32
|
+
* a previously-saved value.
|
|
33
|
+
*/
|
|
34
|
+
export class FileClientIdStore {
|
|
35
|
+
rootDir;
|
|
36
|
+
constructor(rootDir) {
|
|
37
|
+
this.rootDir = rootDir;
|
|
38
|
+
}
|
|
39
|
+
async load(key) {
|
|
40
|
+
try {
|
|
41
|
+
const data = await readFile(this.fileFor(key), "utf8");
|
|
42
|
+
const trimmed = data.trim();
|
|
43
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// ENOENT is the only expected error; anything else is a real problem.
|
|
47
|
+
if (err.code === "ENOENT") {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async save(key, clientId) {
|
|
54
|
+
const target = this.fileFor(key);
|
|
55
|
+
const tmp = `${target}.${randomUUID()}.tmp`;
|
|
56
|
+
await mkdir(dirname(target), { recursive: true });
|
|
57
|
+
await writeFile(tmp, clientId, "utf8");
|
|
58
|
+
await rename(tmp, target);
|
|
59
|
+
}
|
|
60
|
+
fileFor(key) {
|
|
61
|
+
// Sanitize: only allow [A-Za-z0-9._-]. Other characters are URL-encoded.
|
|
62
|
+
const safe = key.replace(/[^A-Za-z0-9._-]/g, (c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
63
|
+
return join(this.rootDir, `${safe}.clientid`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=client-id-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-id-store.js","sourceRoot":"","sources":["../src/client-id-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAUzC;;;GAGG;AACH,MAAM,OAAO,qBAAqB;IACf,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE9C,KAAK,CAAC,IAAI,CAAC,GAAW;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,QAAgB;QAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,iBAAiB;IACQ;IAApC,YAAoC,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;IAEhD,KAAK,CAAC,IAAI,CAAC,GAAW;QAC3B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;YACvD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;QAClD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,sEAAsE;YACtE,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,QAAgB;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,UAAU,EAAE,MAAM,CAAC;QAC5C,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QACvC,MAAM,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC5B,CAAC;IAEO,OAAO,CAAC,GAAW;QACzB,yEAAyE;QACzE,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CACtB,kBAAkB,EAClB,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC3D,CAAC;QACF,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,WAAW,CAAC,CAAC;IAChD,CAAC;CACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local transport-layer errors. These are NEVER sent on the wire — they are
|
|
3
|
+
* thrown / used to reject pending promises inside this package when a request
|
|
4
|
+
* cannot complete (connection lost, timed out, etc.).
|
|
5
|
+
*
|
|
6
|
+
* On-the-wire JSON-RPC + AHP error codes live in `@grackle-ai/ahp`'s
|
|
7
|
+
* `JsonRpcErrorCodes` / `AhpErrorCodes` (spec-defined integers).
|
|
8
|
+
*/
|
|
9
|
+
/** Discriminator for {@link TransportError}. */
|
|
10
|
+
export type TransportErrorKind =
|
|
11
|
+
/** A pending request was abandoned because the underlying socket closed. */
|
|
12
|
+
"connection-lost"
|
|
13
|
+
/** A pending request exceeded its `requestTimeoutMs`. */
|
|
14
|
+
| "request-timeout"
|
|
15
|
+
/** Authentication was rejected by the host (HTTP 401 on upgrade, or close-code 4401). */
|
|
16
|
+
| "auth-failed"
|
|
17
|
+
/** Peer sent a binary frame; we close 1003 and abort. */
|
|
18
|
+
| "binary-frame"
|
|
19
|
+
/** Peer sent a non-`initialize` request before the handshake completed. */
|
|
20
|
+
| "not-initialized"
|
|
21
|
+
/** Peer sent `initialize` twice on the same connection. */
|
|
22
|
+
| "already-initialized"
|
|
23
|
+
/** Operation attempted after the caller invoked `.close()`. */
|
|
24
|
+
| "user-closed";
|
|
25
|
+
/** Error thrown / used to reject promises for transport-internal failures. */
|
|
26
|
+
export declare class TransportError extends Error {
|
|
27
|
+
readonly kind: TransportErrorKind;
|
|
28
|
+
constructor(kind: TransportErrorKind, message: string);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* WebSocket close codes used by this package. The 4xxx range is the
|
|
32
|
+
* application-defined range per [RFC 6455 §7.4.2](https://www.rfc-editor.org/rfc/rfc6455#section-7.4.2).
|
|
33
|
+
*/
|
|
34
|
+
export declare const WsCloseCode: {
|
|
35
|
+
/** Normal closure initiated by either peer. */
|
|
36
|
+
readonly Normal: 1000;
|
|
37
|
+
/** Peer sent a binary frame; we don't speak binary. */
|
|
38
|
+
readonly UnsupportedData: 1003;
|
|
39
|
+
/** Heartbeat: 2 consecutive missed pongs. */
|
|
40
|
+
readonly HeartbeatTimeout: 4001;
|
|
41
|
+
/** Auth rejected mid-session (rare; pre-upgrade auth covers most cases). */
|
|
42
|
+
readonly AuthRejected: 4401;
|
|
43
|
+
};
|
|
44
|
+
export type WsCloseCode = (typeof WsCloseCode)[keyof typeof WsCloseCode];
|
|
45
|
+
//# sourceMappingURL=error-codes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gDAAgD;AAChD,MAAM,MAAM,kBAAkB;AAC5B,4EAA4E;AAC1E,iBAAiB;AACnB,yDAAyD;GACvD,iBAAiB;AACnB,yFAAyF;GACvF,aAAa;AACf,yDAAyD;GACvD,cAAc;AAChB,2EAA2E;GACzE,iBAAiB;AACnB,2DAA2D;GACzD,qBAAqB;AACvB,+DAA+D;GAC7D,aAAa,CAAC;AAElB,8EAA8E;AAC9E,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,kBAAkB,CAAC;gBAEtB,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM;CAK7D;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW;IACtB,+CAA+C;;IAE/C,uDAAuD;;IAEvD,6CAA6C;;IAE7C,4EAA4E;;CAEpE,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local transport-layer errors. These are NEVER sent on the wire — they are
|
|
3
|
+
* thrown / used to reject pending promises inside this package when a request
|
|
4
|
+
* cannot complete (connection lost, timed out, etc.).
|
|
5
|
+
*
|
|
6
|
+
* On-the-wire JSON-RPC + AHP error codes live in `@grackle-ai/ahp`'s
|
|
7
|
+
* `JsonRpcErrorCodes` / `AhpErrorCodes` (spec-defined integers).
|
|
8
|
+
*/
|
|
9
|
+
/** Error thrown / used to reject promises for transport-internal failures. */
|
|
10
|
+
export class TransportError extends Error {
|
|
11
|
+
kind;
|
|
12
|
+
constructor(kind, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "TransportError";
|
|
15
|
+
this.kind = kind;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* WebSocket close codes used by this package. The 4xxx range is the
|
|
20
|
+
* application-defined range per [RFC 6455 §7.4.2](https://www.rfc-editor.org/rfc/rfc6455#section-7.4.2).
|
|
21
|
+
*/
|
|
22
|
+
export const WsCloseCode = {
|
|
23
|
+
/** Normal closure initiated by either peer. */
|
|
24
|
+
Normal: 1000,
|
|
25
|
+
/** Peer sent a binary frame; we don't speak binary. */
|
|
26
|
+
UnsupportedData: 1003,
|
|
27
|
+
/** Heartbeat: 2 consecutive missed pongs. */
|
|
28
|
+
HeartbeatTimeout: 4001,
|
|
29
|
+
/** Auth rejected mid-session (rare; pre-upgrade auth covers most cases). */
|
|
30
|
+
AuthRejected: 4401,
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=error-codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmBH,8EAA8E;AAC9E,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvB,IAAI,CAAqB;IAEzC,YAAmB,IAAwB,EAAE,OAAe;QAC1D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,+CAA+C;IAC/C,MAAM,EAAE,IAAI;IACZ,uDAAuD;IACvD,eAAe,EAAE,IAAI;IACrB,6CAA6C;IAC7C,gBAAgB,EAAE,IAAI;IACtB,4EAA4E;IAC5E,YAAY,EAAE,IAAI;CACV,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Spawn an echo subscriber: a server that emits `count` `action`
|
|
19
|
+
* notifications to any client that issues a `subscribe` JSON-RPC request,
|
|
20
|
+
* paired with a client that connects, subscribes, and returns the
|
|
21
|
+
* collected actions.
|
|
22
|
+
*
|
|
23
|
+
* Returns a `dispose()` that tears everything down.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runEchoSubscriber(count: number): Promise<{
|
|
26
|
+
received: unknown[];
|
|
27
|
+
dispose: () => Promise<void>;
|
|
28
|
+
}>;
|
|
29
|
+
//# sourceMappingURL=echo-subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"echo-subscriber.d.ts","sourceRoot":"","sources":["../../src/examples/echo-subscriber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAqBH;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9D,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC,CA4ED"}
|