orbit-bus 0.1.1

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 (117) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +501 -0
  4. package/dist/src/agent_ipc.d.ts +4 -0
  5. package/dist/src/agent_ipc.js +77 -0
  6. package/dist/src/api_contract.d.ts +19 -0
  7. package/dist/src/api_contract.js +81 -0
  8. package/dist/src/api_http.d.ts +23 -0
  9. package/dist/src/api_http.js +62 -0
  10. package/dist/src/call_protection.d.ts +5 -0
  11. package/dist/src/call_protection.js +83 -0
  12. package/dist/src/cell/gateway.d.ts +13 -0
  13. package/dist/src/cell/gateway.js +171 -0
  14. package/dist/src/cell/routing.d.ts +18 -0
  15. package/dist/src/cell/routing.js +48 -0
  16. package/dist/src/cell/template.d.ts +7 -0
  17. package/dist/src/cell/template.js +24 -0
  18. package/dist/src/cli.d.ts +1 -0
  19. package/dist/src/cli.js +305 -0
  20. package/dist/src/commands/agent.d.ts +3 -0
  21. package/dist/src/commands/agent.js +187 -0
  22. package/dist/src/commands/api.d.ts +6 -0
  23. package/dist/src/commands/api.js +226 -0
  24. package/dist/src/commands/bench.d.ts +13 -0
  25. package/dist/src/commands/bench.js +125 -0
  26. package/dist/src/commands/bench_overhead.d.ts +8 -0
  27. package/dist/src/commands/bench_overhead.js +71 -0
  28. package/dist/src/commands/call.d.ts +10 -0
  29. package/dist/src/commands/call.js +45 -0
  30. package/dist/src/commands/cell.d.ts +3 -0
  31. package/dist/src/commands/cell.js +186 -0
  32. package/dist/src/commands/context.d.ts +9 -0
  33. package/dist/src/commands/context.js +71 -0
  34. package/dist/src/commands/dlq_inspect.d.ts +11 -0
  35. package/dist/src/commands/dlq_inspect.js +86 -0
  36. package/dist/src/commands/dlq_purge.d.ts +12 -0
  37. package/dist/src/commands/dlq_purge.js +84 -0
  38. package/dist/src/commands/dlq_replay.d.ts +15 -0
  39. package/dist/src/commands/dlq_replay.js +127 -0
  40. package/dist/src/commands/inspect.d.ts +6 -0
  41. package/dist/src/commands/inspect.js +57 -0
  42. package/dist/src/commands/monitor.d.ts +32 -0
  43. package/dist/src/commands/monitor.js +201 -0
  44. package/dist/src/commands/publish.d.ts +10 -0
  45. package/dist/src/commands/publish.js +64 -0
  46. package/dist/src/commands/serve.d.ts +8 -0
  47. package/dist/src/commands/serve.js +258 -0
  48. package/dist/src/commands/subscribe.d.ts +11 -0
  49. package/dist/src/commands/subscribe.js +78 -0
  50. package/dist/src/commands/trace.d.ts +5 -0
  51. package/dist/src/commands/trace.js +26 -0
  52. package/dist/src/commands/up.d.ts +3 -0
  53. package/dist/src/commands/up.js +91 -0
  54. package/dist/src/config.d.ts +6 -0
  55. package/dist/src/config.js +281 -0
  56. package/dist/src/dlq.d.ts +20 -0
  57. package/dist/src/dlq.js +71 -0
  58. package/dist/src/echo/benchmark.d.ts +10 -0
  59. package/dist/src/echo/benchmark.js +105 -0
  60. package/dist/src/echo/bus.d.ts +22 -0
  61. package/dist/src/echo/bus.js +89 -0
  62. package/dist/src/echo/cli.d.ts +1 -0
  63. package/dist/src/echo/cli.js +135 -0
  64. package/dist/src/echo/client.d.ts +12 -0
  65. package/dist/src/echo/client.js +46 -0
  66. package/dist/src/echo/daemon.d.ts +8 -0
  67. package/dist/src/echo/daemon.js +181 -0
  68. package/dist/src/echo/index.d.ts +6 -0
  69. package/dist/src/echo/index.js +5 -0
  70. package/dist/src/echo/ring_buffer.d.ts +27 -0
  71. package/dist/src/echo/ring_buffer.js +73 -0
  72. package/dist/src/echo/types.d.ts +27 -0
  73. package/dist/src/echo/types.js +1 -0
  74. package/dist/src/echocore.d.ts +2 -0
  75. package/dist/src/echocore.js +6 -0
  76. package/dist/src/envelope.d.ts +14 -0
  77. package/dist/src/envelope.js +92 -0
  78. package/dist/src/errors.d.ts +5 -0
  79. package/dist/src/errors.js +9 -0
  80. package/dist/src/index.d.ts +2 -0
  81. package/dist/src/index.js +12 -0
  82. package/dist/src/jetstream_durable.d.ts +4 -0
  83. package/dist/src/jetstream_durable.js +51 -0
  84. package/dist/src/json_schema.d.ts +6 -0
  85. package/dist/src/json_schema.js +154 -0
  86. package/dist/src/logger.d.ts +16 -0
  87. package/dist/src/logger.js +31 -0
  88. package/dist/src/metrics.d.ts +6 -0
  89. package/dist/src/metrics.js +95 -0
  90. package/dist/src/nats.d.ts +16 -0
  91. package/dist/src/nats.js +129 -0
  92. package/dist/src/orbit_actions.d.ts +4 -0
  93. package/dist/src/orbit_actions.js +129 -0
  94. package/dist/src/otel.d.ts +2 -0
  95. package/dist/src/otel.js +96 -0
  96. package/dist/src/registry.d.ts +10 -0
  97. package/dist/src/registry.js +57 -0
  98. package/dist/src/retry.d.ts +9 -0
  99. package/dist/src/retry.js +32 -0
  100. package/dist/src/rpc_call.d.ts +22 -0
  101. package/dist/src/rpc_call.js +119 -0
  102. package/dist/src/service_adapter.d.ts +7 -0
  103. package/dist/src/service_adapter.js +163 -0
  104. package/dist/src/spec.d.ts +2 -0
  105. package/dist/src/spec.js +30 -0
  106. package/dist/src/subjects.d.ts +2 -0
  107. package/dist/src/subjects.js +4 -0
  108. package/dist/src/trace.d.ts +4 -0
  109. package/dist/src/trace.js +86 -0
  110. package/dist/src/types.d.ts +133 -0
  111. package/dist/src/types.js +1 -0
  112. package/dist/src/util.d.ts +10 -0
  113. package/dist/src/util.js +63 -0
  114. package/dist/src/worker_pool.d.ts +9 -0
  115. package/dist/src/worker_pool.js +163 -0
  116. package/docs/orbit-api-contract.yaml +376 -0
  117. package/package.json +40 -0
@@ -0,0 +1,226 @@
1
+ import fs from "node:fs";
2
+ import http from "node:http";
3
+ import https from "node:https";
4
+ import { closeBus, connectBus } from "../nats.js";
5
+ import { actionFromApiPath, parseObjectPayload } from "../api_contract.js";
6
+ import { executeOrbitAction } from "../orbit_actions.js";
7
+ import { randomId } from "../util.js";
8
+ import { ApiHttpError, normalizeApiError } from "../api_http.js";
9
+ import { OrbitError } from "../errors.js";
10
+ import { incCounter, observeHistogram, renderPrometheusMetrics, setGauge } from "../metrics.js";
11
+ function writeJson(res, status, body) {
12
+ res.statusCode = status;
13
+ res.setHeader("content-type", "application/json");
14
+ res.end(`${JSON.stringify(body)}\n`);
15
+ }
16
+ function writeError(res, id, error) {
17
+ writeJson(res, error.status, {
18
+ id,
19
+ ok: false,
20
+ error: {
21
+ type: "orbit_error",
22
+ code: error.code,
23
+ message: error.message,
24
+ ...(error.details === undefined ? {} : { details: error.details })
25
+ }
26
+ });
27
+ }
28
+ function extractBearerToken(req) {
29
+ const auth = req.headers.authorization;
30
+ if (typeof auth === "string" && auth.startsWith("Bearer ")) {
31
+ return auth.slice("Bearer ".length).trim();
32
+ }
33
+ const header = req.headers["x-orbit-token"];
34
+ if (typeof header === "string")
35
+ return header.trim();
36
+ return undefined;
37
+ }
38
+ function ensureAuthorized(req, config) {
39
+ const requiredToken = config.api.authToken;
40
+ if (requiredToken && extractBearerToken(req) !== requiredToken) {
41
+ throw new ApiHttpError(401, "UNAUTHORIZED", "missing or invalid API token");
42
+ }
43
+ if (config.api.tls.enabled && config.api.tls.requestClientCert && config.api.tls.requireClientCert) {
44
+ const maybeTls = req.socket;
45
+ if (!maybeTls.authorized) {
46
+ throw new ApiHttpError(401, "UNAUTHORIZED", "valid client TLS certificate is required");
47
+ }
48
+ }
49
+ }
50
+ async function readJsonBody(req, maxBytes) {
51
+ const chunks = [];
52
+ let totalBytes = 0;
53
+ for await (const chunk of req) {
54
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
55
+ totalBytes += buf.length;
56
+ if (totalBytes > maxBytes) {
57
+ throw new ApiHttpError(413, "PAYLOAD_TOO_LARGE", `request exceeds max body size ${maxBytes} bytes`);
58
+ }
59
+ chunks.push(buf);
60
+ }
61
+ const text = Buffer.concat(chunks).toString("utf-8").trim();
62
+ if (!text)
63
+ return {};
64
+ try {
65
+ return JSON.parse(text);
66
+ }
67
+ catch {
68
+ throw new ApiHttpError(400, "BAD_JSON", "invalid JSON body");
69
+ }
70
+ }
71
+ function ensureBindHostAllowed(config, host) {
72
+ const allow = config.api.allowedHosts;
73
+ if (allow.includes("*") || allow.includes(host))
74
+ return;
75
+ throw new OrbitError("FORBIDDEN_BIND_HOST", `host ${host} is not in api.allowedHosts`, { allowedHosts: allow });
76
+ }
77
+ async function isNatsReady(nc) {
78
+ if (nc.isClosed())
79
+ return false;
80
+ let timer;
81
+ const timeout = new Promise((_, reject) => {
82
+ timer = setTimeout(() => reject(new Error("readiness timeout")), 1000);
83
+ timer.unref?.();
84
+ });
85
+ try {
86
+ await Promise.race([nc.flush(), timeout]);
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ finally {
93
+ if (timer)
94
+ clearTimeout(timer);
95
+ }
96
+ }
97
+ async function withTimeout(input, timeoutMs) {
98
+ let timer;
99
+ const timeoutPromise = new Promise((_, reject) => {
100
+ timer = setTimeout(() => reject(new ApiHttpError(408, "REQUEST_TIMEOUT", `request timed out after ${timeoutMs}ms`)), timeoutMs);
101
+ timer.unref?.();
102
+ });
103
+ try {
104
+ return await Promise.race([input, timeoutPromise]);
105
+ }
106
+ finally {
107
+ if (timer)
108
+ clearTimeout(timer);
109
+ }
110
+ }
111
+ function createServer(config, handler) {
112
+ if (!config.api.tls.enabled)
113
+ return http.createServer(handler);
114
+ if (!config.api.tls.keyFile || !config.api.tls.certFile) {
115
+ throw new OrbitError("BAD_ARGS", "api tls requires keyFile and certFile");
116
+ }
117
+ const key = fs.readFileSync(config.api.tls.keyFile, "utf-8");
118
+ const cert = fs.readFileSync(config.api.tls.certFile, "utf-8");
119
+ const ca = config.api.tls.caFile ? fs.readFileSync(config.api.tls.caFile, "utf-8") : undefined;
120
+ return https.createServer({
121
+ key,
122
+ cert,
123
+ ca,
124
+ requestCert: config.api.tls.requestClientCert,
125
+ rejectUnauthorized: config.api.tls.requireClientCert
126
+ }, handler);
127
+ }
128
+ export async function cmdApi(config, logger, opts) {
129
+ ensureBindHostAllowed(config, opts.host);
130
+ const nc = await connectBus(config.natsUrl);
131
+ let inFlight = 0;
132
+ setGauge("orbit_api_inflight", inFlight);
133
+ const server = createServer(config, (req, res) => {
134
+ void (async () => {
135
+ const started = Date.now();
136
+ const requestId = req.headers["x-request-id"]?.toString() || randomId();
137
+ let actionLabel = "unknown";
138
+ let entered = false;
139
+ let status = 500;
140
+ try {
141
+ if (req.method === "GET" && req.url === "/healthz") {
142
+ status = 200;
143
+ writeJson(res, 200, { ok: true, status: "up" });
144
+ return;
145
+ }
146
+ if (req.method === "GET" && req.url === "/readyz") {
147
+ const ready = await isNatsReady(nc);
148
+ status = ready ? 200 : 503;
149
+ writeJson(res, status, {
150
+ ok: ready,
151
+ status: ready ? "ready" : "degraded",
152
+ dependencies: { nats: ready ? "up" : "down" }
153
+ });
154
+ return;
155
+ }
156
+ if (req.method === "GET" && req.url === "/metrics") {
157
+ ensureAuthorized(req, config);
158
+ status = 200;
159
+ res.statusCode = 200;
160
+ res.setHeader("content-type", "text/plain; version=0.0.4");
161
+ res.end(renderPrometheusMetrics());
162
+ return;
163
+ }
164
+ if (req.method !== "POST" || !req.url) {
165
+ throw new ApiHttpError(405, "METHOD_NOT_ALLOWED", "use POST /v1/<action>");
166
+ }
167
+ ensureAuthorized(req, config);
168
+ if (inFlight >= config.runtime.apiMaxConcurrent) {
169
+ throw new ApiHttpError(429, "API_OVERLOADED", "api concurrency limit reached");
170
+ }
171
+ inFlight += 1;
172
+ entered = true;
173
+ setGauge("orbit_api_inflight", inFlight);
174
+ const parsedAction = actionFromApiPath(req.url.split("?")[0]);
175
+ if (!parsedAction) {
176
+ throw new ApiHttpError(404, "NOT_FOUND", "unknown endpoint");
177
+ }
178
+ actionLabel = parsedAction;
179
+ const payload = parseObjectPayload(await readJsonBody(req, config.runtime.apiMaxBodyBytes));
180
+ const out = await withTimeout(executeOrbitAction(config, nc, parsedAction, payload, "api"), config.runtime.apiRequestTimeoutMs);
181
+ status = 200;
182
+ writeJson(res, 200, { id: requestId, ok: true, payload: out });
183
+ }
184
+ catch (err) {
185
+ const apiErr = normalizeApiError(err);
186
+ status = apiErr.status;
187
+ writeError(res, requestId, apiErr);
188
+ }
189
+ finally {
190
+ const durationMs = Date.now() - started;
191
+ incCounter("orbit_api_requests_total", 1, { action: actionLabel, method: req.method ?? "UNKNOWN", status });
192
+ observeHistogram("orbit_api_request_duration_ms", durationMs, { action: actionLabel, status });
193
+ if (entered) {
194
+ inFlight -= 1;
195
+ setGauge("orbit_api_inflight", inFlight);
196
+ }
197
+ }
198
+ })();
199
+ });
200
+ server.requestTimeout = config.runtime.apiRequestTimeoutMs;
201
+ server.keepAliveTimeout = 5000;
202
+ await new Promise((resolve, reject) => {
203
+ server.once("error", reject);
204
+ server.listen(opts.port, opts.host, () => resolve());
205
+ });
206
+ logger.info("api online", {
207
+ host: opts.host,
208
+ port: opts.port,
209
+ nats_url: config.natsUrl,
210
+ tls_enabled: config.api.tls.enabled,
211
+ mtls_required: config.api.tls.requestClientCert && config.api.tls.requireClientCert,
212
+ token_auth_enabled: Boolean(config.api.authToken)
213
+ });
214
+ const shutdown = async () => {
215
+ await new Promise((resolve) => server.close(() => resolve()));
216
+ try {
217
+ await closeBus(config.natsUrl);
218
+ }
219
+ catch {
220
+ // ignore shutdown failures
221
+ }
222
+ };
223
+ process.once("SIGINT", () => void shutdown().finally(() => process.exit(0)));
224
+ process.once("SIGTERM", () => void shutdown().finally(() => process.exit(0)));
225
+ await nc.closed();
226
+ }
@@ -0,0 +1,13 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdBench(config: OrbitConfig, _logger: Logger, opts: {
4
+ target: string;
5
+ body: unknown;
6
+ durationSec: number;
7
+ concurrency: number;
8
+ rampToConcurrency?: number;
9
+ rampStepSec?: number;
10
+ rampStepConcurrency?: number;
11
+ timeoutMs?: number;
12
+ retries?: number;
13
+ }): Promise<void>;
@@ -0,0 +1,125 @@
1
+ import { createEnvelope, validateEnvelope } from "../envelope.js";
2
+ import { OrbitError } from "../errors.js";
3
+ import { closeBus, connectBus, decodeJson, encodeJson } from "../nats.js";
4
+ import { withRetries } from "../retry.js";
5
+ import { appendTraceEvent } from "../trace.js";
6
+ import { randomId } from "../util.js";
7
+ import { prefixedSubject } from "../subjects.js";
8
+ function percentile(input, p) {
9
+ if (input.length === 0)
10
+ return 0;
11
+ const sorted = [...input].sort((a, b) => a - b);
12
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor((p / 100) * sorted.length)));
13
+ return sorted[idx];
14
+ }
15
+ export async function cmdBench(config, _logger, opts) {
16
+ const match = opts.target.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/);
17
+ if (!match)
18
+ throw new OrbitError("BAD_TARGET", "target must be <service>.<method>");
19
+ const [, svc, method] = match;
20
+ const subject = prefixedSubject(config, "rpc", svc, method);
21
+ const timeoutMs = opts.timeoutMs ?? config.requestTimeoutMs;
22
+ const retries = opts.retries ?? config.retries;
23
+ const durationMs = Math.max(1, Math.floor(opts.durationSec * 1000));
24
+ const baseConcurrency = Math.max(1, Math.floor(opts.concurrency));
25
+ const rampTo = opts.rampToConcurrency ? Math.max(baseConcurrency, Math.floor(opts.rampToConcurrency)) : baseConcurrency;
26
+ const maxWorkers = rampTo;
27
+ const rampStepSec = Math.max(1, Math.floor(opts.rampStepSec ?? 1));
28
+ const rampStepConcurrency = Math.max(1, Math.floor(opts.rampStepConcurrency ?? 1));
29
+ const nc = await connectBus(config.natsUrl);
30
+ const runId = randomId();
31
+ const start = Date.now();
32
+ const stopAt = start + durationMs;
33
+ let total = 0;
34
+ let success = 0;
35
+ let failed = 0;
36
+ const latencies = [];
37
+ appendTraceEvent(config, {
38
+ span_id: randomId(),
39
+ run_id: runId,
40
+ ts: new Date().toISOString(),
41
+ actor: "bench",
42
+ event: "bench_start",
43
+ svc,
44
+ method,
45
+ detail: `duration_ms=${durationMs},concurrency=${baseConcurrency},ramp_to=${rampTo}`
46
+ });
47
+ let activeWorkers = baseConcurrency;
48
+ if (rampTo > baseConcurrency) {
49
+ const rampTimer = setInterval(() => {
50
+ activeWorkers = Math.min(rampTo, activeWorkers + rampStepConcurrency);
51
+ if (activeWorkers >= rampTo)
52
+ clearInterval(rampTimer);
53
+ }, rampStepSec * 1000);
54
+ rampTimer.unref?.();
55
+ }
56
+ const worker = async (index) => {
57
+ while (Date.now() < stopAt) {
58
+ if (index >= activeWorkers) {
59
+ await new Promise((resolve) => setTimeout(resolve, 50));
60
+ continue;
61
+ }
62
+ const callStart = Date.now();
63
+ total += 1;
64
+ try {
65
+ const reqEnv = createEnvelope({
66
+ kind: "request",
67
+ runId,
68
+ payload: opts.body,
69
+ provenance: { caller: "orbit-bench", target: opts.target }
70
+ });
71
+ const { value } = await withRetries(async () => nc.request(subject, encodeJson(reqEnv), { timeout: timeoutMs }), { retries, timeoutMs });
72
+ const reply = validateEnvelope(decodeJson(value.data), { skipHashCheck: config.performance.trustedLocal });
73
+ const payload = reply.payload;
74
+ if (payload?.ok === false) {
75
+ failed += 1;
76
+ }
77
+ else {
78
+ success += 1;
79
+ latencies.push(Date.now() - callStart);
80
+ }
81
+ }
82
+ catch {
83
+ failed += 1;
84
+ }
85
+ }
86
+ };
87
+ await Promise.all(Array.from({ length: maxWorkers }, (_v, idx) => worker(idx)));
88
+ await closeBus(config.natsUrl);
89
+ const elapsedMs = Math.max(1, Date.now() - start);
90
+ const result = {
91
+ ok: true,
92
+ target: opts.target,
93
+ duration_ms: elapsedMs,
94
+ concurrency: {
95
+ start: baseConcurrency,
96
+ max: rampTo,
97
+ ramp_step_sec: rampTo > baseConcurrency ? rampStepSec : null,
98
+ ramp_step_concurrency: rampTo > baseConcurrency ? rampStepConcurrency : null
99
+ },
100
+ total_requests: total,
101
+ success,
102
+ failed,
103
+ error_rate: total === 0 ? 0 : failed / total,
104
+ throughput_rps: Number(((total * 1000) / elapsedMs).toFixed(2)),
105
+ latency_ms: {
106
+ min: latencies.length ? Math.min(...latencies) : 0,
107
+ avg: latencies.length ? Number((latencies.reduce((a, b) => a + b, 0) / latencies.length).toFixed(2)) : 0,
108
+ p50: percentile(latencies, 50),
109
+ p95: percentile(latencies, 95),
110
+ p99: percentile(latencies, 99),
111
+ max: latencies.length ? Math.max(...latencies) : 0
112
+ }
113
+ };
114
+ appendTraceEvent(config, {
115
+ span_id: randomId(),
116
+ run_id: runId,
117
+ ts: new Date().toISOString(),
118
+ actor: "bench",
119
+ event: "bench_end",
120
+ svc,
121
+ method,
122
+ detail: JSON.stringify(result)
123
+ });
124
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
125
+ }
@@ -0,0 +1,8 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdBenchOverhead(config: OrbitConfig, _logger: Logger, opts: {
4
+ target: string;
5
+ body: unknown;
6
+ iterations: number;
7
+ timeoutMs?: number;
8
+ }): Promise<void>;
@@ -0,0 +1,71 @@
1
+ import { createEnvelope, validateEnvelope } from "../envelope.js";
2
+ import { OrbitError } from "../errors.js";
3
+ import { closeBus, connectBus, decodeJson, encodeJson } from "../nats.js";
4
+ import { canUseAgent, requestAgent } from "../agent_ipc.js";
5
+ import { prefixedSubject } from "../subjects.js";
6
+ import { randomId } from "../util.js";
7
+ function percentile(input, p) {
8
+ if (input.length === 0)
9
+ return 0;
10
+ const sorted = [...input].sort((a, b) => a - b);
11
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor((p / 100) * sorted.length)));
12
+ return sorted[idx];
13
+ }
14
+ export async function cmdBenchOverhead(config, _logger, opts) {
15
+ const m = opts.target.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/);
16
+ if (!m)
17
+ throw new OrbitError("BAD_TARGET", "target must be <service>.<method>");
18
+ if (!canUseAgent(config)) {
19
+ throw new OrbitError("AGENT_DISABLED", "bench-overhead requires local agent enabled");
20
+ }
21
+ const [, svc, method] = m;
22
+ const iterations = Math.max(1, Math.floor(opts.iterations));
23
+ const timeoutMs = opts.timeoutMs ?? config.requestTimeoutMs;
24
+ const subject = prefixedSubject(config, "rpc", svc, method);
25
+ const direct = [];
26
+ const viaAgent = [];
27
+ const nc = await connectBus(config.natsUrl);
28
+ try {
29
+ for (let i = 0; i < iterations; i += 1) {
30
+ const runId = randomId();
31
+ const reqEnv = createEnvelope({
32
+ kind: "request",
33
+ runId,
34
+ payload: opts.body,
35
+ provenance: { caller: "orbit-bench-overhead", target: opts.target }
36
+ });
37
+ const directStart = Date.now();
38
+ const directMsg = await nc.request(subject, encodeJson(reqEnv), { timeout: timeoutMs });
39
+ validateEnvelope(decodeJson(directMsg.data), { skipHashCheck: config.performance.trustedLocal });
40
+ direct.push(Date.now() - directStart);
41
+ const agentStart = Date.now();
42
+ await requestAgent(config, "call", { target: opts.target, body: opts.body, timeoutMs, runId: randomId() }, timeoutMs);
43
+ viaAgent.push(Date.now() - agentStart);
44
+ }
45
+ }
46
+ finally {
47
+ await closeBus(config.natsUrl);
48
+ }
49
+ const p95Direct = percentile(direct, 95);
50
+ const p95Agent = percentile(viaAgent, 95);
51
+ const result = {
52
+ ok: true,
53
+ target: opts.target,
54
+ iterations,
55
+ latency_ms: {
56
+ direct: {
57
+ p50: percentile(direct, 50),
58
+ p95: p95Direct
59
+ },
60
+ via_agent: {
61
+ p50: percentile(viaAgent, 50),
62
+ p95: p95Agent
63
+ }
64
+ },
65
+ orbit_overhead_ms: {
66
+ p50: Math.max(0, percentile(viaAgent, 50) - percentile(direct, 50)),
67
+ p95: Math.max(0, p95Agent - p95Direct)
68
+ }
69
+ };
70
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
71
+ }
@@ -0,0 +1,10 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdCall(config: OrbitConfig, logger: Logger, opts: {
4
+ target: string;
5
+ body: unknown;
6
+ timeoutMs?: number;
7
+ retries?: number;
8
+ runId?: string;
9
+ packFile?: string;
10
+ }): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { OrbitError } from "../errors.js";
2
+ import { closeBus, connectBus } from "../nats.js";
3
+ import { canUseAgent, requestAgent } from "../agent_ipc.js";
4
+ import { executeRpcCall } from "../rpc_call.js";
5
+ export async function cmdCall(config, logger, opts) {
6
+ const m = opts.target.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/);
7
+ if (!m)
8
+ throw new OrbitError("BAD_TARGET", "target must be <service>.<method>");
9
+ if (canUseAgent(config)) {
10
+ try {
11
+ const payload = await requestAgent(config, "call", {
12
+ target: opts.target,
13
+ body: opts.body,
14
+ timeoutMs: opts.timeoutMs ?? config.requestTimeoutMs,
15
+ retries: opts.retries ?? config.retries,
16
+ runId: opts.runId,
17
+ packFile: opts.packFile
18
+ }, opts.timeoutMs ?? config.requestTimeoutMs);
19
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
20
+ return;
21
+ }
22
+ catch (err) {
23
+ logger.warn("agent call failed, falling back to direct NATS path", { err: String(err) });
24
+ }
25
+ }
26
+ const nc = await connectBus(config.natsUrl);
27
+ const payload = (await executeRpcCall(config, nc, {
28
+ target: opts.target,
29
+ body: opts.body,
30
+ timeoutMs: opts.timeoutMs,
31
+ retries: opts.retries,
32
+ runId: opts.runId,
33
+ packFile: opts.packFile,
34
+ actor: "cli"
35
+ }));
36
+ if (payload?.ok === false) {
37
+ logger.error("service returned error", { code: payload.error?.code, message: payload.error?.message });
38
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
39
+ process.exitCode = 2;
40
+ }
41
+ else {
42
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
43
+ }
44
+ await closeBus(config.natsUrl);
45
+ }
@@ -0,0 +1,3 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdCell(config: OrbitConfig, logger: Logger, args: string[]): Promise<void>;