@vhyxvoid/agent 1.0.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 (37) hide show
  1. package/dist/AgentClient.d.ts +70 -0
  2. package/dist/AgentClient.d.ts.map +1 -0
  3. package/dist/AgentClient.js +261 -0
  4. package/dist/AgentClient.js.map +1 -0
  5. package/dist/batcher/MessageBatcher.d.ts +40 -0
  6. package/dist/batcher/MessageBatcher.d.ts.map +1 -0
  7. package/dist/batcher/MessageBatcher.js +86 -0
  8. package/dist/batcher/MessageBatcher.js.map +1 -0
  9. package/dist/cache/ResponseCache.d.ts +50 -0
  10. package/dist/cache/ResponseCache.d.ts.map +1 -0
  11. package/dist/cache/ResponseCache.js +127 -0
  12. package/dist/cache/ResponseCache.js.map +1 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +106 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/discovery/LocalDiscoveryServer.d.ts +24 -0
  18. package/dist/discovery/LocalDiscoveryServer.d.ts.map +1 -0
  19. package/dist/discovery/LocalDiscoveryServer.js +58 -0
  20. package/dist/discovery/LocalDiscoveryServer.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +21 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/proxy/BackendProxy.d.ts +19 -0
  26. package/dist/proxy/BackendProxy.d.ts.map +1 -0
  27. package/dist/proxy/BackendProxy.js +122 -0
  28. package/dist/proxy/BackendProxy.js.map +1 -0
  29. package/dist/queue/DurableQueue.d.ts +37 -0
  30. package/dist/queue/DurableQueue.d.ts.map +1 -0
  31. package/dist/queue/DurableQueue.js +142 -0
  32. package/dist/queue/DurableQueue.js.map +1 -0
  33. package/dist/replay/replayQueue.d.ts +11 -0
  34. package/dist/replay/replayQueue.d.ts.map +1 -0
  35. package/dist/replay/replayQueue.js +58 -0
  36. package/dist/replay/replayQueue.js.map +1 -0
  37. package/package.json +39 -0
@@ -0,0 +1,70 @@
1
+ export type AgentState = "IDLE" | "CONNECTING" | "AUTHENTICATING" | "CONNECTED" | "RECONNECTING" | "STOPPED";
2
+ export interface AgentConfig {
3
+ /** Hub WebSocket URL e.g. wss://hub.yourplatform.com/agent */
4
+ hubUrl: string;
5
+ /** API key public ID e.g. vhyxvoid_dev_abc123 */
6
+ keyId: string;
7
+ /**
8
+ * secretHash = HMAC-SHA256(rawSecret, SERVER_HMAC_PEPPER).
9
+ * The CLI hashes the raw secret on startup so it's never kept in memory.
10
+ */
11
+ secretHash: string;
12
+ /** Tunnel label — identifies this agent if an account has multiple agents */
13
+ label: string;
14
+ /** Local backend port to forward requests to */
15
+ port: number;
16
+ /** npm package version — sent to hub for compatibility checking */
17
+ agentVersion: string;
18
+ /** SQLite queue file path (default: ~/.vhyxvoid/queue.db) */
19
+ queuePath?: string;
20
+ /** Enable local discovery HTTP server on port 4242 (default: true) */
21
+ localDiscovery?: boolean;
22
+ /** Called whenever the agent state changes */
23
+ onStateChange?: (state: AgentState) => void;
24
+ /** Called on each log event — defaults to console */
25
+ logger?: {
26
+ info: (obj: object, msg?: string) => void;
27
+ warn: (obj: object, msg?: string) => void;
28
+ error: (obj: object, msg?: string) => void;
29
+ };
30
+ }
31
+ export declare class AgentClient {
32
+ private readonly config;
33
+ private state;
34
+ private ws;
35
+ private agentId;
36
+ private reconnectDelay;
37
+ private reconnectTimer;
38
+ private stopped;
39
+ private readonly queue;
40
+ private readonly proxy;
41
+ private readonly batcher;
42
+ private readonly discovery;
43
+ private readonly log;
44
+ constructor(config: AgentConfig);
45
+ start(): void;
46
+ stop(): void;
47
+ getState(): AgentState;
48
+ getQueueStatus(): {
49
+ ready: number;
50
+ pending: number;
51
+ deadLetter: number;
52
+ };
53
+ getCacheStats(): {
54
+ size: number;
55
+ totalHits: number;
56
+ };
57
+ private connect;
58
+ private onOpen;
59
+ private onMessage;
60
+ private onRegistered;
61
+ private onPing;
62
+ private onHubError;
63
+ private onForward;
64
+ private onClose;
65
+ private onError;
66
+ private scheduleReconnect;
67
+ private sendRaw;
68
+ private setState;
69
+ }
70
+ //# sourceMappingURL=AgentClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentClient.d.ts","sourceRoot":"","sources":["../src/AgentClient.ts"],"names":[],"mappings":"AA+BA,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,YAAY,GACZ,gBAAgB,GAChB,WAAW,GACX,cAAc,GACd,SAAS,CAAC;AAId,MAAM,WAAW,WAAW;IAC1B,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,qDAAqD;IACrD,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAC1C,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAC1C,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;KAC5C,CAAC;CACH;AAED,qBAAa,WAAW;IAcV,OAAO,CAAC,QAAQ,CAAC,MAAM;IAbnC,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,cAAc,CAAuC;IAC7D,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,OAAO,CAAkB;IAEjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqC;gBAE5B,MAAM,EAAE,WAAW;IAqChD,KAAK,IAAI,IAAI;IAUb,IAAI,IAAI,IAAI;IAWZ,QAAQ,IAAI,UAAU;IAGtB,cAAc;;;;;IAGd,aAAa;;;;IAMb,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,MAAM;IAkCd,OAAO,CAAC,SAAS;YAkCH,YAAY;IA2B1B,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,UAAU;YAkBJ,SAAS;IA4BvB,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,QAAQ;CAKjB"}
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ // packages/agent/src/AgentClient.ts
3
+ // Main agent class. State machine with reconnect. Single WS connection.
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.AgentClient = void 0;
9
+ const ws_1 = __importDefault(require("ws"));
10
+ const crypto_1 = require("crypto");
11
+ const path_1 = __importDefault(require("path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const protocol_1 = require("@vhyxvoid/protocol");
15
+ const DurableQueue_1 = require("./queue/DurableQueue");
16
+ const BackendProxy_1 = require("./proxy/BackendProxy");
17
+ const MessageBatcher_1 = require("./batcher/MessageBatcher");
18
+ const replayQueue_1 = require("./replay/replayQueue");
19
+ const LocalDiscoveryServer_1 = require("./discovery/LocalDiscoveryServer");
20
+ class AgentClient {
21
+ config;
22
+ state = "IDLE";
23
+ ws = null;
24
+ agentId = null;
25
+ reconnectDelay = protocol_1.TIMING.RECONNECT_INITIAL_MS;
26
+ reconnectTimer = null;
27
+ stopped = false;
28
+ queue;
29
+ proxy;
30
+ batcher;
31
+ discovery;
32
+ log;
33
+ constructor(config) {
34
+ this.config = config;
35
+ this.log = config.logger ?? {
36
+ info: (obj, msg) => console.info(msg ?? "", obj),
37
+ warn: (obj, msg) => console.warn(msg ?? "", obj),
38
+ error: (obj, msg) => console.error(msg ?? "", obj),
39
+ };
40
+ // Ensure queue directory exists
41
+ const queuePath = config.queuePath ?? path_1.default.join(os_1.default.homedir(), ".vhyxvoid", "queue.db");
42
+ fs_1.default.mkdirSync(path_1.default.dirname(queuePath), { recursive: true });
43
+ this.queue = new DurableQueue_1.DurableQueue(queuePath);
44
+ this.proxy = new BackendProxy_1.BackendProxy(config.port);
45
+ this.batcher = new MessageBatcher_1.MessageBatcher(
46
+ // onFlush: send over WS
47
+ (data) => this.sendRaw(data),
48
+ // onQueueFallback: WS is down, persist to durable queue
49
+ (msg) => this.queue.enqueueOutbound(msg),
50
+ // isConnected: checked before every flush
51
+ () => this.state === "CONNECTED");
52
+ const useDiscovery = config.localDiscovery !== false;
53
+ this.discovery = useDiscovery
54
+ ? new LocalDiscoveryServer_1.LocalDiscoveryServer({
55
+ agentVersion: config.agentVersion,
56
+ label: config.label,
57
+ port: config.port,
58
+ accountIdHash: "", // populated after hub:registered
59
+ })
60
+ : null;
61
+ }
62
+ // ── Public API ──────────────────────────────────────────────────────────────
63
+ start() {
64
+ if (this.state !== "IDLE") {
65
+ throw new Error("AgentClient already started. Create a new instance to restart.");
66
+ }
67
+ this.discovery?.start();
68
+ this.connect();
69
+ }
70
+ stop() {
71
+ this.stopped = true;
72
+ this.setState("STOPPED");
73
+ this.batcher.flushToQueue(); // save buffered messages to disk
74
+ if (this.reconnectTimer)
75
+ clearTimeout(this.reconnectTimer);
76
+ this.ws?.close(1000, "agent_stopped");
77
+ this.proxy.stop();
78
+ this.discovery?.stop();
79
+ this.queue.close();
80
+ }
81
+ getState() {
82
+ return this.state;
83
+ }
84
+ getQueueStatus() {
85
+ return this.queue.count();
86
+ }
87
+ getCacheStats() {
88
+ return this.proxy.getCacheStats();
89
+ }
90
+ // ── WS connection lifecycle ─────────────────────────────────────────────────
91
+ connect() {
92
+ if (this.stopped)
93
+ return;
94
+ this.setState("CONNECTING");
95
+ const ws = new ws_1.default(this.config.hubUrl, {
96
+ perMessageDeflate: true,
97
+ handshakeTimeout: 10_000,
98
+ });
99
+ this.ws = ws;
100
+ ws.on("open", () => this.onOpen());
101
+ ws.on("message", (data) => this.onMessage(data));
102
+ ws.on("close", (code, reason) => this.onClose(code, reason.toString()));
103
+ ws.on("error", (err) => this.onError(err));
104
+ }
105
+ onOpen() {
106
+ this.setState("AUTHENTICATING");
107
+ this.reconnectDelay = protocol_1.TIMING.RECONNECT_INITIAL_MS; // reset backoff on success
108
+ const now = Date.now();
109
+ const requestId = (0, crypto_1.randomUUID)();
110
+ const canonical = (0, protocol_1.buildCanonical)({
111
+ method: "AGENT_REGISTER",
112
+ path: "/agent/register",
113
+ query: "",
114
+ body: this.config.label,
115
+ requestId,
116
+ ts: now,
117
+ });
118
+ const msg = {
119
+ v: protocol_1.PROTOCOL_VERSION,
120
+ type: "agent:register",
121
+ keyId: this.config.keyId,
122
+ label: this.config.label,
123
+ requestId,
124
+ ts: now,
125
+ signature: (0, protocol_1.signCanonical)(canonical, this.config.secretHash),
126
+ agentVersion: this.config.agentVersion,
127
+ };
128
+ this.sendRaw((0, protocol_1.serialize)(msg));
129
+ this.log.info({ label: this.config.label, keyId: this.config.keyId }, "[agent] authenticating with hub");
130
+ }
131
+ onMessage(data) {
132
+ let msg;
133
+ try {
134
+ msg = (0, protocol_1.parseMessage)(data);
135
+ }
136
+ catch (err) {
137
+ if (err instanceof protocol_1.ProtocolError) {
138
+ this.log.warn({
139
+ code: err.code,
140
+ message: `Local backend on port ${this.config.port} is not responding: ${err.message}`,
141
+ }, // ← this is fine, err IS ProtocolError here
142
+ "[agent] protocol error from hub");
143
+ }
144
+ return;
145
+ }
146
+ switch (msg.type) {
147
+ case "hub:registered":
148
+ return void this.onRegistered(msg);
149
+ case "hub:ping":
150
+ return this.onPing(msg);
151
+ case "hub:error":
152
+ return this.onHubError(msg);
153
+ case "tunnel:forward":
154
+ return void this.onForward(msg);
155
+ default:
156
+ this.log.warn({ type: msg.type }, "[agent] unknown message type from hub");
157
+ }
158
+ }
159
+ async onRegistered(msg) {
160
+ this.agentId = msg.agentId;
161
+ this.setState("CONNECTED");
162
+ this.log.info({
163
+ agentId: msg.agentId,
164
+ accountId: msg.accountId,
165
+ label: this.config.label,
166
+ port: this.config.port,
167
+ }, "[agent] ✅ tunnel is live");
168
+ // Replay durable queue AFTER state = CONNECTED so sendRaw works
169
+ const counts = this.queue.count();
170
+ if (counts.ready > 0) {
171
+ this.log.info({ items: counts.ready }, "[agent] replaying queue");
172
+ const result = await (0, replayQueue_1.replayQueue)(this.queue, (data) => this.sendRaw(data), this.proxy);
173
+ this.log.info(result, "[agent] queue replay complete");
174
+ }
175
+ }
176
+ onPing(msg) {
177
+ if (!this.agentId)
178
+ return;
179
+ const pong = {
180
+ v: protocol_1.PROTOCOL_VERSION,
181
+ type: "agent:pong",
182
+ agentId: this.agentId,
183
+ ts: Date.now(),
184
+ };
185
+ this.batcher.add(pong);
186
+ }
187
+ onHubError(msg) {
188
+ this.log.error({ code: msg.code, message: msg.message, fatal: msg.fatal }, "[agent] hub error");
189
+ if (msg.fatal) {
190
+ if (msg.code === "AUTH_FAILED" || msg.code === "VERSION_UNSUPPORTED") {
191
+ console.error(`\n❌ Fatal error: ${msg.message}\n` +
192
+ ` Check your API key and agent version, then restart.\n`);
193
+ this.stop();
194
+ }
195
+ // AGENT_LIMIT_REACHED → allow reconnect — user may free up a slot
196
+ }
197
+ }
198
+ async onForward(msg) {
199
+ // Invalidate related cache entries on mutating requests
200
+ this.proxy.invalidateCacheFor(msg.method, msg.path);
201
+ try {
202
+ const result = await this.proxy.forward(msg);
203
+ const response = {
204
+ v: protocol_1.PROTOCOL_VERSION,
205
+ type: "tunnel:response",
206
+ requestId: msg.requestId,
207
+ ...result,
208
+ };
209
+ this.batcher.add(response);
210
+ }
211
+ catch (err) {
212
+ // Never leave a request unanswered — always send an error back
213
+ const agentError = {
214
+ v: protocol_1.PROTOCOL_VERSION,
215
+ type: "tunnel:agent-error",
216
+ requestId: msg.requestId,
217
+ code: "BACKEND_UNAVAILABLE",
218
+ message: `Local backend on port ${this.config.port} is not responding: ${err.message}`,
219
+ };
220
+ // If WS is up, batcher sends it; if down, it goes to durable queue
221
+ this.batcher.add(agentError);
222
+ }
223
+ }
224
+ onClose(code, reason) {
225
+ if (this.stopped)
226
+ return;
227
+ this.log.warn({ code, reason }, "[agent] WS closed — scheduling reconnect");
228
+ this.batcher.flushToQueue(); // save in-memory buffer to SQLite
229
+ this.setState("RECONNECTING");
230
+ this.scheduleReconnect();
231
+ }
232
+ onError(err) {
233
+ // WS error is always followed by close — just log
234
+ this.log.warn({ message: err.message }, "[agent] WS error");
235
+ }
236
+ scheduleReconnect() {
237
+ if (this.stopped)
238
+ return;
239
+ const delay = this.reconnectDelay;
240
+ this.reconnectDelay = Math.min(delay * 2, protocol_1.TIMING.RECONNECT_MAX_MS);
241
+ this.log.info({ delayMs: delay }, "[agent] reconnecting");
242
+ this.reconnectTimer = setTimeout(() => {
243
+ this.reconnectTimer = null;
244
+ this.connect();
245
+ }, delay);
246
+ }
247
+ // ── Helpers ─────────────────────────────────────────────────────────────────
248
+ sendRaw(data) {
249
+ if (this.ws?.readyState === ws_1.default.OPEN) {
250
+ this.ws.send(data);
251
+ }
252
+ }
253
+ setState(state) {
254
+ if (this.state === state)
255
+ return;
256
+ this.state = state;
257
+ this.config.onStateChange?.(state);
258
+ }
259
+ }
260
+ exports.AgentClient = AgentClient;
261
+ //# sourceMappingURL=AgentClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentClient.js","sourceRoot":"","sources":["../src/AgentClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { TunnelResponseMsg, TunnelAgentErrorMsg, AgentPongMsg } from "@vhyxvoid/protocol";
2
+ export type BatchableMsg = TunnelResponseMsg | TunnelAgentErrorMsg | AgentPongMsg;
3
+ type FlushFn = (data: string) => void;
4
+ type QueueFallback = (msg: TunnelResponseMsg | TunnelAgentErrorMsg) => void;
5
+ type IsConnected = () => boolean;
6
+ export declare class MessageBatcher {
7
+ /** Called with serialized data when WS is available */
8
+ private readonly onFlush;
9
+ /** Called with individual messages when WS is down — routes to DurableQueue */
10
+ private readonly onQueueFallback;
11
+ /** Returns true when WS connection is CONNECTED */
12
+ private readonly isConnected;
13
+ private buffer;
14
+ private timer;
15
+ constructor(
16
+ /** Called with serialized data when WS is available */
17
+ onFlush: FlushFn,
18
+ /** Called with individual messages when WS is down — routes to DurableQueue */
19
+ onQueueFallback: QueueFallback,
20
+ /** Returns true when WS connection is CONNECTED */
21
+ isConnected: IsConnected);
22
+ /** Add a message to the batch. Flushes immediately if MAX_BATCH_SIZE reached. */
23
+ add(msg: BatchableMsg): void;
24
+ /**
25
+ * Flush the buffer.
26
+ * - If WS is connected: send as batch (or single message if only one item)
27
+ * - If WS is down: route non-pong messages to DurableQueue
28
+ */
29
+ flush(): void;
30
+ /**
31
+ * Called on WS close BEFORE reconnect starts.
32
+ * Moves everything currently in the buffer to DurableQueue.
33
+ * Pongs are discarded (they are point-in-time, not meaningful after disconnect).
34
+ */
35
+ flushToQueue(): void;
36
+ /** Number of messages currently buffered (for diagnostics). */
37
+ size(): number;
38
+ }
39
+ export {};
40
+ //# sourceMappingURL=MessageBatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageBatcher.d.ts","sourceRoot":"","sources":["../../src/batcher/MessageBatcher.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EAIb,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,YAAY,GACpB,iBAAiB,GACjB,mBAAmB,GACnB,YAAY,CAAC;AAEjB,KAAK,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AACtC,KAAK,aAAa,GAAG,CAAC,GAAG,EAAE,iBAAiB,GAAG,mBAAmB,KAAK,IAAI,CAAC;AAC5E,KAAK,WAAW,GAAG,MAAM,OAAO,CAAC;AAEjC,qBAAa,cAAc;IAKvB,uDAAuD;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,mDAAmD;IACnD,OAAO,CAAC,QAAQ,CAAC,WAAW;IAT9B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAA+B;;IAG1C,uDAAuD;IACtC,OAAO,EAAE,OAAO;IACjC,+EAA+E;IAC9D,eAAe,EAAE,aAAa;IAC/C,mDAAmD;IAClC,WAAW,EAAE,WAAW;IAG3C,iFAAiF;IACjF,GAAG,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAU5B;;;;OAIG;IACH,KAAK,IAAI,IAAI;IA6Bb;;;;OAIG;IACH,YAAY,IAAI,IAAI;IAcpB,+DAA+D;IAC/D,IAAI,IAAI,MAAM;CAGf"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ // packages/agent/src/batcher/MessageBatcher.ts
3
+ // In-memory only. Never written to disk. Never persisted.
4
+ // If WS drops while items are buffered → flush() routes to DurableQueue.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MessageBatcher = void 0;
7
+ const protocol_1 = require("@vhyxvoid/protocol");
8
+ class MessageBatcher {
9
+ onFlush;
10
+ onQueueFallback;
11
+ isConnected;
12
+ buffer = [];
13
+ timer = null;
14
+ constructor(
15
+ /** Called with serialized data when WS is available */
16
+ onFlush,
17
+ /** Called with individual messages when WS is down — routes to DurableQueue */
18
+ onQueueFallback,
19
+ /** Returns true when WS connection is CONNECTED */
20
+ isConnected) {
21
+ this.onFlush = onFlush;
22
+ this.onQueueFallback = onQueueFallback;
23
+ this.isConnected = isConnected;
24
+ }
25
+ /** Add a message to the batch. Flushes immediately if MAX_BATCH_SIZE reached. */
26
+ add(msg) {
27
+ this.buffer.push(msg);
28
+ if (this.buffer.length >= protocol_1.TIMING.BATCH_MAX_SIZE) {
29
+ this.flush();
30
+ }
31
+ else if (!this.timer) {
32
+ this.timer = setTimeout(() => this.flush(), protocol_1.TIMING.BATCH_WINDOW_MS);
33
+ }
34
+ }
35
+ /**
36
+ * Flush the buffer.
37
+ * - If WS is connected: send as batch (or single message if only one item)
38
+ * - If WS is down: route non-pong messages to DurableQueue
39
+ */
40
+ flush() {
41
+ if (this.buffer.length === 0)
42
+ return;
43
+ if (this.timer) {
44
+ clearTimeout(this.timer);
45
+ this.timer = null;
46
+ }
47
+ const messages = this.buffer.splice(0);
48
+ if (!this.isConnected()) {
49
+ // WS is down — persist responses/errors, discard pongs (ephemeral)
50
+ for (const msg of messages) {
51
+ if (msg.type !== "agent:pong") {
52
+ this.onQueueFallback(msg);
53
+ }
54
+ }
55
+ return;
56
+ }
57
+ // WS is up — send
58
+ const serialized = messages.length === 1
59
+ ? (0, protocol_1.serialize)(messages[0])
60
+ : (0, protocol_1.serialize)({ v: "1", type: "agent:batch", messages });
61
+ this.onFlush(serialized);
62
+ }
63
+ /**
64
+ * Called on WS close BEFORE reconnect starts.
65
+ * Moves everything currently in the buffer to DurableQueue.
66
+ * Pongs are discarded (they are point-in-time, not meaningful after disconnect).
67
+ */
68
+ flushToQueue() {
69
+ if (this.timer) {
70
+ clearTimeout(this.timer);
71
+ this.timer = null;
72
+ }
73
+ const messages = this.buffer.splice(0);
74
+ for (const msg of messages) {
75
+ if (msg.type !== "agent:pong") {
76
+ this.onQueueFallback(msg);
77
+ }
78
+ }
79
+ }
80
+ /** Number of messages currently buffered (for diagnostics). */
81
+ size() {
82
+ return this.buffer.length;
83
+ }
84
+ }
85
+ exports.MessageBatcher = MessageBatcher;
86
+ //# sourceMappingURL=MessageBatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageBatcher.js","sourceRoot":"","sources":["../../src/batcher/MessageBatcher.ts"],"names":[],"mappings":";AAAA,+CAA+C;AAC/C,0DAA0D;AAC1D,yEAAyE;;;AAEzE,iDAO4B;AAW5B,MAAa,cAAc;IAMN;IAEA;IAEA;IATX,MAAM,GAAmB,EAAE,CAAC;IAC5B,KAAK,GAA0B,IAAI,CAAC;IAE5C;IACE,uDAAuD;IACtC,OAAgB;IACjC,+EAA+E;IAC9D,eAA8B;IAC/C,mDAAmD;IAClC,WAAwB;QAJxB,YAAO,GAAP,OAAO,CAAS;QAEhB,oBAAe,GAAf,eAAe,CAAe;QAE9B,gBAAW,GAAX,WAAW,CAAa;IACxC,CAAC;IAEJ,iFAAiF;IACjF,GAAG,CAAC,GAAiB;QACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEtB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,iBAAM,CAAC,cAAc,EAAE,CAAC;YAChD,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACvB,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,iBAAM,CAAC,eAAe,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK;QACH,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAErC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAEvC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,mEAAmE;YACnE,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;gBAC3B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;oBAC9B,IAAI,CAAC,eAAe,CAAC,GAA8C,CAAC,CAAC;gBACvE,CAAC;YACH,CAAC;YACD,OAAO;QACT,CAAC;QAED,kBAAkB;QAClB,MAAM,UAAU,GACd,QAAQ,CAAC,MAAM,KAAK,CAAC;YACnB,CAAC,CAAC,IAAA,oBAAS,EAAC,QAAQ,CAAC,CAAC,CAAiB,CAAC;YACxC,CAAC,CAAC,IAAA,oBAAS,EAAC,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAmB,CAAC,CAAC;QAE5E,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3B,CAAC;IAED;;;;OAIG;IACH,YAAY;QACV,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC9B,IAAI,CAAC,eAAe,CAAC,GAA8C,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,IAAI;QACF,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;CACF;AAjFD,wCAiFC"}
@@ -0,0 +1,50 @@
1
+ export interface CachedResponse {
2
+ status: number;
3
+ headers: Record<string, string>;
4
+ body: string | null;
5
+ durationMs: number;
6
+ cachedAt: number;
7
+ expiresAt: number;
8
+ hits: number;
9
+ }
10
+ export declare class ResponseCache {
11
+ private readonly store;
12
+ private readonly MAX_ENTRIES;
13
+ /**
14
+ * Try to get a cached response.
15
+ * Returns null on miss or if method is not GET.
16
+ */
17
+ get(method: string, path: string, query: string): CachedResponse | null;
18
+ /**
19
+ * Store a response if the backend's Cache-Control header allows it.
20
+ * Does nothing for non-GET methods or non-cacheable responses.
21
+ */
22
+ set(method: string, path: string, query: string, response: Omit<CachedResponse, "cachedAt" | "expiresAt" | "hits">): void;
23
+ /**
24
+ * Invalidate all cached entries for a path prefix.
25
+ * Call after any POST/PUT/PATCH/DELETE to related resources.
26
+ */
27
+ invalidatePrefix(pathPrefix: string): number;
28
+ /** Remove all expired entries. Called periodically. */
29
+ evictExpired(): number;
30
+ stats(): {
31
+ size: number;
32
+ totalHits: number;
33
+ };
34
+ clear(): void;
35
+ private buildKey;
36
+ /**
37
+ * Parse max-age seconds from a Cache-Control header value.
38
+ * Returns 0 if no caching is allowed.
39
+ *
40
+ * Examples:
41
+ * "max-age=300" → 300
42
+ * "public, max-age=60" → 60
43
+ * "no-cache" → 0
44
+ * "no-store" → 0
45
+ * "private, max-age=120" → 0 (private = don't cache in shared proxy)
46
+ * "" → 0
47
+ */
48
+ private parseMaxAge;
49
+ }
50
+ //# sourceMappingURL=ResponseCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResponseCache.d.ts","sourceRoot":"","sources":["../../src/cache/ResponseCache.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAG3D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAO;IAEnC;;;OAGG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAkBvE;;;OAGG;IACH,GAAG,CACD,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,UAAU,GAAG,WAAW,GAAG,MAAM,CAAC,GAChE,IAAI;IA0BP;;;OAGG;IACH,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAW5C,uDAAuD;IACvD,YAAY,IAAI,MAAM;IAYtB,KAAK,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAM5C,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,QAAQ;IAIhB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,WAAW;CAmBpB"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ // packages/agent/src/cache/ResponseCache.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ResponseCache = void 0;
5
+ class ResponseCache {
6
+ store = new Map();
7
+ // Evict entries when cache exceeds this size (LRU-lite: evict oldest)
8
+ MAX_ENTRIES = 500;
9
+ /**
10
+ * Try to get a cached response.
11
+ * Returns null on miss or if method is not GET.
12
+ */
13
+ get(method, path, query) {
14
+ // Only cache GET requests — POST/PUT/DELETE are never safe to cache
15
+ if (method.toUpperCase() !== "GET")
16
+ return null;
17
+ const key = this.buildKey(path, query);
18
+ const cached = this.store.get(key);
19
+ if (!cached)
20
+ return null;
21
+ if (Date.now() > cached.expiresAt) {
22
+ this.store.delete(key);
23
+ return null;
24
+ }
25
+ cached.hits += 1;
26
+ return cached;
27
+ }
28
+ /**
29
+ * Store a response if the backend's Cache-Control header allows it.
30
+ * Does nothing for non-GET methods or non-cacheable responses.
31
+ */
32
+ set(method, path, query, response) {
33
+ if (method.toUpperCase() !== "GET")
34
+ return;
35
+ // Only cache 2xx responses
36
+ if (response.status < 200 || response.status >= 300)
37
+ return;
38
+ const maxAge = this.parseMaxAge(response.headers["cache-control"] ?? "");
39
+ if (maxAge <= 0)
40
+ return; // no-cache, no-store, or no max-age directive
41
+ const now = Date.now();
42
+ const key = this.buildKey(path, query);
43
+ // Evict oldest entry if at capacity
44
+ if (this.store.size >= this.MAX_ENTRIES && !this.store.has(key)) {
45
+ const oldest = this.store.keys().next().value;
46
+ if (oldest)
47
+ this.store.delete(oldest);
48
+ }
49
+ this.store.set(key, {
50
+ ...response,
51
+ cachedAt: now,
52
+ expiresAt: now + maxAge * 1_000,
53
+ hits: 0,
54
+ });
55
+ }
56
+ /**
57
+ * Invalidate all cached entries for a path prefix.
58
+ * Call after any POST/PUT/PATCH/DELETE to related resources.
59
+ */
60
+ invalidatePrefix(pathPrefix) {
61
+ let count = 0;
62
+ for (const key of this.store.keys()) {
63
+ if (key.startsWith(pathPrefix)) {
64
+ this.store.delete(key);
65
+ count++;
66
+ }
67
+ }
68
+ return count;
69
+ }
70
+ /** Remove all expired entries. Called periodically. */
71
+ evictExpired() {
72
+ const now = Date.now();
73
+ let count = 0;
74
+ for (const [key, entry] of this.store) {
75
+ if (now > entry.expiresAt) {
76
+ this.store.delete(key);
77
+ count++;
78
+ }
79
+ }
80
+ return count;
81
+ }
82
+ stats() {
83
+ let totalHits = 0;
84
+ for (const entry of this.store.values())
85
+ totalHits += entry.hits;
86
+ return { size: this.store.size, totalHits };
87
+ }
88
+ clear() {
89
+ this.store.clear();
90
+ }
91
+ // ── Private helpers ─────────────────────────────────────────────────────────
92
+ buildKey(path, query) {
93
+ return query ? `${path}?${query}` : path;
94
+ }
95
+ /**
96
+ * Parse max-age seconds from a Cache-Control header value.
97
+ * Returns 0 if no caching is allowed.
98
+ *
99
+ * Examples:
100
+ * "max-age=300" → 300
101
+ * "public, max-age=60" → 60
102
+ * "no-cache" → 0
103
+ * "no-store" → 0
104
+ * "private, max-age=120" → 0 (private = don't cache in shared proxy)
105
+ * "" → 0
106
+ */
107
+ parseMaxAge(cacheControl) {
108
+ if (!cacheControl)
109
+ return 0;
110
+ const lower = cacheControl.toLowerCase();
111
+ // Directives that mean "do not cache"
112
+ if (lower.includes("no-store") || lower.includes("no-cache"))
113
+ return 0;
114
+ // Private responses should not be cached at the agent level
115
+ // (agent acts as a shared intermediary)
116
+ if (lower.includes("private"))
117
+ return 0;
118
+ // Extract max-age=N
119
+ const match = lower.match(/max-age\s*=\s*(\d+)/);
120
+ if (!match)
121
+ return 0;
122
+ const seconds = parseInt(match[1], 10);
123
+ return isNaN(seconds) ? 0 : seconds;
124
+ }
125
+ }
126
+ exports.ResponseCache = ResponseCache;
127
+ //# sourceMappingURL=ResponseCache.js.map