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,186 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { startEchoDaemon } from "../echo/daemon.js";
5
+ import { connectEchoClient } from "../echo/client.js";
6
+ import { ensureDir, randomId } from "../util.js";
7
+ import { runCellGateway } from "../cell/gateway.js";
8
+ import { resolveCellRoutingPlan } from "../cell/routing.js";
9
+ import { buildCellRoutesTemplate } from "../cell/template.js";
10
+ function cellStatusPath(config) {
11
+ return path.join(config.dataDir, "cell", "status.json");
12
+ }
13
+ function parseChannels(args) {
14
+ const channels = [];
15
+ for (let i = 0; i < args.length; i += 1) {
16
+ if (args[i] === "--channel" && i + 1 < args.length)
17
+ channels.push(args[i + 1]);
18
+ }
19
+ return channels;
20
+ }
21
+ function argValue(args, flag) {
22
+ const i = args.indexOf(flag);
23
+ if (i >= 0 && i + 1 < args.length)
24
+ return args[i + 1];
25
+ return undefined;
26
+ }
27
+ function hasFlag(args, flag) {
28
+ return args.includes(flag);
29
+ }
30
+ function parseMode(value) {
31
+ if (value === "local_only" || value === "replicate" || value === "global_only")
32
+ return value;
33
+ return "replicate";
34
+ }
35
+ function parseProfile(value) {
36
+ if (value === "high_throughput")
37
+ return "high_throughput";
38
+ return "production";
39
+ }
40
+ function isPidAlive(pid) {
41
+ try {
42
+ process.kill(pid, 0);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ async function fetchEchoStats(socketPath, host, port) {
50
+ try {
51
+ const client = await connectEchoClient({ socketPath, host, port });
52
+ const response = await new Promise((resolve) => {
53
+ client.onLine((line) => {
54
+ const msg = JSON.parse(line);
55
+ if (!msg.ok || msg.type !== "stats") {
56
+ resolve(null);
57
+ client.close();
58
+ return;
59
+ }
60
+ resolve(msg.channels ?? []);
61
+ client.close();
62
+ });
63
+ client.send({ type: "stats" });
64
+ });
65
+ return response;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ export async function cmdCell(config, logger, args) {
72
+ const sub = args[0];
73
+ if (!sub || sub === "--help" || sub === "-h") {
74
+ process.stdout.write(`orbit cell usage:\n orbit cell init [--out ./examples/cell.routes.production.json] [--profile production|high_throughput] [--subject-prefix orbit] [--force]\n orbit cell start [--socket /tmp/echocore.sock] [--tcp-port 7777] [--gateway] [--cell-id <id>] [--routes @routes.json] [--channel <name> ...] [--mode local_only|replicate|global_only]\n orbit cell gateway [--socket /tmp/echocore.sock] [--cell-id <id>] [--routes @routes.json] [--channel <name> ...] [--mode local_only|replicate|global_only]\n orbit cell status [--socket /tmp/echocore.sock]\n`);
75
+ return;
76
+ }
77
+ if (sub === "init") {
78
+ const out = argValue(args, "--out") ?? path.join(process.cwd(), "examples", "cell.routes.production.json");
79
+ const profile = parseProfile(argValue(args, "--profile"));
80
+ const subjectPrefix = argValue(args, "--subject-prefix") ?? config.routing.subjectPrefix;
81
+ const force = hasFlag(args, "--force");
82
+ if (fs.existsSync(out) && !force) {
83
+ throw new Error(`output exists: ${out} (use --force to overwrite)`);
84
+ }
85
+ const template = buildCellRoutesTemplate(profile, subjectPrefix);
86
+ ensureDir(path.dirname(out));
87
+ fs.writeFileSync(out, `${JSON.stringify(template, null, 2)}\n`, "utf-8");
88
+ process.stdout.write(`${JSON.stringify({ ok: true, file: out, profile, routes: Object.keys(template).length }, null, 2)}\n`);
89
+ return;
90
+ }
91
+ if (sub === "start") {
92
+ const socketPath = argValue(args, "--socket") ?? path.join(config.dataDir, "echocore.sock");
93
+ const tcpPort = argValue(args, "--tcp-port") ? Number(argValue(args, "--tcp-port")) : undefined;
94
+ const cellId = argValue(args, "--cell-id") ?? `${os.hostname()}-${randomId().slice(0, 8)}`;
95
+ const routesFileArg = argValue(args, "--routes");
96
+ const routesFile = routesFileArg?.startsWith("@") ? routesFileArg.slice(1) : routesFileArg;
97
+ const channels = parseChannels(args);
98
+ const mode = parseMode(argValue(args, "--mode"));
99
+ const gateway = hasFlag(args, "--gateway") || hasFlag(args, "--with-gateway");
100
+ const routing = resolveCellRoutingPlan(config, { routesFile, channels, defaultMode: mode });
101
+ const daemon = await startEchoDaemon({ socketPath, tcpPort });
102
+ const statusPath = cellStatusPath(config);
103
+ ensureDir(path.dirname(statusPath));
104
+ const status = {
105
+ cellId,
106
+ pid: process.pid,
107
+ startedAt: new Date().toISOString(),
108
+ socketPath: daemon.socketPath,
109
+ tcpPort: daemon.tcpPort,
110
+ gateway,
111
+ source: routing.source,
112
+ routes: routing.routes.map((route) => ({ channel: route.channel, mode: route.mode, subject: route.subject }))
113
+ };
114
+ fs.writeFileSync(statusPath, `${JSON.stringify(status, null, 2)}\n`, "utf-8");
115
+ process.stdout.write(`${JSON.stringify({ ok: true, mode: "cell", daemon: { socketPath: daemon.socketPath, tcpPort: daemon.tcpPort }, gateway, cellId }, null, 2)}\n`);
116
+ if (gateway) {
117
+ await runCellGateway(config, logger, {
118
+ socketPath: daemon.socketPath,
119
+ port: daemon.tcpPort,
120
+ cellId,
121
+ routes: routing.routes,
122
+ fromLatest: true
123
+ });
124
+ return;
125
+ }
126
+ const shutdown = async () => {
127
+ await daemon.close();
128
+ process.exit(0);
129
+ };
130
+ process.once("SIGINT", () => {
131
+ void shutdown();
132
+ });
133
+ process.once("SIGTERM", () => {
134
+ void shutdown();
135
+ });
136
+ await new Promise(() => undefined);
137
+ return;
138
+ }
139
+ if (sub === "gateway") {
140
+ const socketPath = argValue(args, "--socket") ?? path.join(config.dataDir, "echocore.sock");
141
+ const routesFileArg = argValue(args, "--routes");
142
+ const routesFile = routesFileArg?.startsWith("@") ? routesFileArg.slice(1) : routesFileArg;
143
+ const channels = parseChannels(args);
144
+ const mode = parseMode(argValue(args, "--mode"));
145
+ const cellId = argValue(args, "--cell-id") ?? `${os.hostname()}-${randomId().slice(0, 8)}`;
146
+ const host = argValue(args, "--host");
147
+ const port = argValue(args, "--port") ? Number(argValue(args, "--port")) : undefined;
148
+ const routing = resolveCellRoutingPlan(config, { routesFile, channels, defaultMode: mode });
149
+ process.stdout.write(`${JSON.stringify({ ok: true, mode: "gateway", cellId, socketPath, routes: routing.routes.map((route) => ({ channel: route.channel, mode: route.mode, subject: route.subject })) }, null, 2)}\n`);
150
+ await runCellGateway(config, logger, {
151
+ socketPath,
152
+ host,
153
+ port,
154
+ cellId,
155
+ routes: routing.routes,
156
+ fromLatest: true
157
+ });
158
+ return;
159
+ }
160
+ if (sub === "status") {
161
+ const statusPath = cellStatusPath(config);
162
+ const saved = fs.existsSync(statusPath)
163
+ ? JSON.parse(fs.readFileSync(statusPath, "utf-8"))
164
+ : null;
165
+ const socketPath = argValue(args, "--socket") ?? saved?.socketPath;
166
+ const host = argValue(args, "--host");
167
+ const port = argValue(args, "--port") ? Number(argValue(args, "--port")) : saved?.tcpPort;
168
+ const stats = await fetchEchoStats(socketPath, host, port);
169
+ process.stdout.write(`${JSON.stringify({
170
+ ok: true,
171
+ status: saved
172
+ ? {
173
+ ...saved,
174
+ alive: isPidAlive(saved.pid)
175
+ }
176
+ : null,
177
+ echo: {
178
+ endpoint: socketPath ?? (port ? `${host ?? "127.0.0.1"}:${port}` : null),
179
+ connected: stats !== null,
180
+ channels: stats
181
+ }
182
+ }, null, 2)}\n`);
183
+ return;
184
+ }
185
+ throw new Error(`unknown cell subcommand: ${sub}`);
186
+ }
@@ -0,0 +1,9 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdContext(config: OrbitConfig, _logger: Logger, opts: {
4
+ subcommand?: string;
5
+ name?: string;
6
+ natsUrl?: string;
7
+ timeoutMs?: number;
8
+ retries?: number;
9
+ }): void;
@@ -0,0 +1,71 @@
1
+ import { USER_CONFIG_PATH, readUserConfigRaw, writeUserConfigRaw } from "../config.js";
2
+ import { OrbitError } from "../errors.js";
3
+ function readContexts(config) {
4
+ const raw = readUserConfigRaw();
5
+ const contexts = raw.contexts ?? {
6
+ default: {
7
+ natsUrl: config.natsUrl,
8
+ requestTimeoutMs: config.requestTimeoutMs,
9
+ retries: config.retries
10
+ }
11
+ };
12
+ const active = raw.activeContext ?? config.activeContext ?? "default";
13
+ return { active, contexts };
14
+ }
15
+ export function cmdContext(config, _logger, opts) {
16
+ const { active, contexts } = readContexts(config);
17
+ const sub = opts.subcommand ?? "current";
18
+ if (sub === "list") {
19
+ process.stdout.write(`${JSON.stringify({
20
+ active,
21
+ config_file: USER_CONFIG_PATH,
22
+ contexts
23
+ }, null, 2)}\n`);
24
+ return;
25
+ }
26
+ if (sub === "current") {
27
+ process.stdout.write(`${JSON.stringify({
28
+ active,
29
+ value: contexts[active]
30
+ }, null, 2)}\n`);
31
+ return;
32
+ }
33
+ if (sub === "use") {
34
+ if (!opts.name)
35
+ throw new OrbitError("BAD_ARGS", "context use requires context name");
36
+ if (!contexts[opts.name])
37
+ throw new OrbitError("BAD_ARGS", `context ${opts.name} does not exist`);
38
+ const raw = readUserConfigRaw();
39
+ writeUserConfigRaw({
40
+ ...raw,
41
+ activeContext: opts.name,
42
+ contexts
43
+ });
44
+ process.stdout.write(`${JSON.stringify({ ok: true, activeContext: opts.name }, null, 2)}\n`);
45
+ return;
46
+ }
47
+ if (sub === "set") {
48
+ if (!opts.name)
49
+ throw new OrbitError("BAD_ARGS", "context set requires context name");
50
+ const base = contexts[opts.name] ?? contexts[active] ?? {
51
+ natsUrl: config.natsUrl,
52
+ requestTimeoutMs: config.requestTimeoutMs,
53
+ retries: config.retries
54
+ };
55
+ const next = {
56
+ natsUrl: opts.natsUrl ?? base.natsUrl,
57
+ requestTimeoutMs: opts.timeoutMs ?? base.requestTimeoutMs,
58
+ retries: opts.retries ?? base.retries
59
+ };
60
+ const updated = { ...contexts, [opts.name]: next };
61
+ const raw = readUserConfigRaw();
62
+ writeUserConfigRaw({
63
+ ...raw,
64
+ activeContext: raw.activeContext ?? active,
65
+ contexts: updated
66
+ });
67
+ process.stdout.write(`${JSON.stringify({ ok: true, name: opts.name, value: next }, null, 2)}\n`);
68
+ return;
69
+ }
70
+ throw new OrbitError("BAD_ARGS", `unknown context subcommand: ${sub}`);
71
+ }
@@ -0,0 +1,11 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdDlqInspect(config: OrbitConfig, _logger: Logger, opts: {
4
+ dlqTopic: string;
5
+ streamName?: string;
6
+ limit?: number;
7
+ fromTs?: string;
8
+ toTs?: string;
9
+ errorCode?: string;
10
+ sourceConsumer?: string;
11
+ }): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import { closeBus, connectBus, decodeJson } from "../nats.js";
2
+ import { defaultDurableStreamName } from "../jetstream_durable.js";
3
+ import { extractDlqMeta, matchesDlqFilter, parseOptionalIsoTs } from "../dlq.js";
4
+ function asStoredMessage(raw) {
5
+ const msg = raw?.message ?? raw;
6
+ if (!msg)
7
+ return null;
8
+ const seq = typeof msg.seq === "number" ? msg.seq : undefined;
9
+ const subject = typeof msg.subject === "string" ? msg.subject : undefined;
10
+ const data = msg.data;
11
+ if (seq === undefined || !subject || !data)
12
+ return null;
13
+ return {
14
+ seq,
15
+ subject,
16
+ time: typeof msg.time === "string" ? msg.time : undefined,
17
+ data
18
+ };
19
+ }
20
+ export async function cmdDlqInspect(config, _logger, opts) {
21
+ const nc = await connectBus(config.natsUrl);
22
+ const jsm = await nc.jetstreamManager();
23
+ const streamName = opts.streamName ?? defaultDurableStreamName(opts.dlqTopic);
24
+ const limit = Math.max(1, opts.limit ?? 100);
25
+ const filter = {
26
+ fromTsMs: parseOptionalIsoTs(opts.fromTs, "--from-ts"),
27
+ toTsMs: parseOptionalIsoTs(opts.toTs, "--to-ts"),
28
+ errorCode: opts.errorCode,
29
+ sourceConsumer: opts.sourceConsumer
30
+ };
31
+ let info;
32
+ try {
33
+ info = await jsm.streams.info(streamName);
34
+ }
35
+ catch {
36
+ await closeBus(config.natsUrl);
37
+ process.stdout.write(`${JSON.stringify({ ok: true, stream: streamName, scanned: 0, matched: 0, entries: [] }, null, 2)}\n`);
38
+ return;
39
+ }
40
+ const firstSeq = Number(info?.state?.first_seq ?? 0);
41
+ const lastSeq = Number(info?.state?.last_seq ?? 0);
42
+ let scanned = 0;
43
+ let matched = 0;
44
+ const entries = [];
45
+ for (let seq = firstSeq; seq <= lastSeq; seq += 1) {
46
+ if (entries.length >= limit)
47
+ break;
48
+ let storedRaw;
49
+ try {
50
+ storedRaw = await jsm.streams.getMessage(streamName, { seq });
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ const stored = asStoredMessage(storedRaw);
56
+ if (!stored)
57
+ continue;
58
+ if (stored.subject !== opts.dlqTopic)
59
+ continue;
60
+ scanned += 1;
61
+ let decoded = null;
62
+ try {
63
+ decoded = decodeJson(stored.data);
64
+ }
65
+ catch {
66
+ decoded = null;
67
+ }
68
+ const meta = extractDlqMeta(decoded, stored.time);
69
+ if (!matchesDlqFilter(meta, filter))
70
+ continue;
71
+ matched += 1;
72
+ entries.push({
73
+ seq: stored.seq,
74
+ subject: stored.subject,
75
+ failed_at: meta.failedAt,
76
+ source_topic: meta.sourceTopic,
77
+ source_stream: meta.sourceStream,
78
+ source_consumer: meta.sourceConsumer,
79
+ delivery_count: meta.deliveryCount,
80
+ error_code: meta.errorCode,
81
+ error: meta.error
82
+ });
83
+ }
84
+ await closeBus(config.natsUrl);
85
+ process.stdout.write(`${JSON.stringify({ ok: true, stream: streamName, scanned, matched, entries }, null, 2)}\n`);
86
+ }
@@ -0,0 +1,12 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdDlqPurge(config: OrbitConfig, _logger: Logger, opts: {
4
+ dlqTopic: string;
5
+ streamName?: string;
6
+ limit?: number;
7
+ dryRun?: boolean;
8
+ fromTs?: string;
9
+ toTs?: string;
10
+ errorCode?: string;
11
+ sourceConsumer?: string;
12
+ }): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import { closeBus, connectBus, decodeJson } from "../nats.js";
2
+ import { defaultDurableStreamName } from "../jetstream_durable.js";
3
+ import { extractDlqMeta, matchesDlqFilter, parseOptionalIsoTs } from "../dlq.js";
4
+ function asStoredMessage(raw) {
5
+ const msg = raw?.message ?? raw;
6
+ if (!msg)
7
+ return null;
8
+ const seq = typeof msg.seq === "number" ? msg.seq : undefined;
9
+ const subject = typeof msg.subject === "string" ? msg.subject : undefined;
10
+ const data = msg.data;
11
+ if (seq === undefined || !subject || !data)
12
+ return null;
13
+ return {
14
+ seq,
15
+ subject,
16
+ time: typeof msg.time === "string" ? msg.time : undefined,
17
+ data
18
+ };
19
+ }
20
+ export async function cmdDlqPurge(config, _logger, opts) {
21
+ const nc = await connectBus(config.natsUrl);
22
+ const jsm = await nc.jetstreamManager();
23
+ const streamName = opts.streamName ?? defaultDurableStreamName(opts.dlqTopic);
24
+ const limit = Math.max(0, opts.limit ?? 0);
25
+ const filter = {
26
+ fromTsMs: parseOptionalIsoTs(opts.fromTs, "--from-ts"),
27
+ toTsMs: parseOptionalIsoTs(opts.toTs, "--to-ts"),
28
+ errorCode: opts.errorCode,
29
+ sourceConsumer: opts.sourceConsumer
30
+ };
31
+ let info;
32
+ try {
33
+ info = await jsm.streams.info(streamName);
34
+ }
35
+ catch {
36
+ await closeBus(config.natsUrl);
37
+ process.stdout.write(`${JSON.stringify({ ok: true, stream: streamName, scanned: 0, matched: 0, purged: 0, dry_run: Boolean(opts.dryRun) }, null, 2)}\n`);
38
+ return;
39
+ }
40
+ const firstSeq = Number(info?.state?.first_seq ?? 0);
41
+ const lastSeq = Number(info?.state?.last_seq ?? 0);
42
+ let scanned = 0;
43
+ let matched = 0;
44
+ let purged = 0;
45
+ for (let seq = firstSeq; seq <= lastSeq; seq += 1) {
46
+ if (limit > 0 && purged >= limit)
47
+ break;
48
+ let storedRaw;
49
+ try {
50
+ storedRaw = await jsm.streams.getMessage(streamName, { seq });
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ const stored = asStoredMessage(storedRaw);
56
+ if (!stored)
57
+ continue;
58
+ if (stored.subject !== opts.dlqTopic)
59
+ continue;
60
+ scanned += 1;
61
+ let decoded = null;
62
+ try {
63
+ decoded = decodeJson(stored.data);
64
+ }
65
+ catch {
66
+ decoded = null;
67
+ }
68
+ const meta = extractDlqMeta(decoded, stored.time);
69
+ if (!matchesDlqFilter(meta, filter))
70
+ continue;
71
+ matched += 1;
72
+ if (opts.dryRun)
73
+ continue;
74
+ try {
75
+ await jsm.streams.deleteMessage(streamName, seq);
76
+ purged += 1;
77
+ }
78
+ catch {
79
+ // best effort purge
80
+ }
81
+ }
82
+ await closeBus(config.natsUrl);
83
+ process.stdout.write(`${JSON.stringify({ ok: true, stream: streamName, scanned, matched, purged, dry_run: Boolean(opts.dryRun) }, null, 2)}\n`);
84
+ }
@@ -0,0 +1,15 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function replayBytesFromDlqPayload(input: unknown, fallback: Uint8Array): Uint8Array;
4
+ export declare function cmdDlqReplay(config: OrbitConfig, _logger: Logger, opts: {
5
+ dlqTopic: string;
6
+ targetTopic: string;
7
+ limit?: number;
8
+ streamName?: string;
9
+ durablePublish?: boolean;
10
+ purgeReplayed?: boolean;
11
+ fromTs?: string;
12
+ toTs?: string;
13
+ errorCode?: string;
14
+ sourceConsumer?: string;
15
+ }): Promise<void>;
@@ -0,0 +1,127 @@
1
+ import { closeBus, connectBus, decodeJson, encodeJson, publishSubject } from "../nats.js";
2
+ import { defaultDurableStreamName } from "../jetstream_durable.js";
3
+ import { extractDlqMeta, matchesDlqFilter, parseOptionalIsoTs } from "../dlq.js";
4
+ import { randomId } from "../util.js";
5
+ function asStoredMessage(raw) {
6
+ const msg = raw?.message ?? raw;
7
+ if (!msg)
8
+ return null;
9
+ const seq = typeof msg.seq === "number" ? msg.seq : undefined;
10
+ const subject = typeof msg.subject === "string" ? msg.subject : undefined;
11
+ const data = msg.data;
12
+ if (seq === undefined || !subject || !data)
13
+ return null;
14
+ return {
15
+ seq,
16
+ subject,
17
+ time: typeof msg.time === "string" ? msg.time : undefined,
18
+ data
19
+ };
20
+ }
21
+ export function replayBytesFromDlqPayload(input, fallback) {
22
+ if (!input || typeof input !== "object")
23
+ return fallback;
24
+ const raw = input;
25
+ const original = raw.original;
26
+ if (original && typeof original.base64 === "string") {
27
+ return Buffer.from(original.base64, "base64");
28
+ }
29
+ if (typeof raw.raw_base64 === "string") {
30
+ return Buffer.from(raw.raw_base64, "base64");
31
+ }
32
+ if ("payload" in raw) {
33
+ return encodeJson(raw.payload);
34
+ }
35
+ return fallback;
36
+ }
37
+ export async function cmdDlqReplay(config, _logger, opts) {
38
+ const nc = await connectBus(config.natsUrl);
39
+ const jsm = await nc.jetstreamManager();
40
+ const streamName = opts.streamName ?? defaultDurableStreamName(opts.dlqTopic);
41
+ const limit = Math.max(0, opts.limit ?? 0);
42
+ const durablePublish = opts.durablePublish ?? true;
43
+ const filter = {
44
+ fromTsMs: parseOptionalIsoTs(opts.fromTs, "--from-ts"),
45
+ toTsMs: parseOptionalIsoTs(opts.toTs, "--to-ts"),
46
+ errorCode: opts.errorCode,
47
+ sourceConsumer: opts.sourceConsumer
48
+ };
49
+ let info;
50
+ try {
51
+ info = await jsm.streams.info(streamName);
52
+ }
53
+ catch {
54
+ await closeBus(config.natsUrl);
55
+ process.stdout.write(`${JSON.stringify({ ok: true, replayed: 0, scanned: 0, matched: 0, stream: streamName }, null, 2)}\n`);
56
+ return;
57
+ }
58
+ const firstSeq = Number(info?.state?.first_seq ?? 0);
59
+ const lastSeq = Number(info?.state?.last_seq ?? 0);
60
+ let scanned = 0;
61
+ let matched = 0;
62
+ let replayed = 0;
63
+ let failed = 0;
64
+ let purged = 0;
65
+ for (let seq = firstSeq; seq <= lastSeq; seq += 1) {
66
+ if (limit > 0 && replayed >= limit)
67
+ break;
68
+ let storedRaw;
69
+ try {
70
+ storedRaw = await jsm.streams.getMessage(streamName, { seq });
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ const stored = asStoredMessage(storedRaw);
76
+ if (!stored)
77
+ continue;
78
+ if (stored.subject !== opts.dlqTopic)
79
+ continue;
80
+ scanned += 1;
81
+ let decoded = null;
82
+ try {
83
+ decoded = decodeJson(stored.data);
84
+ }
85
+ catch {
86
+ decoded = null;
87
+ }
88
+ const meta = extractDlqMeta(decoded, stored.time);
89
+ if (!matchesDlqFilter(meta, filter))
90
+ continue;
91
+ matched += 1;
92
+ try {
93
+ const payloadBytes = replayBytesFromDlqPayload(decoded, stored.data);
94
+ await publishSubject(nc, opts.targetTopic, payloadBytes, {
95
+ durable: durablePublish,
96
+ dedupeKey: `replay-${opts.targetTopic}-${seq}-${Date.now()}-${randomId()}`,
97
+ timeoutMs: config.runtime.publishDurableTimeoutMs
98
+ });
99
+ replayed += 1;
100
+ if (opts.purgeReplayed) {
101
+ try {
102
+ await jsm.streams.deleteMessage(streamName, seq);
103
+ purged += 1;
104
+ }
105
+ catch {
106
+ // best effort purge
107
+ }
108
+ }
109
+ }
110
+ catch (err) {
111
+ failed += 1;
112
+ process.stderr.write(`dlq replay failed (seq=${seq}): ${err.message}\n`);
113
+ }
114
+ }
115
+ await closeBus(config.natsUrl);
116
+ process.stdout.write(`${JSON.stringify({
117
+ ok: true,
118
+ replayed,
119
+ failed,
120
+ purged,
121
+ scanned,
122
+ matched,
123
+ dlq_topic: opts.dlqTopic,
124
+ target_topic: opts.targetTopic,
125
+ stream: streamName
126
+ }, null, 2)}\n`);
127
+ }
@@ -0,0 +1,6 @@
1
+ import { Logger } from "../logger.js";
2
+ import { OrbitConfig } from "../types.js";
3
+ export declare function cmdInspect(config: OrbitConfig, logger: Logger, opts: {
4
+ service: string;
5
+ timeoutMs?: number;
6
+ }): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { createEnvelope, validateEnvelope } from "../envelope.js";
2
+ import { closeBus, connectBus, decodeJson, encodeJson } from "../nats.js";
3
+ import { loadServiceRecordDistributed } from "../registry.js";
4
+ import { canUseAgent, requestAgent } from "../agent_ipc.js";
5
+ import { prefixedSubject } from "../subjects.js";
6
+ export async function cmdInspect(config, logger, opts) {
7
+ if (canUseAgent(config)) {
8
+ try {
9
+ const payload = await requestAgent(config, "inspect", { service: opts.service, timeoutMs: opts.timeoutMs ?? config.requestTimeoutMs }, opts.timeoutMs ?? config.requestTimeoutMs);
10
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
11
+ return;
12
+ }
13
+ catch (err) {
14
+ logger.warn("agent inspect failed, falling back to direct NATS path", { service: opts.service, err: String(err) });
15
+ }
16
+ }
17
+ try {
18
+ const nc = await connectBus(config.natsUrl);
19
+ try {
20
+ const msg = await nc.request(prefixedSubject(config, "inspect", opts.service), encodeJson(createEnvelope({ kind: "request", payload: {} })), { timeout: opts.timeoutMs ?? config.requestTimeoutMs });
21
+ const env = validateEnvelope(decodeJson(msg.data), { skipHashCheck: config.performance.trustedLocal });
22
+ process.stdout.write(`${JSON.stringify(env.payload, null, 2)}\n`);
23
+ return;
24
+ }
25
+ catch {
26
+ try {
27
+ const msg = await nc.request(`$SRV.INFO.${opts.service}`, encodeJson({}), {
28
+ timeout: opts.timeoutMs ?? config.requestTimeoutMs
29
+ });
30
+ const payload = decodeJson(msg.data);
31
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
32
+ return;
33
+ }
34
+ catch {
35
+ const msg = await nc.request(prefixedSubject(config, "discovery", "query"), encodeJson({ service: opts.service }), {
36
+ timeout: opts.timeoutMs ?? config.requestTimeoutMs
37
+ });
38
+ const env = validateEnvelope(decodeJson(msg.data), { skipHashCheck: config.performance.trustedLocal });
39
+ process.stdout.write(`${JSON.stringify(env.payload, null, 2)}\n`);
40
+ return;
41
+ }
42
+ }
43
+ finally {
44
+ await closeBus(config.natsUrl);
45
+ }
46
+ }
47
+ catch (err) {
48
+ logger.warn("inspect live request failed, falling back to local registry", { service: opts.service, err: String(err) });
49
+ }
50
+ const local = await loadServiceRecordDistributed(config, opts.service);
51
+ if (!local) {
52
+ process.stdout.write(`${JSON.stringify({ ok: false, error: `service ${opts.service} not found` }, null, 2)}\n`);
53
+ process.exitCode = 2;
54
+ return;
55
+ }
56
+ process.stdout.write(`${JSON.stringify(local, null, 2)}\n`);
57
+ }