@phosphorco/bb-thread-metrics 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bb.js ADDED
@@ -0,0 +1,208 @@
1
+ // src/bb.ts
2
+ import { z } from "zod";
3
+ import { RECENT_LIMITS, recentSnapshotInputSchema, validateRecentSnapshot, LIMITS, METHODS, describeInputSchema, describeEnvelopeSchema, capabilitiesSchema, readInputSchema, detailInputSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult, classifyOwnerError } from "./index.js";
4
+ function sdkSchema(schema, maxBytes) {
5
+ return { "~standard": { version: 1, vendor: "bb-thread-metrics", jsonSchema: { input: () => z.toJSONSchema(schema, { io: "input", unrepresentable: "any" }), output: () => z.toJSONSchema(schema, { io: "output", unrepresentable: "any" }) }, validate(value) {
6
+ try {
7
+ return { value: maxBytes === undefined ? schema.parse(value) : parseBounded(schema, value, maxBytes) };
8
+ } catch (e) {
9
+ return { issues: e instanceof z.ZodError ? e.issues.map((i) => ({ message: i.message, path: i.path })) : [{ message: "Payload byte limit" }] };
10
+ }
11
+ } } };
12
+ }
13
+ function registerThreadMetricsOwner(bb, port) {
14
+ const capabilities = capabilitiesSchema.parse(port.capabilities);
15
+ if (Boolean(capabilities.recentSnapshotV1) !== Boolean(port.recentSnapshot))
16
+ throw new Error("Recent snapshot capability/handler mismatch");
17
+ if (capabilities.detail !== Boolean(port.detail))
18
+ throw new Error("Detail capability/handler mismatch");
19
+ const describe = () => ({ protocol: "bb-thread-metrics", versions: [1], capabilities });
20
+ const contract = {
21
+ threadMetricsDescribe: { input: sdkSchema(describeInputSchema, LIMITS.inputBytes), output: sdkSchema(z.unknown()) },
22
+ threadMetricsReadV1: { input: sdkSchema(readInputSchema, LIMITS.inputBytes), output: sdkSchema(z.unknown()) }
23
+ };
24
+ const handlers = {
25
+ threadMetricsDescribe: (raw) => {
26
+ parseBounded(describeInputSchema, raw, LIMITS.inputBytes);
27
+ return describe();
28
+ },
29
+ threadMetricsReadV1: async (raw) => {
30
+ const input = parseBounded(readInputSchema, raw, LIMITS.inputBytes);
31
+ if (input.series && !capabilities.series)
32
+ throw new Error("Series not supported");
33
+ return validateReadResult(input, await port.read(input));
34
+ }
35
+ };
36
+ bb.rpc.register(contract, handlers, { experimental_discoverable: true });
37
+ if (port.recentSnapshot)
38
+ bb.rpc.register({ threadMetricsRecentSnapshotV1: { input: sdkSchema(recentSnapshotInputSchema, RECENT_LIMITS.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsRecentSnapshotV1: async (raw) => {
39
+ const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
40
+ return validateRecentSnapshot(input, await port.recentSnapshot(input));
41
+ } }, { experimental_discoverable: true });
42
+ if (port.detail)
43
+ bb.rpc.register({ threadMetricsDetailV1: { input: sdkSchema(detailInputSchema, LIMITS.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsDetailV1: async (raw) => {
44
+ const input = parseBounded(detailInputSchema, raw, LIMITS.inputBytes);
45
+ return validateDetailResult(input, await port.detail(input));
46
+ } }, { experimental_discoverable: true });
47
+ }
48
+ function createThreadMetricsClient(sdk, pluginId) {
49
+ if (!pluginId || pluginId.length > 200)
50
+ throw new Error("Invalid target plugin id");
51
+ async function call(method, input, signal) {
52
+ return sdk.plugins.callRpc({ pluginId, method, input, outputSchema: z.unknown(), ...signal ? { signal } : {} });
53
+ }
54
+ return {
55
+ pluginId,
56
+ async describe(signal) {
57
+ const raw = await call(METHODS.describe, {}, signal);
58
+ return negotiateVersion(parseBounded(describeEnvelopeSchema, raw, LIMITS.describeBytes));
59
+ },
60
+ async recentSnapshot(raw = {}, signal) {
61
+ const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
62
+ return validateRecentSnapshot(input, await call(METHODS.recentSnapshot, input, signal));
63
+ },
64
+ async read(raw, signal) {
65
+ const input = parseBounded(readInputSchema, raw, LIMITS.inputBytes);
66
+ return validateReadResult(input, await call(METHODS.read, input, signal));
67
+ },
68
+ async detail(raw, signal) {
69
+ const input = parseBounded(detailInputSchema, raw, LIMITS.inputBytes);
70
+ return validateDetailResult(input, await call(METHODS.detail, input, signal));
71
+ }
72
+ };
73
+ }
74
+ async function discoverThreadMetricsOwners(options) {
75
+ const bounded = (v, max) => {
76
+ const n = v ?? max;
77
+ if (!Number.isInteger(n) || n < 1 || n > max)
78
+ throw new Error("Invalid discovery budget");
79
+ return n;
80
+ };
81
+ const count = bounded(options.maxCandidates, LIMITS.candidates), concurrency = bounded(options.concurrency, LIMITS.concurrency), callMs = bounded(options.callMs, LIMITS.callMs), deadlineMs = bounded(options.deadlineMs, LIMITS.discoveryMs);
82
+ const pass = new AbortController, rows = new Map, active = new Map;
83
+ let done = false, ending = false, listedReady = false, omittedCount = 0, eligibleCount = 0, started = 0;
84
+ const timers = new Set;
85
+ let finish;
86
+ const result = new Promise((resolve) => {
87
+ finish = resolve;
88
+ });
89
+ const publish = (row) => {
90
+ if (done)
91
+ return;
92
+ rows.set(row.pluginId, row);
93
+ try {
94
+ options.onRow?.(row);
95
+ } catch {}
96
+ };
97
+ const end = (complete, listError) => {
98
+ if (done || ending)
99
+ return;
100
+ ending = true;
101
+ for (const [pluginId] of active)
102
+ if (!rows.has(pluginId))
103
+ publish({ pluginId, state: "error", error: options.signal?.aborted ? "aborted" : "timeout" });
104
+ done = true;
105
+ pass.abort();
106
+ for (const c of active.values())
107
+ c.abort();
108
+ for (const t of timers)
109
+ clearTimeout(t);
110
+ timers.clear();
111
+ options.signal?.removeEventListener("abort", abort);
112
+ finish({ rows: [...rows.values()], omittedCount: omittedCount + Math.max(0, eligibleCount - started), complete, ...listError ? { listError } : {} });
113
+ };
114
+ const abort = () => end(false, "aborted");
115
+ options.signal?.addEventListener("abort", abort, { once: true });
116
+ if (options.signal?.aborted) {
117
+ abort();
118
+ return result;
119
+ }
120
+ const deadline = setTimeout(() => end(false, listedReady ? undefined : "timeout"), deadlineMs);
121
+ timers.add(deadline);
122
+ (async () => {
123
+ let listed;
124
+ try {
125
+ listed = await options.sdk.plugins.list({ signal: pass.signal });
126
+ } catch (e) {
127
+ end(false, classifyOwnerError(e));
128
+ return;
129
+ }
130
+ if (done)
131
+ return;
132
+ listedReady = true;
133
+ const unique = new Map;
134
+ for (const p of listed.plugins.slice(0, LIMITS.listedPlugins))
135
+ if (p.id.length <= 200 && (p.status === "running" || p.status === "degraded"))
136
+ unique.set(p.id, p);
137
+ const candidates = [...unique.values()].slice(0, count);
138
+ omittedCount = unique.size - candidates.length + Math.max(0, listed.plugins.length - LIMITS.listedPlugins);
139
+ eligibleCount = candidates.length;
140
+ let index = 0;
141
+ async function worker() {
142
+ while (!done && index < candidates.length) {
143
+ const p = candidates[index++];
144
+ started++;
145
+ const control = new AbortController;
146
+ active.set(p.id, control);
147
+ let timedOut = false;
148
+ const timer = setTimeout(() => {
149
+ timedOut = true;
150
+ control.abort();
151
+ publish({ pluginId: p.id, state: "error", error: "timeout" });
152
+ }, callMs);
153
+ timers.add(timer);
154
+ try {
155
+ const negotiated = await createThreadMetricsClient(options.sdk, p.id).describe(control.signal);
156
+ if (!timedOut && !done)
157
+ publish(negotiated.kind === "ready" ? { pluginId: p.id, state: "ready", version: 1, capabilities: negotiated.capabilities } : { pluginId: p.id, state: "incompatible" });
158
+ } catch (e) {
159
+ if (!timedOut && !done)
160
+ publish({ pluginId: p.id, state: "error", error: classifyOwnerError(e) });
161
+ } finally {
162
+ clearTimeout(timer);
163
+ timers.delete(timer);
164
+ active.delete(p.id);
165
+ }
166
+ }
167
+ }
168
+ await Promise.all(Array.from({ length: Math.min(concurrency, candidates.length) }, () => worker()));
169
+ if (!done)
170
+ end(omittedCount === 0);
171
+ })().catch(() => end(false, "failed"));
172
+ return result;
173
+ }
174
+ function createThreadMetricsDiscovery(sdk) {
175
+ let running = false, outstanding = 0;
176
+ async function track(fn) {
177
+ outstanding++;
178
+ try {
179
+ return await fn();
180
+ } finally {
181
+ outstanding--;
182
+ }
183
+ }
184
+ const admitted = { plugins: {
185
+ list: (args) => track(() => sdk.plugins.list(args)),
186
+ callRpc: (args) => track(() => sdk.plugins.callRpc(args))
187
+ } };
188
+ return {
189
+ get outstandingReads() {
190
+ return outstanding;
191
+ },
192
+ async discover(options = {}) {
193
+ if (running || outstanding > 0)
194
+ return { rows: [], omittedCount: 0, complete: false, listError: "busy" };
195
+ running = true;
196
+ try {
197
+ return await discoverThreadMetricsOwners({ ...options, sdk: admitted });
198
+ } finally {
199
+ running = false;
200
+ }
201
+ }
202
+ };
203
+ }
204
+ export {
205
+ registerThreadMetricsOwner,
206
+ createThreadMetricsDiscovery,
207
+ createThreadMetricsClient
208
+ };