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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +501 -0
- package/dist/src/agent_ipc.d.ts +4 -0
- package/dist/src/agent_ipc.js +77 -0
- package/dist/src/api_contract.d.ts +19 -0
- package/dist/src/api_contract.js +81 -0
- package/dist/src/api_http.d.ts +23 -0
- package/dist/src/api_http.js +62 -0
- package/dist/src/call_protection.d.ts +5 -0
- package/dist/src/call_protection.js +83 -0
- package/dist/src/cell/gateway.d.ts +13 -0
- package/dist/src/cell/gateway.js +171 -0
- package/dist/src/cell/routing.d.ts +18 -0
- package/dist/src/cell/routing.js +48 -0
- package/dist/src/cell/template.d.ts +7 -0
- package/dist/src/cell/template.js +24 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +305 -0
- package/dist/src/commands/agent.d.ts +3 -0
- package/dist/src/commands/agent.js +187 -0
- package/dist/src/commands/api.d.ts +6 -0
- package/dist/src/commands/api.js +226 -0
- package/dist/src/commands/bench.d.ts +13 -0
- package/dist/src/commands/bench.js +125 -0
- package/dist/src/commands/bench_overhead.d.ts +8 -0
- package/dist/src/commands/bench_overhead.js +71 -0
- package/dist/src/commands/call.d.ts +10 -0
- package/dist/src/commands/call.js +45 -0
- package/dist/src/commands/cell.d.ts +3 -0
- package/dist/src/commands/cell.js +186 -0
- package/dist/src/commands/context.d.ts +9 -0
- package/dist/src/commands/context.js +71 -0
- package/dist/src/commands/dlq_inspect.d.ts +11 -0
- package/dist/src/commands/dlq_inspect.js +86 -0
- package/dist/src/commands/dlq_purge.d.ts +12 -0
- package/dist/src/commands/dlq_purge.js +84 -0
- package/dist/src/commands/dlq_replay.d.ts +15 -0
- package/dist/src/commands/dlq_replay.js +127 -0
- package/dist/src/commands/inspect.d.ts +6 -0
- package/dist/src/commands/inspect.js +57 -0
- package/dist/src/commands/monitor.d.ts +32 -0
- package/dist/src/commands/monitor.js +201 -0
- package/dist/src/commands/publish.d.ts +10 -0
- package/dist/src/commands/publish.js +64 -0
- package/dist/src/commands/serve.d.ts +8 -0
- package/dist/src/commands/serve.js +258 -0
- package/dist/src/commands/subscribe.d.ts +11 -0
- package/dist/src/commands/subscribe.js +78 -0
- package/dist/src/commands/trace.d.ts +5 -0
- package/dist/src/commands/trace.js +26 -0
- package/dist/src/commands/up.d.ts +3 -0
- package/dist/src/commands/up.js +91 -0
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.js +281 -0
- package/dist/src/dlq.d.ts +20 -0
- package/dist/src/dlq.js +71 -0
- package/dist/src/echo/benchmark.d.ts +10 -0
- package/dist/src/echo/benchmark.js +105 -0
- package/dist/src/echo/bus.d.ts +22 -0
- package/dist/src/echo/bus.js +89 -0
- package/dist/src/echo/cli.d.ts +1 -0
- package/dist/src/echo/cli.js +135 -0
- package/dist/src/echo/client.d.ts +12 -0
- package/dist/src/echo/client.js +46 -0
- package/dist/src/echo/daemon.d.ts +8 -0
- package/dist/src/echo/daemon.js +181 -0
- package/dist/src/echo/index.d.ts +6 -0
- package/dist/src/echo/index.js +5 -0
- package/dist/src/echo/ring_buffer.d.ts +27 -0
- package/dist/src/echo/ring_buffer.js +73 -0
- package/dist/src/echo/types.d.ts +27 -0
- package/dist/src/echo/types.js +1 -0
- package/dist/src/echocore.d.ts +2 -0
- package/dist/src/echocore.js +6 -0
- package/dist/src/envelope.d.ts +14 -0
- package/dist/src/envelope.js +92 -0
- package/dist/src/errors.d.ts +5 -0
- package/dist/src/errors.js +9 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +12 -0
- package/dist/src/jetstream_durable.d.ts +4 -0
- package/dist/src/jetstream_durable.js +51 -0
- package/dist/src/json_schema.d.ts +6 -0
- package/dist/src/json_schema.js +154 -0
- package/dist/src/logger.d.ts +16 -0
- package/dist/src/logger.js +31 -0
- package/dist/src/metrics.d.ts +6 -0
- package/dist/src/metrics.js +95 -0
- package/dist/src/nats.d.ts +16 -0
- package/dist/src/nats.js +129 -0
- package/dist/src/orbit_actions.d.ts +4 -0
- package/dist/src/orbit_actions.js +129 -0
- package/dist/src/otel.d.ts +2 -0
- package/dist/src/otel.js +96 -0
- package/dist/src/registry.d.ts +10 -0
- package/dist/src/registry.js +57 -0
- package/dist/src/retry.d.ts +9 -0
- package/dist/src/retry.js +32 -0
- package/dist/src/rpc_call.d.ts +22 -0
- package/dist/src/rpc_call.js +119 -0
- package/dist/src/service_adapter.d.ts +7 -0
- package/dist/src/service_adapter.js +163 -0
- package/dist/src/spec.d.ts +2 -0
- package/dist/src/spec.js +30 -0
- package/dist/src/subjects.d.ts +2 -0
- package/dist/src/subjects.js +4 -0
- package/dist/src/trace.d.ts +4 -0
- package/dist/src/trace.js +86 -0
- package/dist/src/types.d.ts +133 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util.d.ts +10 -0
- package/dist/src/util.js +63 -0
- package/dist/src/worker_pool.d.ts +9 -0
- package/dist/src/worker_pool.js +163 -0
- package/docs/orbit-api-contract.yaml +376 -0
- 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,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
|
+
}
|