@masons/runtime-broker 0.1.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/broker/broker-daemon.d.ts +71 -0
  4. package/dist/broker/broker-daemon.d.ts.map +1 -0
  5. package/dist/broker/broker-daemon.js +837 -0
  6. package/dist/broker/claude-code-spawn-driver.d.ts +14 -0
  7. package/dist/broker/claude-code-spawn-driver.d.ts.map +1 -0
  8. package/dist/broker/claude-code-spawn-driver.js +39 -0
  9. package/dist/broker/closed-endpoint-lookup.d.ts +25 -0
  10. package/dist/broker/closed-endpoint-lookup.d.ts.map +1 -0
  11. package/dist/broker/closed-endpoint-lookup.js +59 -0
  12. package/dist/broker/codex-spawn-driver-stub.d.ts +7 -0
  13. package/dist/broker/codex-spawn-driver-stub.d.ts.map +1 -0
  14. package/dist/broker/codex-spawn-driver-stub.js +13 -0
  15. package/dist/broker/connector-ws.d.ts +47 -0
  16. package/dist/broker/connector-ws.d.ts.map +1 -0
  17. package/dist/broker/connector-ws.js +60 -0
  18. package/dist/broker/control-event-dispatcher.d.ts +21 -0
  19. package/dist/broker/control-event-dispatcher.d.ts.map +1 -0
  20. package/dist/broker/control-event-dispatcher.js +45 -0
  21. package/dist/broker/control-event-types.d.ts +28 -0
  22. package/dist/broker/control-event-types.d.ts.map +1 -0
  23. package/dist/broker/control-event-types.js +1 -0
  24. package/dist/broker/correlation-ring.d.ts +10 -0
  25. package/dist/broker/correlation-ring.d.ts.map +1 -0
  26. package/dist/broker/correlation-ring.js +32 -0
  27. package/dist/broker/discovery-file.d.ts +12 -0
  28. package/dist/broker/discovery-file.d.ts.map +1 -0
  29. package/dist/broker/discovery-file.js +77 -0
  30. package/dist/broker/endpoint-registry.d.ts +53 -0
  31. package/dist/broker/endpoint-registry.d.ts.map +1 -0
  32. package/dist/broker/endpoint-registry.js +83 -0
  33. package/dist/broker/endpoint-state-machine.d.ts +40 -0
  34. package/dist/broker/endpoint-state-machine.d.ts.map +1 -0
  35. package/dist/broker/endpoint-state-machine.js +92 -0
  36. package/dist/broker/entry.d.ts +13 -0
  37. package/dist/broker/entry.d.ts.map +1 -0
  38. package/dist/broker/entry.js +235 -0
  39. package/dist/broker/grace-timer.d.ts +9 -0
  40. package/dist/broker/grace-timer.d.ts.map +1 -0
  41. package/dist/broker/grace-timer.js +34 -0
  42. package/dist/broker/ipc-server.d.ts +79 -0
  43. package/dist/broker/ipc-server.d.ts.map +1 -0
  44. package/dist/broker/ipc-server.js +263 -0
  45. package/dist/broker/logger.d.ts +10 -0
  46. package/dist/broker/logger.d.ts.map +1 -0
  47. package/dist/broker/logger.js +34 -0
  48. package/dist/broker/network-presence-changed-event-types.d.ts +8 -0
  49. package/dist/broker/network-presence-changed-event-types.d.ts.map +1 -0
  50. package/dist/broker/network-presence-changed-event-types.js +1 -0
  51. package/dist/broker/network-presence-emitter.d.ts +22 -0
  52. package/dist/broker/network-presence-emitter.d.ts.map +1 -0
  53. package/dist/broker/network-presence-emitter.js +150 -0
  54. package/dist/broker/network-presence.d.ts +31 -0
  55. package/dist/broker/network-presence.d.ts.map +1 -0
  56. package/dist/broker/network-presence.js +109 -0
  57. package/dist/broker/paths.d.ts +11 -0
  58. package/dist/broker/paths.d.ts.map +1 -0
  59. package/dist/broker/paths.js +30 -0
  60. package/dist/broker/plugin-liveness.d.ts +2 -0
  61. package/dist/broker/plugin-liveness.d.ts.map +1 -0
  62. package/dist/broker/plugin-liveness.js +15 -0
  63. package/dist/broker/received-message-correlation-cache.d.ts +23 -0
  64. package/dist/broker/received-message-correlation-cache.d.ts.map +1 -0
  65. package/dist/broker/received-message-correlation-cache.js +114 -0
  66. package/dist/broker/reconnecting-buffer.d.ts +23 -0
  67. package/dist/broker/reconnecting-buffer.d.ts.map +1 -0
  68. package/dist/broker/reconnecting-buffer.js +107 -0
  69. package/dist/broker/routing-table.d.ts +22 -0
  70. package/dist/broker/routing-table.d.ts.map +1 -0
  71. package/dist/broker/routing-table.js +35 -0
  72. package/dist/broker/runtime-endpoint-port.d.ts +20 -0
  73. package/dist/broker/runtime-endpoint-port.d.ts.map +1 -0
  74. package/dist/broker/runtime-endpoint-port.js +1 -0
  75. package/dist/broker/services-event-client.d.ts +21 -0
  76. package/dist/broker/services-event-client.d.ts.map +1 -0
  77. package/dist/broker/services-event-client.js +221 -0
  78. package/dist/broker/spawn-correlation.d.ts +28 -0
  79. package/dist/broker/spawn-correlation.d.ts.map +1 -0
  80. package/dist/broker/spawn-correlation.js +77 -0
  81. package/dist/broker/spawn-driver.d.ts +27 -0
  82. package/dist/broker/spawn-driver.d.ts.map +1 -0
  83. package/dist/broker/spawn-driver.js +15 -0
  84. package/dist/broker/task-hint-handler.d.ts +21 -0
  85. package/dist/broker/task-hint-handler.d.ts.map +1 -0
  86. package/dist/broker/task-hint-handler.js +33 -0
  87. package/dist/broker/transition-state-retry-queue.d.ts +20 -0
  88. package/dist/broker/transition-state-retry-queue.d.ts.map +1 -0
  89. package/dist/broker/transition-state-retry-queue.js +48 -0
  90. package/dist/broker/undispatched-changed-event-types.d.ts +29 -0
  91. package/dist/broker/undispatched-changed-event-types.d.ts.map +1 -0
  92. package/dist/broker/undispatched-changed-event-types.js +14 -0
  93. package/dist/broker/undispatched-emitter.d.ts +22 -0
  94. package/dist/broker/undispatched-emitter.d.ts.map +1 -0
  95. package/dist/broker/undispatched-emitter.js +149 -0
  96. package/dist/broker/undispatched-inbox.d.ts +26 -0
  97. package/dist/broker/undispatched-inbox.d.ts.map +1 -0
  98. package/dist/broker/undispatched-inbox.js +53 -0
  99. package/dist/broker/version-handshake.d.ts +30 -0
  100. package/dist/broker/version-handshake.d.ts.map +1 -0
  101. package/dist/broker/version-handshake.js +47 -0
  102. package/dist/broker-client/broker-client.d.ts +65 -0
  103. package/dist/broker-client/broker-client.d.ts.map +1 -0
  104. package/dist/broker-client/broker-client.js +165 -0
  105. package/dist/broker-client/lazy-spawn.d.ts +18 -0
  106. package/dist/broker-client/lazy-spawn.d.ts.map +1 -0
  107. package/dist/broker-client/lazy-spawn.js +61 -0
  108. package/dist/config-fs.d.ts +4 -0
  109. package/dist/config-fs.d.ts.map +1 -0
  110. package/dist/config-fs.js +23 -0
  111. package/dist/connector-client.d.ts +65 -0
  112. package/dist/connector-client.d.ts.map +1 -0
  113. package/dist/connector-client.js +364 -0
  114. package/dist/environment-context.d.ts +21 -0
  115. package/dist/environment-context.d.ts.map +1 -0
  116. package/dist/environment-context.js +39 -0
  117. package/dist/platform-client.d.ts +84 -0
  118. package/dist/platform-client.d.ts.map +1 -0
  119. package/dist/platform-client.js +94 -0
  120. package/dist/runtime-endpoint-client.d.ts +74 -0
  121. package/dist/runtime-endpoint-client.d.ts.map +1 -0
  122. package/dist/runtime-endpoint-client.js +163 -0
  123. package/dist/types.d.ts +90 -0
  124. package/dist/types.d.ts.map +1 -0
  125. package/dist/types.js +38 -0
  126. package/dist/version.d.ts +2 -0
  127. package/dist/version.d.ts.map +1 -0
  128. package/dist/version.js +1 -0
  129. package/package.json +60 -0
@@ -0,0 +1,263 @@
1
+ import { createServer } from "node:http";
2
+ import { WebSocket, WebSocketServer } from "ws";
3
+ import { PLUGIN_VERSION } from "../version.js";
4
+ import { handleInitialize, IPC_PROTOCOL_VERSION, SERVER_CAPABILITIES, } from "./version-handshake.js";
5
+ export class BrokerHttpError extends Error {
6
+ status;
7
+ code;
8
+ constructor(status, code, message) {
9
+ super(message);
10
+ this.name = "BrokerHttpError";
11
+ this.status = status;
12
+ this.code = code;
13
+ }
14
+ }
15
+ export async function startIPCServer(opts) {
16
+ const { bearerToken, handlers, logger, onChannelOpened, onChannelClosed } = opts;
17
+ const http = createServer((req, res) => {
18
+ void routeHttp(req, res, bearerToken, handlers, logger);
19
+ });
20
+ const wss = new WebSocketServer({ noServer: true });
21
+ http.on("upgrade", (req, socket, head) => {
22
+ if (req.url !== "/v1/stream") {
23
+ socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
24
+ socket.destroy();
25
+ return;
26
+ }
27
+ if (!authorizedRequest(req, bearerToken)) {
28
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
29
+ socket.destroy();
30
+ return;
31
+ }
32
+ const pid = readPluginPid(req);
33
+ if (pid === null) {
34
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
35
+ socket.destroy();
36
+ return;
37
+ }
38
+ wss.handleUpgrade(req, socket, head, (ws) => {
39
+ onChannelOpened?.(pid, ws);
40
+ ws.once("close", () => onChannelClosed?.(pid, ws));
41
+ wss.emit("connection", ws, req);
42
+ });
43
+ });
44
+ await new Promise((resolve, reject) => {
45
+ http.once("error", reject);
46
+ http.listen(0, "127.0.0.1", () => {
47
+ http.off("error", reject);
48
+ resolve();
49
+ });
50
+ });
51
+ const addr = http.address();
52
+ const ipcUrl = `http://127.0.0.1:${addr.port}`;
53
+ logger.info("ipc_server_listening", { ipcUrl });
54
+ return {
55
+ ipcUrl,
56
+ port: addr.port,
57
+ async close() {
58
+ await closeServer(http, wss);
59
+ },
60
+ };
61
+ }
62
+ async function routeHttp(req, res, bearerToken, handlers, logger) {
63
+ const url = req.url ?? "";
64
+ const method = req.method ?? "GET";
65
+ if (method === "GET" && url === "/health") {
66
+ sendJson(res, 200, {
67
+ ok: true,
68
+ version: PLUGIN_VERSION,
69
+ protocol_version: IPC_PROTOCOL_VERSION,
70
+ capabilities: SERVER_CAPABILITIES,
71
+ });
72
+ return;
73
+ }
74
+ if (!authorizedRequest(req, bearerToken)) {
75
+ sendJson(res, 401, { error: "unauthorized" });
76
+ return;
77
+ }
78
+ try {
79
+ if (method === "POST" && url === "/v1/initialize") {
80
+ const body = await readJson(req);
81
+ const result = handleInitialize(body);
82
+ sendJson(res, result.status, result.body);
83
+ return;
84
+ }
85
+ if (method === "POST" && url === "/v1/endpoint/register") {
86
+ const body = await readJson(req);
87
+ const out = await handlers.registerEndpoint(body, null);
88
+ sendJson(res, 200, out);
89
+ return;
90
+ }
91
+ const heartbeatMatch = /^\/v1\/endpoint\/([^/]+)\/heartbeat$/.exec(url);
92
+ if (method === "POST" && heartbeatMatch && heartbeatMatch[1]) {
93
+ await handlers.heartbeatEndpoint(decodeURIComponent(heartbeatMatch[1]));
94
+ sendJson(res, 200, { ok: true });
95
+ return;
96
+ }
97
+ const reattachMatch = /^\/v1\/endpoint\/([^/]+)\/reattach$/.exec(url);
98
+ if (method === "POST" && reattachMatch && reattachMatch[1]) {
99
+ const body = await readJson(req);
100
+ const pid = typeof body.plugin_pid === "number" ? body.plugin_pid : Number.NaN;
101
+ if (!Number.isFinite(pid) || pid <= 0) {
102
+ throw new BrokerHttpError(400, "plugin_pid_invalid", "plugin_pid is required and must be a positive number");
103
+ }
104
+ const out = await handlers.reattachEndpoint(decodeURIComponent(reattachMatch[1]), pid, null);
105
+ sendJson(res, 200, out);
106
+ return;
107
+ }
108
+ const unregisterMatch = /^\/v1\/endpoint\/([^/]+)$/.exec(url);
109
+ if (method === "DELETE" && unregisterMatch && unregisterMatch[1]) {
110
+ await handlers.unregisterEndpoint(decodeURIComponent(unregisterMatch[1]));
111
+ sendJson(res, 200, { ok: true });
112
+ return;
113
+ }
114
+ if (method === "POST" && url === "/v1/send") {
115
+ const body = await readJson(req);
116
+ const out = await handlers.send(body);
117
+ sendJson(res, 200, out);
118
+ return;
119
+ }
120
+ if (method === "GET" && url === "/v1/undispatched") {
121
+ const items = await handlers.listUndispatched();
122
+ sendJson(res, 200, { items });
123
+ return;
124
+ }
125
+ if (method === "POST" && url === "/v1/dispatch") {
126
+ const body = await readJson(req);
127
+ const uid = typeof body.undispatched_id === "string"
128
+ ? body.undispatched_id
129
+ : undefined;
130
+ const tid = typeof body.target_endpoint_id === "string"
131
+ ? body.target_endpoint_id
132
+ : undefined;
133
+ if (!uid || !tid) {
134
+ throw new BrokerHttpError(400, "dispatch_body_invalid", "POST /v1/dispatch requires { undispatched_id, target_endpoint_id }");
135
+ }
136
+ await handlers.dispatch(uid, tid);
137
+ sendJson(res, 200, { ok: true });
138
+ return;
139
+ }
140
+ const taskHintMatch = /^\/v1\/endpoint\/([^/]+)\/task-hint$/.exec(url);
141
+ if (method === "POST" && taskHintMatch && taskHintMatch[1]) {
142
+ const endpointId = decodeURIComponent(taskHintMatch[1]);
143
+ const body = await readJson(req);
144
+ const pid = typeof body.plugin_pid === "number" ? body.plugin_pid : Number.NaN;
145
+ if (!Number.isFinite(pid) || pid <= 0) {
146
+ throw new BrokerHttpError(400, "plugin_pid_invalid", "plugin_pid is required and must be a positive number");
147
+ }
148
+ const hint = typeof body.task_hint === "string" ? body.task_hint : undefined;
149
+ if (hint === undefined) {
150
+ throw new BrokerHttpError(400, "task_hint_invalid", "task_hint is required and must be a string");
151
+ }
152
+ await handlers.setTaskHint({
153
+ endpoint_id: endpointId,
154
+ plugin_pid: pid,
155
+ task_hint: hint,
156
+ });
157
+ sendJson(res, 200, { ok: true });
158
+ return;
159
+ }
160
+ sendJson(res, 404, { error: "not_found", path: url });
161
+ }
162
+ catch (err) {
163
+ if (err instanceof BrokerHttpError) {
164
+ logger.info("ipc_route_rejected", {
165
+ url,
166
+ method,
167
+ status: err.status,
168
+ code: err.code,
169
+ message: err.message,
170
+ });
171
+ sendJson(res, err.status, { error: err.code, message: err.message });
172
+ return;
173
+ }
174
+ logger.error("ipc_route_error", {
175
+ url,
176
+ method,
177
+ err: err instanceof Error ? err.message : String(err),
178
+ });
179
+ sendJson(res, 500, {
180
+ error: "internal_error",
181
+ message: err instanceof Error ? err.message : "unknown",
182
+ });
183
+ }
184
+ }
185
+ function authorizedRequest(req, bearerToken) {
186
+ const header = req.headers.authorization;
187
+ if (typeof header !== "string")
188
+ return false;
189
+ const trimmed = header.startsWith("Bearer ")
190
+ ? header.slice("Bearer ".length)
191
+ : header;
192
+ return constantTimeEquals(trimmed, bearerToken);
193
+ }
194
+ function readPluginPid(req) {
195
+ const raw = req.headers["x-plugin-pid"];
196
+ const str = Array.isArray(raw) ? raw[0] : raw;
197
+ if (!str)
198
+ return null;
199
+ const n = Number.parseInt(str, 10);
200
+ return Number.isFinite(n) && n > 0 ? n : null;
201
+ }
202
+ function constantTimeEquals(a, b) {
203
+ if (a.length !== b.length)
204
+ return false;
205
+ let mismatch = 0;
206
+ for (let i = 0; i < a.length; i++) {
207
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
208
+ }
209
+ return mismatch === 0;
210
+ }
211
+ async function readJson(req) {
212
+ return new Promise((resolve, reject) => {
213
+ const chunks = [];
214
+ let total = 0;
215
+ const MAX = 1 * 1024 * 1024;
216
+ req.on("data", (chunk) => {
217
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
218
+ total += buf.length;
219
+ if (total > MAX) {
220
+ reject(new Error("request body too large"));
221
+ req.destroy();
222
+ return;
223
+ }
224
+ chunks.push(buf);
225
+ });
226
+ req.on("end", () => {
227
+ try {
228
+ const raw = Buffer.concat(chunks).toString("utf8");
229
+ resolve(raw.length === 0 ? {} : JSON.parse(raw));
230
+ }
231
+ catch (err) {
232
+ reject(err instanceof Error ? err : new Error(String(err)));
233
+ }
234
+ });
235
+ req.on("error", reject);
236
+ });
237
+ }
238
+ function sendJson(res, status, body) {
239
+ res.statusCode = status;
240
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
241
+ res.end(JSON.stringify(body));
242
+ }
243
+ async function closeServer(http, wss) {
244
+ await new Promise((resolve) => {
245
+ wss.close(() => resolve());
246
+ });
247
+ await new Promise((resolve, reject) => {
248
+ http.close((err) => (err ? reject(err) : resolve()));
249
+ });
250
+ }
251
+ export function pushToPlugin(ws, event) {
252
+ if (ws.readyState !== WebSocket.OPEN)
253
+ return;
254
+ ws.send(JSON.stringify(event));
255
+ }
256
+ export function parsePluginFrame(data) {
257
+ try {
258
+ return JSON.parse(data.toString());
259
+ }
260
+ catch {
261
+ return null;
262
+ }
263
+ }
@@ -0,0 +1,10 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export interface BrokerLogger {
3
+ debug(msg: string, fields?: Record<string, unknown>): void;
4
+ info(msg: string, fields?: Record<string, unknown>): void;
5
+ warn(msg: string, fields?: Record<string, unknown>): void;
6
+ error(msg: string, fields?: Record<string, unknown>): void;
7
+ }
8
+ export declare function createBrokerLogger(logDir: string): BrokerLogger;
9
+ export declare function createNullLogger(): BrokerLogger;
10
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/broker/logger.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CA6B/D;AAGD,wBAAgB,gBAAgB,IAAI,YAAY,CAO/C"}
@@ -0,0 +1,34 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function createBrokerLogger(logDir) {
4
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
5
+ const path = join(logDir, `broker-${process.pid}.log`);
6
+ const write = (level, msg, fields) => {
7
+ const line = JSON.stringify({
8
+ ts: new Date().toISOString(),
9
+ level,
10
+ pid: process.pid,
11
+ msg,
12
+ ...fields,
13
+ });
14
+ try {
15
+ appendFileSync(path, `${line}\n`);
16
+ }
17
+ catch {
18
+ }
19
+ };
20
+ return {
21
+ debug: (msg, fields) => write("debug", msg, fields),
22
+ info: (msg, fields) => write("info", msg, fields),
23
+ warn: (msg, fields) => write("warn", msg, fields),
24
+ error: (msg, fields) => write("error", msg, fields),
25
+ };
26
+ }
27
+ export function createNullLogger() {
28
+ return {
29
+ debug: () => { },
30
+ info: () => { },
31
+ warn: () => { },
32
+ error: () => { },
33
+ };
34
+ }
@@ -0,0 +1,8 @@
1
+ export interface NetworkPresenceChangedEvent {
2
+ type: "network_presence_changed";
3
+ version: 1;
4
+ state: "online" | "reconnecting" | "offline";
5
+ ts: number;
6
+ reason?: string;
7
+ }
8
+ //# sourceMappingURL=network-presence-changed-event-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-presence-changed-event-types.d.ts","sourceRoot":"","sources":["../../src/broker/network-presence-changed-event-types.ts"],"names":[],"mappings":"AAqCA,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,0BAA0B,CAAC;IAGjC,OAAO,EAAE,CAAC,CAAC;IAEX,KAAK,EAAE,QAAQ,GAAG,cAAc,GAAG,SAAS,CAAC;IAG7C,EAAE,EAAE,MAAM,CAAC;IAMX,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,22 @@
1
+ import type { BrokerLogger } from "./logger.js";
2
+ import type { NetworkPresenceChangedEvent } from "./network-presence-changed-event-types.js";
3
+ import type { EmitOutcome, RuntimeEndpointPort } from "./runtime-endpoint-port.js";
4
+ export declare const DEFAULT_PRESENCE_EMITTER_CAPACITY = 128;
5
+ export declare const DEFAULT_PRESENCE_EMITTER_BACKOFF_INITIAL_MS = 500;
6
+ export declare const DEFAULT_PRESENCE_EMITTER_BACKOFF_MAX_MS = 30000;
7
+ export declare const DEFAULT_PRESENCE_EMITTER_MAX_RETRIES = 5;
8
+ export interface NetworkPresenceEmitterOptions {
9
+ capacity?: number;
10
+ backoffInitialMs?: number;
11
+ backoffMaxMs?: number;
12
+ maxRetries?: number;
13
+ }
14
+ export interface NetworkPresenceEmitter {
15
+ enqueue(event: NetworkPresenceChangedEvent): boolean;
16
+ size(): number;
17
+ capacity(): number;
18
+ shutdown(): Promise<void>;
19
+ }
20
+ export declare function createNetworkPresenceEmitter(post: (event: NetworkPresenceChangedEvent) => Promise<EmitOutcome>, logger: BrokerLogger, opts?: NetworkPresenceEmitterOptions): NetworkPresenceEmitter;
21
+ export declare function postNetworkPresenceChangedViaPort(apiPort: RuntimeEndpointPort): (event: NetworkPresenceChangedEvent) => Promise<EmitOutcome>;
22
+ //# sourceMappingURL=network-presence-emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-presence-emitter.d.ts","sourceRoot":"","sources":["../../src/broker/network-presence-emitter.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAC;AAC7F,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AAEpC,eAAO,MAAM,iCAAiC,MAAM,CAAC;AACrD,eAAO,MAAM,2CAA2C,MAAM,CAAC;AAC/D,eAAO,MAAM,uCAAuC,QAAS,CAAC;AAC9D,eAAO,MAAM,oCAAoC,IAAI,CAAC;AAEtD,MAAM,WAAW,6BAA6B;IAE5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IAIrC,OAAO,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC;IAErD,IAAI,IAAI,MAAM,CAAC;IAEf,QAAQ,IAAI,MAAM,CAAC;IAGnB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,CAAC,KAAK,EAAE,2BAA2B,KAAK,OAAO,CAAC,WAAW,CAAC,EAClE,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE,6BAAkC,GACvC,sBAAsB,CAyIxB;AAcD,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,mBAAmB,GAC3B,CAAC,KAAK,EAAE,2BAA2B,KAAK,OAAO,CAAC,WAAW,CAAC,CAW9D"}
@@ -0,0 +1,150 @@
1
+ export const DEFAULT_PRESENCE_EMITTER_CAPACITY = 128;
2
+ export const DEFAULT_PRESENCE_EMITTER_BACKOFF_INITIAL_MS = 500;
3
+ export const DEFAULT_PRESENCE_EMITTER_BACKOFF_MAX_MS = 30_000;
4
+ export const DEFAULT_PRESENCE_EMITTER_MAX_RETRIES = 5;
5
+ export function createNetworkPresenceEmitter(post, logger, opts = {}) {
6
+ const cap = opts.capacity ?? DEFAULT_PRESENCE_EMITTER_CAPACITY;
7
+ const initialBackoff = opts.backoffInitialMs ?? DEFAULT_PRESENCE_EMITTER_BACKOFF_INITIAL_MS;
8
+ const maxBackoff = opts.backoffMaxMs ?? DEFAULT_PRESENCE_EMITTER_BACKOFF_MAX_MS;
9
+ const maxRetries = opts.maxRetries ?? DEFAULT_PRESENCE_EMITTER_MAX_RETRIES;
10
+ if (cap <= 0)
11
+ throw new Error(`capacity must be positive: ${cap}`);
12
+ if (initialBackoff <= 0)
13
+ throw new Error(`backoffInitialMs must be positive: ${initialBackoff}`);
14
+ if (maxBackoff < initialBackoff)
15
+ throw new Error(`backoffMaxMs must be >= backoffInitialMs`);
16
+ if (maxRetries < 0)
17
+ throw new Error(`maxRetries must be non-negative: ${maxRetries}`);
18
+ const queue = [];
19
+ let draining = false;
20
+ let stopped = false;
21
+ let activeDrain = null;
22
+ let wakeSleep = null;
23
+ const sleep = (ms) => new Promise((resolve) => {
24
+ const t = setTimeout(() => {
25
+ wakeSleep = null;
26
+ resolve();
27
+ }, ms);
28
+ t.unref?.();
29
+ wakeSleep = () => {
30
+ clearTimeout(t);
31
+ wakeSleep = null;
32
+ resolve();
33
+ };
34
+ });
35
+ const drain = async () => {
36
+ while (!stopped && queue.length > 0) {
37
+ const event = queue[0];
38
+ if (!event)
39
+ break;
40
+ let attempts = 0;
41
+ while (true) {
42
+ if (stopped)
43
+ return;
44
+ let outcome;
45
+ try {
46
+ outcome = await post(event);
47
+ }
48
+ catch (err) {
49
+ outcome = {
50
+ ok: false,
51
+ terminal: false,
52
+ detail: err instanceof Error ? err.message : String(err),
53
+ };
54
+ }
55
+ if (outcome.ok)
56
+ break;
57
+ if (outcome.terminal) {
58
+ logger.warn("network_presence_changed_drop_terminal", {
59
+ state: event.state,
60
+ ts: event.ts,
61
+ reason: event.reason,
62
+ status: outcome.status,
63
+ detail: outcome.detail,
64
+ });
65
+ break;
66
+ }
67
+ attempts++;
68
+ if (attempts > maxRetries) {
69
+ logger.warn("network_presence_changed_drop_max_retries", {
70
+ state: event.state,
71
+ ts: event.ts,
72
+ attempts,
73
+ status: outcome.status,
74
+ detail: outcome.detail,
75
+ });
76
+ break;
77
+ }
78
+ const backoff = Math.min(initialBackoff * 2 ** (attempts - 1), maxBackoff);
79
+ logger.info("network_presence_changed_retry", {
80
+ state: event.state,
81
+ ts: event.ts,
82
+ attempt: attempts,
83
+ backoff_ms: backoff,
84
+ status: outcome.status,
85
+ });
86
+ await sleep(backoff);
87
+ }
88
+ queue.shift();
89
+ }
90
+ draining = false;
91
+ };
92
+ const kick = () => {
93
+ if (draining || stopped)
94
+ return;
95
+ draining = true;
96
+ activeDrain = drain().catch((err) => {
97
+ logger.error("network_presence_changed_drain_unexpected_error", {
98
+ err: err instanceof Error ? err.message : String(err),
99
+ });
100
+ draining = false;
101
+ });
102
+ };
103
+ return {
104
+ enqueue(event) {
105
+ if (stopped)
106
+ return true;
107
+ let evicted = false;
108
+ if (queue.length >= cap) {
109
+ const old = queue.shift();
110
+ if (old) {
111
+ logger.warn("network_presence_changed_queue_overflow", {
112
+ dropped_state: old.state,
113
+ dropped_ts: old.ts,
114
+ size: queue.length,
115
+ capacity: cap,
116
+ });
117
+ evicted = true;
118
+ }
119
+ }
120
+ queue.push(event);
121
+ kick();
122
+ return !evicted;
123
+ },
124
+ size() {
125
+ return queue.length;
126
+ },
127
+ capacity() {
128
+ return cap;
129
+ },
130
+ async shutdown() {
131
+ stopped = true;
132
+ wakeSleep?.();
133
+ if (activeDrain) {
134
+ await activeDrain.catch(() => { });
135
+ }
136
+ },
137
+ };
138
+ }
139
+ export function postNetworkPresenceChangedViaPort(apiPort) {
140
+ return async (event) => {
141
+ if (!apiPort.emitNetworkPresenceChanged) {
142
+ return {
143
+ ok: false,
144
+ terminal: true,
145
+ detail: "emitNetworkPresenceChanged not implemented on this apiPort",
146
+ };
147
+ }
148
+ return apiPort.emitNetworkPresenceChanged(event);
149
+ };
150
+ }
@@ -0,0 +1,31 @@
1
+ export declare const DEFAULT_PRESENCE_GRACE_MS: number;
2
+ export type NetworkPresence = "online" | "reconnecting" | "offline";
3
+ export type PresenceEvent = {
4
+ type: "connector_connected";
5
+ } | {
6
+ type: "connector_disconnected";
7
+ } | {
8
+ type: "presence_grace_expired";
9
+ } | {
10
+ type: "shutdown";
11
+ };
12
+ export type PresenceEffect = {
13
+ type: "start_grace_timer";
14
+ deadline_ms: number;
15
+ } | {
16
+ type: "cancel_grace_timer";
17
+ } | {
18
+ type: "emit_presence";
19
+ presence: NetworkPresence;
20
+ reason: string;
21
+ };
22
+ export interface PresenceTransitionResult {
23
+ next: NetworkPresence;
24
+ effects: readonly PresenceEffect[];
25
+ ignored?: boolean;
26
+ }
27
+ export interface PresenceTransitionOptions {
28
+ graceMs?: number;
29
+ }
30
+ export declare function transitionPresence(current: NetworkPresence, event: PresenceEvent, opts?: PresenceTransitionOptions): PresenceTransitionResult;
31
+ //# sourceMappingURL=network-presence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-presence.d.ts","sourceRoot":"","sources":["../../src/broker/network-presence.ts"],"names":[],"mappings":"AAmCA,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,cAAc,GAAG,SAAS,CAAC;AAEpE,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,qBAAqB,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,wBAAwB,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,wBAAwB,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,oBAAoB,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAOD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,aAAa,EACpB,IAAI,GAAE,yBAA8B,GACnC,wBAAwB,CAgH1B"}
@@ -0,0 +1,109 @@
1
+ export const DEFAULT_PRESENCE_GRACE_MS = 10 * 60 * 1000;
2
+ export function transitionPresence(current, event, opts = {}) {
3
+ const graceMs = opts.graceMs ?? DEFAULT_PRESENCE_GRACE_MS;
4
+ switch (current) {
5
+ case "offline": {
6
+ switch (event.type) {
7
+ case "connector_connected":
8
+ return {
9
+ next: "online",
10
+ effects: [
11
+ {
12
+ type: "emit_presence",
13
+ presence: "online",
14
+ reason: "connector_connected",
15
+ },
16
+ ],
17
+ };
18
+ case "shutdown":
19
+ return {
20
+ next: "offline",
21
+ effects: [
22
+ {
23
+ type: "emit_presence",
24
+ presence: "offline",
25
+ reason: "broker_shutdown",
26
+ },
27
+ ],
28
+ };
29
+ case "connector_disconnected":
30
+ case "presence_grace_expired":
31
+ return { next: "offline", effects: [], ignored: true };
32
+ }
33
+ return { next: current, effects: [], ignored: true };
34
+ }
35
+ case "online": {
36
+ switch (event.type) {
37
+ case "connector_disconnected":
38
+ return {
39
+ next: "reconnecting",
40
+ effects: [
41
+ { type: "start_grace_timer", deadline_ms: graceMs },
42
+ {
43
+ type: "emit_presence",
44
+ presence: "reconnecting",
45
+ reason: "connector_disconnected",
46
+ },
47
+ ],
48
+ };
49
+ case "shutdown":
50
+ return {
51
+ next: "offline",
52
+ effects: [
53
+ {
54
+ type: "emit_presence",
55
+ presence: "offline",
56
+ reason: "broker_shutdown",
57
+ },
58
+ ],
59
+ };
60
+ case "connector_connected":
61
+ case "presence_grace_expired":
62
+ return { next: "online", effects: [], ignored: true };
63
+ }
64
+ return { next: current, effects: [], ignored: true };
65
+ }
66
+ case "reconnecting": {
67
+ switch (event.type) {
68
+ case "connector_connected":
69
+ return {
70
+ next: "online",
71
+ effects: [
72
+ { type: "cancel_grace_timer" },
73
+ {
74
+ type: "emit_presence",
75
+ presence: "online",
76
+ reason: "connector_reconnected",
77
+ },
78
+ ],
79
+ };
80
+ case "presence_grace_expired":
81
+ return {
82
+ next: "offline",
83
+ effects: [
84
+ {
85
+ type: "emit_presence",
86
+ presence: "offline",
87
+ reason: "grace_timeout",
88
+ },
89
+ ],
90
+ };
91
+ case "shutdown":
92
+ return {
93
+ next: "offline",
94
+ effects: [
95
+ { type: "cancel_grace_timer" },
96
+ {
97
+ type: "emit_presence",
98
+ presence: "offline",
99
+ reason: "broker_shutdown",
100
+ },
101
+ ],
102
+ };
103
+ case "connector_disconnected":
104
+ return { next: "reconnecting", effects: [], ignored: true };
105
+ }
106
+ return { next: current, effects: [], ignored: true };
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,11 @@
1
+ export interface BrokerPathOptions {
2
+ userDataDir?: string;
3
+ }
4
+ export interface BrokerPaths {
5
+ discoveryFile: string;
6
+ discoveryDir: string;
7
+ logDir: string;
8
+ buffersDir: string;
9
+ }
10
+ export declare function resolveBrokerPaths(principal: string, runtimeToken: string, opts?: BrokerPathOptions): BrokerPaths;
11
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/broker/paths.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,iBAAiB;IAEhC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAE1B,aAAa,EAAE,MAAM,CAAC;IAEtB,YAAY,EAAE,MAAM,CAAC;IAErB,MAAM,EAAE,MAAM,CAAC;IAQf,UAAU,EAAE,MAAM,CAAC;CACpB;AASD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,IAAI,GAAE,iBAAsB,GAC3B,WAAW,CAgCb"}