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,92 @@
1
+ import { OrbitError } from "./errors.js";
2
+ import { randomId, sha256, stableStringify } from "./util.js";
3
+ const SCHEMA_VERSION = "1.0";
4
+ export function hashEnvelopeFields(input) {
5
+ return sha256(stableStringify(input));
6
+ }
7
+ export function createEnvelope(params) {
8
+ const base = {
9
+ id: randomId(),
10
+ run_id: params.runId ?? randomId(),
11
+ ts: new Date().toISOString(),
12
+ kind: params.kind,
13
+ schema_version: SCHEMA_VERSION,
14
+ payload: params.payload,
15
+ data_pack: params.dataPack,
16
+ provenance: params.provenance,
17
+ cost: params.cost,
18
+ a2a: params.a2a
19
+ };
20
+ return { ...base, hash: hashEnvelopeFields(base) };
21
+ }
22
+ function validateA2A(input) {
23
+ if (input === undefined)
24
+ return undefined;
25
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
26
+ throw new OrbitError("INVALID_ENVELOPE", "a2a must be an object");
27
+ }
28
+ const raw = input;
29
+ const strOrUndefined = (value, field) => {
30
+ if (value === undefined)
31
+ return undefined;
32
+ if (typeof value !== "string" || !value) {
33
+ throw new OrbitError("INVALID_ENVELOPE", `a2a.${field} must be a non-empty string`);
34
+ }
35
+ return value;
36
+ };
37
+ if (raw.capabilities !== undefined) {
38
+ if (!Array.isArray(raw.capabilities) || raw.capabilities.some((v) => typeof v !== "string" || !v)) {
39
+ throw new OrbitError("INVALID_ENVELOPE", "a2a.capabilities must be a string[]");
40
+ }
41
+ }
42
+ return {
43
+ task_id: strOrUndefined(raw.task_id, "task_id"),
44
+ thread_id: strOrUndefined(raw.thread_id, "thread_id"),
45
+ parent_message_id: strOrUndefined(raw.parent_message_id, "parent_message_id"),
46
+ capabilities: raw.capabilities,
47
+ traceparent: strOrUndefined(raw.traceparent, "traceparent"),
48
+ dedupe_key: strOrUndefined(raw.dedupe_key, "dedupe_key")
49
+ };
50
+ }
51
+ export function validateEnvelope(input, options) {
52
+ if (!input || typeof input !== "object") {
53
+ throw new OrbitError("INVALID_ENVELOPE", "Envelope must be an object");
54
+ }
55
+ const env = input;
56
+ const required = ["id", "run_id", "ts", "kind", "schema_version", "payload", "hash"];
57
+ for (const key of required) {
58
+ if (!(key in env))
59
+ throw new OrbitError("INVALID_ENVELOPE", `Missing envelope field: ${key}`);
60
+ }
61
+ if (typeof env.id !== "string" || !env.id)
62
+ throw new OrbitError("INVALID_ENVELOPE", "id must be a string");
63
+ if (typeof env.run_id !== "string" || !env.run_id)
64
+ throw new OrbitError("INVALID_ENVELOPE", "run_id must be a string");
65
+ if (typeof env.ts !== "string" || Number.isNaN(Date.parse(env.ts)))
66
+ throw new OrbitError("INVALID_ENVELOPE", "ts must be ISO timestamp");
67
+ if (typeof env.kind !== "string")
68
+ throw new OrbitError("INVALID_ENVELOPE", "kind must be string");
69
+ if (typeof env.schema_version !== "string")
70
+ throw new OrbitError("INVALID_ENVELOPE", "schema_version must be string");
71
+ if (typeof env.hash !== "string" || !env.hash)
72
+ throw new OrbitError("INVALID_ENVELOPE", "hash must be string");
73
+ const candidate = {
74
+ id: env.id,
75
+ run_id: env.run_id,
76
+ ts: env.ts,
77
+ kind: env.kind,
78
+ schema_version: env.schema_version,
79
+ payload: env.payload,
80
+ data_pack: env.data_pack,
81
+ provenance: env.provenance,
82
+ cost: env.cost,
83
+ a2a: validateA2A(env.a2a)
84
+ };
85
+ if (!options?.skipHashCheck && hashEnvelopeFields(candidate) !== env.hash) {
86
+ throw new OrbitError("INVALID_ENVELOPE_HASH", "Envelope hash mismatch");
87
+ }
88
+ return {
89
+ ...env,
90
+ a2a: candidate.a2a
91
+ };
92
+ }
@@ -0,0 +1,5 @@
1
+ export declare class OrbitError extends Error {
2
+ readonly code: string;
3
+ readonly details?: unknown;
4
+ constructor(code: string, message: string, details?: unknown);
5
+ }
@@ -0,0 +1,9 @@
1
+ export class OrbitError extends Error {
2
+ code;
3
+ details;
4
+ constructor(code, message, details) {
5
+ super(message);
6
+ this.code = code;
7
+ this.details = details;
8
+ }
9
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { run } from "./cli.js";
4
+ import { OrbitError } from "./errors.js";
5
+ run(process.argv.slice(2), process.cwd()).catch((err) => {
6
+ if (err instanceof OrbitError) {
7
+ process.stderr.write(`${JSON.stringify({ ok: false, code: err.code, message: err.message, details: err.details })}\n`);
8
+ process.exit(2);
9
+ }
10
+ process.stderr.write(`${JSON.stringify({ ok: false, code: "UNHANDLED", message: err.message })}\n`);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,4 @@
1
+ export declare function defaultDurableStreamName(subject: string): string;
2
+ export declare function defaultDurableConsumerName(subject: string): string;
3
+ export declare function ensureStreamSubjects(jsm: any, streamName: string, subjects: string[]): Promise<void>;
4
+ export declare function ensureDurableConsumer(jsm: any, streamName: string, durableName: string, filterSubject: string, ackWaitMs: number, maxDeliver: number): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import { AckPolicy, DeliverPolicy } from "nats";
2
+ function sanitizeToken(input) {
3
+ return input.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80) || "subject";
4
+ }
5
+ export function defaultDurableStreamName(subject) {
6
+ return `orbit_${sanitizeToken(subject)}_stream`;
7
+ }
8
+ export function defaultDurableConsumerName(subject) {
9
+ return `orbit_${sanitizeToken(subject)}_consumer`;
10
+ }
11
+ export async function ensureStreamSubjects(jsm, streamName, subjects) {
12
+ const unique = Array.from(new Set(subjects.filter(Boolean)));
13
+ if (unique.length === 0)
14
+ return;
15
+ try {
16
+ const info = await jsm.streams.info(streamName);
17
+ const existing = Array.isArray(info?.config?.subjects) ? info.config.subjects : [];
18
+ const merged = Array.from(new Set([...existing, ...unique]));
19
+ if (merged.length !== existing.length) {
20
+ await jsm.streams.update(streamName, {
21
+ ...info.config,
22
+ subjects: merged
23
+ });
24
+ }
25
+ return;
26
+ }
27
+ catch {
28
+ // stream missing; create
29
+ }
30
+ await jsm.streams.add({
31
+ name: streamName,
32
+ subjects: unique
33
+ });
34
+ }
35
+ export async function ensureDurableConsumer(jsm, streamName, durableName, filterSubject, ackWaitMs, maxDeliver) {
36
+ try {
37
+ await jsm.consumers.info(streamName, durableName);
38
+ return;
39
+ }
40
+ catch {
41
+ // consumer missing; create
42
+ }
43
+ await jsm.consumers.add(streamName, {
44
+ durable_name: durableName,
45
+ ack_policy: AckPolicy.Explicit,
46
+ deliver_policy: DeliverPolicy.All,
47
+ filter_subject: filterSubject,
48
+ ack_wait: Math.max(1, Math.floor(ackWaitMs)) * 1_000_000,
49
+ max_deliver: Math.max(1, Math.floor(maxDeliver))
50
+ });
51
+ }
@@ -0,0 +1,6 @@
1
+ export interface JsonSchemaValidationIssue {
2
+ path: string;
3
+ message: string;
4
+ }
5
+ export declare function validateJsonSchema(value: unknown, schema: unknown): JsonSchemaValidationIssue[];
6
+ export declare function assertJsonSchema(value: unknown, schema: unknown, context: string): void;
@@ -0,0 +1,154 @@
1
+ import { OrbitError } from "./errors.js";
2
+ function isObject(value) {
3
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function asSchema(value) {
6
+ return isObject(value) ? value : null;
7
+ }
8
+ function typeMatches(value, expected) {
9
+ switch (expected) {
10
+ case "string":
11
+ return typeof value === "string";
12
+ case "number":
13
+ return typeof value === "number" && Number.isFinite(value);
14
+ case "integer":
15
+ return typeof value === "number" && Number.isInteger(value);
16
+ case "boolean":
17
+ return typeof value === "boolean";
18
+ case "object":
19
+ return isObject(value);
20
+ case "array":
21
+ return Array.isArray(value);
22
+ case "null":
23
+ return value === null;
24
+ default:
25
+ return true;
26
+ }
27
+ }
28
+ function pushIssue(issues, path, message) {
29
+ issues.push({ path, message });
30
+ }
31
+ function validateType(value, schema, path, issues) {
32
+ const rawType = schema.type;
33
+ if (typeof rawType === "string") {
34
+ if (!typeMatches(value, rawType)) {
35
+ pushIssue(issues, path, `expected type ${rawType}`);
36
+ return false;
37
+ }
38
+ return true;
39
+ }
40
+ if (Array.isArray(rawType) && rawType.length > 0) {
41
+ const expected = rawType.filter((v) => typeof v === "string");
42
+ if (expected.length > 0 && !expected.some((t) => typeMatches(value, t))) {
43
+ pushIssue(issues, path, `expected one of types: ${expected.join(", ")}`);
44
+ return false;
45
+ }
46
+ }
47
+ return true;
48
+ }
49
+ function validateEnum(value, schema, path, issues) {
50
+ const options = schema.enum;
51
+ if (!Array.isArray(options) || options.length === 0)
52
+ return;
53
+ const ok = options.some((option) => JSON.stringify(option) === JSON.stringify(value));
54
+ if (!ok)
55
+ pushIssue(issues, path, "value not present in enum");
56
+ }
57
+ function validateString(value, schema, path, issues) {
58
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) {
59
+ pushIssue(issues, path, `string length must be >= ${schema.minLength}`);
60
+ }
61
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
62
+ pushIssue(issues, path, `string length must be <= ${schema.maxLength}`);
63
+ }
64
+ if (typeof schema.pattern === "string") {
65
+ try {
66
+ const re = new RegExp(schema.pattern);
67
+ if (!re.test(value))
68
+ pushIssue(issues, path, `string does not match pattern ${schema.pattern}`);
69
+ }
70
+ catch {
71
+ pushIssue(issues, path, `invalid regex pattern ${schema.pattern}`);
72
+ }
73
+ }
74
+ }
75
+ function validateNumber(value, schema, path, issues) {
76
+ if (typeof schema.minimum === "number" && value < schema.minimum) {
77
+ pushIssue(issues, path, `number must be >= ${schema.minimum}`);
78
+ }
79
+ if (typeof schema.maximum === "number" && value > schema.maximum) {
80
+ pushIssue(issues, path, `number must be <= ${schema.maximum}`);
81
+ }
82
+ }
83
+ function validateArray(value, schema, path, issues) {
84
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) {
85
+ pushIssue(issues, path, `array length must be >= ${schema.minItems}`);
86
+ }
87
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
88
+ pushIssue(issues, path, `array length must be <= ${schema.maxItems}`);
89
+ }
90
+ const itemSchema = asSchema(schema.items);
91
+ if (!itemSchema)
92
+ return;
93
+ for (let i = 0; i < value.length; i += 1) {
94
+ validateAgainstSchema(value[i], itemSchema, `${path}[${i}]`, issues);
95
+ }
96
+ }
97
+ function validateObject(value, schema, path, issues) {
98
+ const required = Array.isArray(schema.required) ? schema.required.filter((v) => typeof v === "string") : [];
99
+ for (const key of required) {
100
+ if (!(key in value)) {
101
+ pushIssue(issues, `${path}.${key}`, "required property is missing");
102
+ }
103
+ }
104
+ const props = asSchema(schema.properties) ?? {};
105
+ const allowAdditional = schema.additionalProperties !== false;
106
+ for (const key of Object.keys(value)) {
107
+ const childPath = `${path}.${key}`;
108
+ const childSchema = asSchema(props[key]);
109
+ if (!childSchema) {
110
+ if (!allowAdditional)
111
+ pushIssue(issues, childPath, "additional properties are not allowed");
112
+ continue;
113
+ }
114
+ validateAgainstSchema(value[key], childSchema, childPath, issues);
115
+ }
116
+ }
117
+ function validateAgainstSchema(value, schema, path, issues) {
118
+ validateEnum(value, schema, path, issues);
119
+ if (Object.prototype.hasOwnProperty.call(schema, "const")) {
120
+ const constValue = schema.const;
121
+ if (JSON.stringify(constValue) !== JSON.stringify(value)) {
122
+ pushIssue(issues, path, "value does not match const");
123
+ }
124
+ }
125
+ const typeOk = validateType(value, schema, path, issues);
126
+ if (!typeOk)
127
+ return;
128
+ if (typeof value === "string") {
129
+ validateString(value, schema, path, issues);
130
+ }
131
+ else if (typeof value === "number") {
132
+ validateNumber(value, schema, path, issues);
133
+ }
134
+ else if (Array.isArray(value)) {
135
+ validateArray(value, schema, path, issues);
136
+ }
137
+ else if (isObject(value)) {
138
+ validateObject(value, schema, path, issues);
139
+ }
140
+ }
141
+ export function validateJsonSchema(value, schema) {
142
+ const schemaObj = asSchema(schema);
143
+ if (!schemaObj)
144
+ return [];
145
+ const issues = [];
146
+ validateAgainstSchema(value, schemaObj, "$", issues);
147
+ return issues;
148
+ }
149
+ export function assertJsonSchema(value, schema, context) {
150
+ const issues = validateJsonSchema(value, schema);
151
+ if (issues.length === 0)
152
+ return;
153
+ throw new OrbitError("SCHEMA_VALIDATION_FAILED", `${context} failed schema validation`, { issues });
154
+ }
@@ -0,0 +1,16 @@
1
+ declare const LEVELS: {
2
+ readonly debug: 10;
3
+ readonly info: 20;
4
+ readonly warn: 30;
5
+ readonly error: 40;
6
+ };
7
+ export declare class Logger {
8
+ private readonly level;
9
+ constructor(level: keyof typeof LEVELS);
10
+ private emit;
11
+ debug(msg: string, ctx?: Record<string, unknown>): void;
12
+ info(msg: string, ctx?: Record<string, unknown>): void;
13
+ warn(msg: string, ctx?: Record<string, unknown>): void;
14
+ error(msg: string, ctx?: Record<string, unknown>): void;
15
+ }
16
+ export {};
@@ -0,0 +1,31 @@
1
+ import process from "node:process";
2
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
3
+ export class Logger {
4
+ level;
5
+ constructor(level) {
6
+ this.level = level;
7
+ }
8
+ emit(level, msg, ctx) {
9
+ if (LEVELS[level] < LEVELS[this.level])
10
+ return;
11
+ const row = {
12
+ ts: new Date().toISOString(),
13
+ level,
14
+ msg,
15
+ ...(ctx ?? {})
16
+ };
17
+ process.stderr.write(`${JSON.stringify(row)}\n`);
18
+ }
19
+ debug(msg, ctx) {
20
+ this.emit("debug", msg, ctx);
21
+ }
22
+ info(msg, ctx) {
23
+ this.emit("info", msg, ctx);
24
+ }
25
+ warn(msg, ctx) {
26
+ this.emit("warn", msg, ctx);
27
+ }
28
+ error(msg, ctx) {
29
+ this.emit("error", msg, ctx);
30
+ }
31
+ }
@@ -0,0 +1,6 @@
1
+ type Labels = Record<string, string | number | boolean>;
2
+ export declare function incCounter(name: string, delta?: number, labels?: Labels): void;
3
+ export declare function setGauge(name: string, value: number, labels?: Labels): void;
4
+ export declare function observeHistogram(name: string, value: number, labels?: Labels, buckets?: number[]): void;
5
+ export declare function renderPrometheusMetrics(): string;
6
+ export {};
@@ -0,0 +1,95 @@
1
+ const metrics = new Map();
2
+ const defaultBucketsMs = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
3
+ function normalizeLabels(labels) {
4
+ if (!labels)
5
+ return [];
6
+ return Object.entries(labels)
7
+ .map(([k, v]) => [k, String(v)])
8
+ .sort((a, b) => a[0].localeCompare(b[0]));
9
+ }
10
+ function labelKey(labels) {
11
+ return JSON.stringify(normalizeLabels(labels));
12
+ }
13
+ function renderLabelBlock(labelsKey) {
14
+ const labels = JSON.parse(labelsKey);
15
+ if (labels.length === 0)
16
+ return "";
17
+ const inner = labels.map(([k, v]) => `${k}="${v.replaceAll('"', '\\"')}"`).join(",");
18
+ return `{${inner}}`;
19
+ }
20
+ function ensureCounter(name) {
21
+ const existing = metrics.get(name);
22
+ if (existing && existing.type === "counter")
23
+ return existing;
24
+ const created = { type: "counter", values: new Map() };
25
+ metrics.set(name, created);
26
+ return created;
27
+ }
28
+ function ensureGauge(name) {
29
+ const existing = metrics.get(name);
30
+ if (existing && existing.type === "gauge")
31
+ return existing;
32
+ const created = { type: "gauge", values: new Map() };
33
+ metrics.set(name, created);
34
+ return created;
35
+ }
36
+ function ensureHistogram(name) {
37
+ const existing = metrics.get(name);
38
+ if (existing && existing.type === "histogram")
39
+ return existing;
40
+ const created = { type: "histogram", values: new Map() };
41
+ metrics.set(name, created);
42
+ return created;
43
+ }
44
+ export function incCounter(name, delta = 1, labels) {
45
+ const metric = ensureCounter(name);
46
+ const key = labelKey(labels);
47
+ metric.values.set(key, (metric.values.get(key) ?? 0) + delta);
48
+ }
49
+ export function setGauge(name, value, labels) {
50
+ const metric = ensureGauge(name);
51
+ metric.values.set(labelKey(labels), value);
52
+ }
53
+ export function observeHistogram(name, value, labels, buckets = defaultBucketsMs) {
54
+ const metric = ensureHistogram(name);
55
+ const key = labelKey(labels);
56
+ let series = metric.values.get(key);
57
+ if (!series) {
58
+ series = {
59
+ buckets: buckets.map((le) => ({ le, count: 0 })),
60
+ sum: 0,
61
+ count: 0
62
+ };
63
+ metric.values.set(key, series);
64
+ }
65
+ series.sum += value;
66
+ series.count += 1;
67
+ for (const bucket of series.buckets) {
68
+ if (value <= bucket.le)
69
+ bucket.count += 1;
70
+ }
71
+ }
72
+ export function renderPrometheusMetrics() {
73
+ const rows = [];
74
+ for (const [name, metric] of metrics.entries()) {
75
+ rows.push(`# TYPE ${name} ${metric.type}`);
76
+ if (metric.type === "counter" || metric.type === "gauge") {
77
+ for (const [labels, value] of metric.values.entries()) {
78
+ rows.push(`${name}${renderLabelBlock(labels)} ${value}`);
79
+ }
80
+ continue;
81
+ }
82
+ for (const [labels, series] of metric.values.entries()) {
83
+ const parsed = JSON.parse(labels);
84
+ for (const bucket of series.buckets) {
85
+ const bucketLabels = JSON.stringify([...parsed, ["le", String(bucket.le)]].sort((a, b) => a[0].localeCompare(b[0])));
86
+ rows.push(`${name}_bucket${renderLabelBlock(bucketLabels)} ${bucket.count}`);
87
+ }
88
+ const infLabels = JSON.stringify([...parsed, ["le", "+Inf"]].sort((a, b) => a[0].localeCompare(b[0])));
89
+ rows.push(`${name}_bucket${renderLabelBlock(infLabels)} ${series.count}`);
90
+ rows.push(`${name}_sum${renderLabelBlock(labels)} ${series.sum}`);
91
+ rows.push(`${name}_count${renderLabelBlock(labels)} ${series.count}`);
92
+ }
93
+ }
94
+ return `${rows.join("\n")}\n`;
95
+ }
@@ -0,0 +1,16 @@
1
+ import { NatsConnection } from "nats";
2
+ export declare function connectBus(url: string): Promise<NatsConnection>;
3
+ export declare function closeBus(url: string): Promise<void>;
4
+ export declare function encodeJson(value: unknown): Uint8Array;
5
+ export declare function decodeJson(value: Uint8Array): unknown;
6
+ export declare function kvPut(nc: NatsConnection, bucket: string, key: string, value: unknown): Promise<void>;
7
+ export declare function kvGet<T>(nc: NatsConnection, bucket: string, key: string): Promise<T | null>;
8
+ export declare function osPut(nc: NatsConnection, bucket: string, key: string, body: Uint8Array, options?: {
9
+ description?: string;
10
+ headers?: Record<string, string>;
11
+ }): Promise<void>;
12
+ export declare function publishSubject(nc: NatsConnection, subject: string, payload: Uint8Array, options?: {
13
+ durable?: boolean;
14
+ dedupeKey?: string;
15
+ timeoutMs?: number;
16
+ }): Promise<void>;
@@ -0,0 +1,129 @@
1
+ import { connect, JSONCodec } from "nats";
2
+ import { OrbitError } from "./errors.js";
3
+ const jc = JSONCodec();
4
+ const connectionCache = new Map();
5
+ const kvViewCache = new WeakMap();
6
+ const osViewCache = new WeakMap();
7
+ export async function connectBus(url) {
8
+ const cached = connectionCache.get(url);
9
+ if (cached)
10
+ return cached;
11
+ const pending = connect({ servers: url })
12
+ .then((nc) => {
13
+ const remove = () => {
14
+ if (connectionCache.get(url) === pending)
15
+ connectionCache.delete(url);
16
+ };
17
+ nc.closed().then(remove).catch(remove);
18
+ return nc;
19
+ })
20
+ .catch((err) => {
21
+ if (connectionCache.get(url) === pending)
22
+ connectionCache.delete(url);
23
+ throw err;
24
+ });
25
+ connectionCache.set(url, pending);
26
+ return pending;
27
+ }
28
+ export async function closeBus(url) {
29
+ const pending = connectionCache.get(url);
30
+ if (!pending)
31
+ return;
32
+ connectionCache.delete(url);
33
+ const nc = await pending;
34
+ await nc.drain();
35
+ }
36
+ export function encodeJson(value) {
37
+ return jc.encode(value);
38
+ }
39
+ export function decodeJson(value) {
40
+ return jc.decode(value);
41
+ }
42
+ async function kvView(nc, bucket) {
43
+ const js = nc.jetstream();
44
+ let perConn = kvViewCache.get(nc);
45
+ if (!perConn) {
46
+ perConn = new Map();
47
+ kvViewCache.set(nc, perConn);
48
+ }
49
+ let kv = perConn.get(bucket);
50
+ if (!kv) {
51
+ kv = await js.views.kv(bucket, { history: 1 });
52
+ perConn.set(bucket, kv);
53
+ }
54
+ return kv;
55
+ }
56
+ async function osView(nc, bucket) {
57
+ const js = nc.jetstream();
58
+ let perConn = osViewCache.get(nc);
59
+ if (!perConn) {
60
+ perConn = new Map();
61
+ osViewCache.set(nc, perConn);
62
+ }
63
+ let os = perConn.get(bucket);
64
+ if (!os) {
65
+ os = await js.views.os(bucket);
66
+ perConn.set(bucket, os);
67
+ }
68
+ return os;
69
+ }
70
+ export async function kvPut(nc, bucket, key, value) {
71
+ const kv = await kvView(nc, bucket);
72
+ await kv.put(key, encodeJson(value));
73
+ }
74
+ export async function kvGet(nc, bucket, key) {
75
+ const kv = await kvView(nc, bucket);
76
+ const entry = await kv.get(key);
77
+ if (!entry)
78
+ return null;
79
+ return decodeJson(entry.value);
80
+ }
81
+ export async function osPut(nc, bucket, key, body, options) {
82
+ const os = await osView(nc, bucket);
83
+ try {
84
+ await os.put({
85
+ name: key,
86
+ data: body,
87
+ description: options?.description,
88
+ headers: options?.headers
89
+ });
90
+ return;
91
+ }
92
+ catch {
93
+ // Fallback signature for client variants.
94
+ }
95
+ try {
96
+ await os.put(key, body, options);
97
+ return;
98
+ }
99
+ catch {
100
+ // final fallback
101
+ }
102
+ await os.put({ name: key }, body);
103
+ }
104
+ export async function publishSubject(nc, subject, payload, options) {
105
+ if (!options?.durable) {
106
+ nc.publish(subject, payload);
107
+ return;
108
+ }
109
+ const js = nc.jetstream();
110
+ const publishPromise = js.publish(subject, payload, options.dedupeKey ? { msgID: options.dedupeKey } : undefined);
111
+ if (!options.timeoutMs || options.timeoutMs <= 0) {
112
+ await publishPromise;
113
+ return;
114
+ }
115
+ let timer;
116
+ const timeoutPromise = new Promise((_, reject) => {
117
+ timer = setTimeout(() => {
118
+ reject(new OrbitError("PUBLISH_TIMEOUT", `durable publish ack timed out after ${options.timeoutMs}ms for subject ${subject}`));
119
+ }, options.timeoutMs);
120
+ timer.unref?.();
121
+ });
122
+ try {
123
+ await Promise.race([publishPromise, timeoutPromise]);
124
+ }
125
+ finally {
126
+ if (timer)
127
+ clearTimeout(timer);
128
+ }
129
+ }
@@ -0,0 +1,4 @@
1
+ import { NatsConnection } from "nats";
2
+ import { OrbitConfig } from "./types.js";
3
+ import { OrbitApiAction } from "./api_contract.js";
4
+ export declare function executeOrbitAction(config: OrbitConfig, nc: NatsConnection, action: OrbitApiAction, payload: Record<string, unknown>, actor: "api" | "agent"): Promise<unknown>;