@phosphorco/bb-thread-metrics 0.2.0 → 0.2.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/CONTRACT.md +130 -0
- package/dist/bb.js +267 -17
- package/dist/index.js +428 -69
- package/dist/testing.js +70 -1
- package/dist/types/bb.d.ts +1064 -8
- package/dist/types/heat-settings.d.ts +413 -0
- package/dist/types/index.d.ts +19 -16
- package/dist/types/recent-session.d.ts +51 -0
- package/dist/types/recent.d.ts +5356 -363
- package/dist/types/testing.d.ts +8 -0
- package/package.json +2 -2
package/CONTRACT.md
CHANGED
|
@@ -398,3 +398,133 @@ selectors. They may conditionally poll at30s while interested/visible/connected,
|
|
|
398
398
|
coalescing existing hints without a per-thread request. Covered listing rows need
|
|
399
399
|
no readV1; explicit inspection outside the window may use the existing bounded
|
|
400
400
|
read/detail lane. The contract package itself never polls or retries.
|
|
401
|
+
|
|
402
|
+
### Optional recent token/count observations
|
|
403
|
+
|
|
404
|
+
A recent scope may carry `usage` with aggregate and per-provider token windows,
|
|
405
|
+
explicit `estimate`/`floor`/`unavailable` values, matching provider omissions,
|
|
406
|
+
and a descendant count with an exactness flag. Missing `usage` is unsupported,
|
|
407
|
+
not zero. Positive tokens do not depend on a price match or lifetime dollar
|
|
408
|
+
observations: wholly unpriced and bucket-only tokens can remain floors while
|
|
409
|
+
USD is unavailable. Unknown/legacy token inputs stay unavailable. A supplied
|
|
410
|
+
zero is an explicit observation attestation.
|
|
411
|
+
|
|
412
|
+
Provider identities must match the displayed dollar providers. The codec checks
|
|
413
|
+
componentwise conservation of known token observations and reconciles descendant
|
|
414
|
+
count with the loaded membership; it does not certify unseen global membership.
|
|
415
|
+
Omitted providers permit a larger aggregate supported subtotal, never a smaller
|
|
416
|
+
one. Presentation fitting removes dollar/token provider entries together and
|
|
417
|
+
retains aggregate supported observations with an explicit cap qualification.
|
|
418
|
+
|
|
419
|
+
Per-provider dollar contributions are the primary money presentation. Aggregate
|
|
420
|
+
cross-provider dollar windows remain internal reconciliation/ranking inputs and
|
|
421
|
+
must not be presented as a grand total. Weighted attention scores are unitless,
|
|
422
|
+
not currency; token counts and descendants are secondary to provider dollars.
|
|
423
|
+
|
|
424
|
+
An aggregate token `estimate` requires estimate-kind tokens for every displayed
|
|
425
|
+
provider and no omitted providers in that window. A floor or unavailable provider
|
|
426
|
+
cannot be upgraded by numerical equality alone. This requirement is independent
|
|
427
|
+
of USD pricing coverage: completely observed zero or positive tokens may remain
|
|
428
|
+
estimates while dollars are unpriced. Explicit member-cap, depth-cap or edge-cap
|
|
429
|
+
coverage cannot carry `descendants.exact=true`; other supplier membership
|
|
430
|
+
attestations still require evidence the codec cannot reconstruct.
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
### Unique seven-day root inputs (unpublished0.2 producer extension)
|
|
434
|
+
|
|
435
|
+
Optional `heatInputs` is a unique all-window root-family table captured in the SAME durable rank publication as `heat`. It is independent of `rows`, whose actual fold activity covers only72h and whose money may be newer. Never join current display money into captured ranks by ID, infer missing activity timestamps, or compare cross-epoch money as equal. Each input carries its financial `asOf`, config revision, explicit attested root topology, root-domain dollar/weighted ranks and all three decisions. `family.dollarRanks/scoreRanks` retain their original scoped meaning; root ranks live only under `heat`. Unknown metadata remains absent/null; title/native personal-or-project navigation is only bounded already-collected data, not identity/membership proof.
|
|
436
|
+
|
|
437
|
+
`cohortComplete` attests the supplied ROOT table, not complete source observation or exact spend. It requires no pending or omitted input and exact omission accounting. Per-window context population coverage and each family value/attribution remain independent. Full certified windows reconcile exact supported-positive counts and first-tie ordering against the complete table; partial transport preserves published ranks but cannot give exact reweighted draft percentiles. A five-day root can appear here without any72h display row. Hidden/archive activity is included; child/deleted/unknown topology never enters this table.
|
|
438
|
+
|
|
439
|
+
Display rows PLUS captured input rows share one aggregate1000 physical-entry budget, whole4MiB canonical JSON,8KiB per entry; duplicate IDs across these two DIFFERENT captures still consume two physical entries. Raw additive fields count before output stripping. Producer packing may retain only a subset; omissions/capped membership are explicit and prevent a complete input attestation. It never reconstructs from display rows or adds a per-ID population endpoint.
|
|
440
|
+
|
|
441
|
+
Optional scope `attribution` carries per-window `unknownProvider`, `attributionComplete` and `unpriced` evidence. Known provider USD is primary presentation. Cross-provider `windows` and unitless weighted `scores` are INTERNAL reconciliation/ranking inputs, not a dollar grand total. Unknown remainder is a lower bound/unavailable reconciliation amount, NEVER an invented provider at weight1. No omitted provider IDs and exact zero remainder are required for complete attribution; complete observation/price are separately required for estimates. Explicit unpriced usage retains missing-price provenance. Legacy scopes lacking this extension remain readable diagnostics; they cannot populate the new certified heat input table. No numeric evidence is invented on migration.
|
|
442
|
+
|
|
443
|
+
The supplier still attests actual membership and ordering; codecs prove internal coherence, not unseen global source truth. Bounds are post-transport parsing containment. The optional producer remains source-only until reviewed publication, exact registry pins, fresh composition gates and host byte/time measurement; the contract extension alone is no activation/adoption/freshness proof.
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
### Full publication identity and ordered consumer admission
|
|
447
|
+
|
|
448
|
+
An additive full-only `publication:{instance,sequence}` header exposes the
|
|
449
|
+
supplier's committed publication identity. `instance` is an opaque1–128-character
|
|
450
|
+
string; `sequence` is a positive safe integer. Old stored/full responses may omit
|
|
451
|
+
this header and remain readable for legacy diagnostics. New changed publications
|
|
452
|
+
from the ledger include it. A Heat capture adopter requires the header; absence
|
|
453
|
+
is unavailable, never inferred by parsing `revision`, rank time or a request
|
|
454
|
+
ordinal. Unchanged replies carry only the previously requested opaque revision,
|
|
455
|
+
never a new identity or recognized full-only fields.
|
|
456
|
+
|
|
457
|
+
Within one durable source history, changed committed publications strictly
|
|
458
|
+
increase sequence across writer-instance changes. No-op attempts and reopening
|
|
459
|
+
retain the exact held revision/pair/content. The publisher injects revision and
|
|
460
|
+
pair inside its guarded commit and excludes BOTH from the semantic digest. The
|
|
461
|
+
pair is validated against the stored instance/counter on hydration; reads return
|
|
462
|
+
the stored payload identity, not the newly registered writer. Actual encoded
|
|
463
|
+
header bytes count toward the same4MiB cap; no new allowance is created.
|
|
464
|
+
|
|
465
|
+
A retained owner resource, outside component mounts, must order validated full
|
|
466
|
+
responses by sequence before feeding Heat: lower is stale; equal accepts only
|
|
467
|
+
the same immutable pair/revision/content; greater with a new instance is an
|
|
468
|
+
ordered writer successor. Null, ordinary reconnect, facade changes and timeouts
|
|
469
|
+
do not reset this watermark or release actual transport admission. Source
|
|
470
|
+
replacement fences captured old callbacks. These are adopter obligations, not
|
|
471
|
+
facts proved by a stateless wire decoder or the presence of this header.
|
|
472
|
+
|
|
473
|
+
The persisted counter does NOT attest order across external database rollback,
|
|
474
|
+
reset or replacement. Such images may repeat or lower sequences while changed
|
|
475
|
+
writer UUIDs keep revisions distinct. A UUID alone must never authorize a reset;
|
|
476
|
+
a retained consumer holds/reports lower or equal-conflicting data until an
|
|
477
|
+
explicit authoritative selected-source replacement. This unit adds no restore
|
|
478
|
+
protocol or automatic reset. Counter exhaustion is explicit refusal, never wrap.
|
|
479
|
+
Settings Save CAS remains keyed by config version, not publication sequence;
|
|
480
|
+
settings published metadata must describe the exact held publication/config.
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
### Retained snapshot client session (unpublished0.2)
|
|
484
|
+
|
|
485
|
+
The optional `/bb` `createRecentSnapshotSession(ownerBoundClient)` helper is an
|
|
486
|
+
imperative admission and ordering boundary, owned by the existing selected-source
|
|
487
|
+
resource outside React. The surface owns discovery, source-selection authority,
|
|
488
|
+
refresh cadence, retries, observable state and rendering. The helper creates no
|
|
489
|
+
subscription, refresh loop, retry queue or shared global store; construction is
|
|
490
|
+
quiet. It holds at most one decoded immutable full snapshot and one actual read.
|
|
491
|
+
A native adopter must retain the SAME session through zero interest, remount,
|
|
492
|
+
ordinary reconnect and facade/writer changes while a read remains outstanding.
|
|
493
|
+
Independent factories do not share admission. This helper alone does not prove
|
|
494
|
+
an adopter's resource lifetime.
|
|
495
|
+
|
|
496
|
+
`read()` sends the held opaque revision conditionally; `read({full:true})` requests
|
|
497
|
+
a full response. Original full envelopes are publicly validated before ordering.
|
|
498
|
+
A full response requires the publication pair: lower sequences fail, equal
|
|
499
|
+
sequences require the same immutable pair/revision/decoded content, and greater
|
|
500
|
+
sequences must use a new opaque revision. An ordered new writer advances the
|
|
501
|
+
private owner generation once; an ordinary same-source facade update does not.
|
|
502
|
+
Valid unchanged content returns the exact held observation object, allowing the
|
|
503
|
+
caller to confirm recovery without allocating or notifying for changed data.
|
|
504
|
+
Null, invalid, missing-identity, stale or failed responses preserve last content
|
|
505
|
+
and highwater. Validation/content comparison still costs bounded payload work;
|
|
506
|
+
this is not a claim of constant-time end-to-end decoding.
|
|
507
|
+
|
|
508
|
+
The default local observation deadline is the existing2000ms contract call
|
|
509
|
+
limit (configurable1–2000ms). Timeout, caller abort, inactive state or explicit
|
|
510
|
+
replacement rejects local observation and aborts its signal, but the actual slot
|
|
511
|
+
remains held until the underlying source Promise settles, even if abort is
|
|
512
|
+
ignored. Busy/inactive/already-aborted reads return typed local
|
|
513
|
+
`RecentSnapshotNotStarted` without invoking the source. A source-thrown instance
|
|
514
|
+
of that type is wrapped and cannot claim this session's pre-invocation proof.
|
|
515
|
+
There is no queued replay. A permanently held transport keeps this lane
|
|
516
|
+
unavailable; caller deadline never proves remote cancellation.
|
|
517
|
+
|
|
518
|
+
`updateSource` is only a same-plugin facade update and retains ordering.
|
|
519
|
+
`replaceSource` requires authoritative selected-source replacement, fences old
|
|
520
|
+
callbacks and clears held lineage while retaining any actual old transport slot.
|
|
521
|
+
Do not invoke it for a new UUID, timeout, remount, lower sequence or an ordinary
|
|
522
|
+
reconnect. External restored/reset images require authority supplied by the
|
|
523
|
+
native resource; this API does not establish that authority. `setActive` changes
|
|
524
|
+
interest without clearing the lineage. Only admitted observations may feed Heat
|
|
525
|
+
owner-generation transitions. Publication identity is independent of matching
|
|
526
|
+
Save config bytes/version, which the retained Heat adapter must still check.
|
|
527
|
+
|
|
528
|
+
These are client source conformance guarantees, not released pins, native
|
|
529
|
+
resource adoption, host latency/freshness or live bytes/time acceptance. No
|
|
530
|
+
existing consumer is switched merely by exporting this helper.
|
package/dist/bb.js
CHANGED
|
@@ -1,6 +1,247 @@
|
|
|
1
1
|
// src/bb.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { HEAT_SETTINGS_METHODS, HEAT_SETTINGS_LIMITS, bounded, validateHeatSaveInput, heatSettingsReadInputSchema, heatSettingsSaveInputSchema, heatSettingsReadResultSchema, heatSettingsSaveResultSchema } from "./index.js";
|
|
4
|
+
import { RECENT_LIMITS, recentSnapshotInputSchema, validateRecentSnapshot as validateRecentSnapshot2, LIMITS as LIMITS2, METHODS, describeInputSchema, describeEnvelopeSchema, capabilitiesSchema, readInputSchema, detailInputSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult, classifyOwnerError } from "./index.js";
|
|
5
|
+
|
|
6
|
+
// src/recent-session.ts
|
|
7
|
+
import { LIMITS, validateRecentSnapshot } from "./index.js";
|
|
8
|
+
|
|
9
|
+
class RecentSnapshotSessionError extends Error {
|
|
10
|
+
reason;
|
|
11
|
+
constructor(reason) {
|
|
12
|
+
super(`Recent snapshot session: ${reason}`);
|
|
13
|
+
this.reason = reason;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class RecentSnapshotNotStarted extends RecentSnapshotSessionError {
|
|
18
|
+
notStarted = true;
|
|
19
|
+
constructor(reason) {
|
|
20
|
+
super(reason);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
var clock = { later(run, ms) {
|
|
24
|
+
const timer = setTimeout(run, ms);
|
|
25
|
+
return () => clearTimeout(timer);
|
|
26
|
+
} };
|
|
27
|
+
function next(n) {
|
|
28
|
+
if (n === Number.MAX_SAFE_INTEGER)
|
|
29
|
+
throw new Error("Recent session generation exhausted");
|
|
30
|
+
return n + 1;
|
|
31
|
+
}
|
|
32
|
+
function sourceCopy(source) {
|
|
33
|
+
if (!source.pluginId || source.pluginId.length > 200 || typeof source.recentSnapshot !== "function")
|
|
34
|
+
throw new Error("Invalid recent snapshot source");
|
|
35
|
+
return { pluginId: source.pluginId, recentSnapshot: source.recentSnapshot.bind(source) };
|
|
36
|
+
}
|
|
37
|
+
function freeze(value) {
|
|
38
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
39
|
+
for (const child of Object.values(value))
|
|
40
|
+
freeze(child);
|
|
41
|
+
Object.freeze(value);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function same(a, b) {
|
|
46
|
+
if (a === b)
|
|
47
|
+
return true;
|
|
48
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object")
|
|
49
|
+
return false;
|
|
50
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
51
|
+
return false;
|
|
52
|
+
const ak = Object.keys(a), bk = Object.keys(b);
|
|
53
|
+
if (ak.length !== bk.length)
|
|
54
|
+
return false;
|
|
55
|
+
return ak.every((key) => Object.hasOwn(b, key) && same(a[key], b[key]));
|
|
56
|
+
}
|
|
57
|
+
function createRecentSnapshotSession(initial, options = {}) {
|
|
58
|
+
const timeoutMs = options.timeoutMs ?? LIMITS.callMs;
|
|
59
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > LIMITS.callMs)
|
|
60
|
+
throw new Error("Invalid recent session deadline");
|
|
61
|
+
const time = options.clock ?? clock;
|
|
62
|
+
let source = sourceCopy(initial), active = true, epoch = Symbol("view"), sourceGeneration = 1, ownerGeneration = 1;
|
|
63
|
+
let held;
|
|
64
|
+
let actual;
|
|
65
|
+
const admit = (input, raw, current) => {
|
|
66
|
+
if (!current())
|
|
67
|
+
throw new RecentSnapshotSessionError("stale-call");
|
|
68
|
+
const value = validateRecentSnapshot(input, raw);
|
|
69
|
+
if (!current())
|
|
70
|
+
throw new RecentSnapshotSessionError("stale-call");
|
|
71
|
+
if (value.unchanged) {
|
|
72
|
+
if (!held || held.snapshot.revision !== value.revision)
|
|
73
|
+
throw new RecentSnapshotSessionError("publication-conflict");
|
|
74
|
+
return held;
|
|
75
|
+
}
|
|
76
|
+
const publication = value.publication;
|
|
77
|
+
if (!publication)
|
|
78
|
+
throw new RecentSnapshotSessionError("missing-publication");
|
|
79
|
+
if (held) {
|
|
80
|
+
if (publication.sequence < held.publication.sequence)
|
|
81
|
+
throw new RecentSnapshotSessionError("stale-publication");
|
|
82
|
+
if (publication.sequence === held.publication.sequence) {
|
|
83
|
+
if (publication.instance !== held.publication.instance || value.revision !== held.snapshot.revision || !same(value, held.snapshot))
|
|
84
|
+
throw new RecentSnapshotSessionError("publication-conflict");
|
|
85
|
+
return held;
|
|
86
|
+
}
|
|
87
|
+
if (value.revision === held.snapshot.revision)
|
|
88
|
+
throw new RecentSnapshotSessionError("publication-conflict");
|
|
89
|
+
if (publication.instance !== held.publication.instance)
|
|
90
|
+
ownerGeneration = next(ownerGeneration);
|
|
91
|
+
}
|
|
92
|
+
held = freeze({ snapshot: value, publication, ownerGeneration, sourceGeneration });
|
|
93
|
+
return held;
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
get current() {
|
|
97
|
+
return held;
|
|
98
|
+
},
|
|
99
|
+
get outstandingReads() {
|
|
100
|
+
return actual ? 1 : 0;
|
|
101
|
+
},
|
|
102
|
+
get ownerGeneration() {
|
|
103
|
+
return ownerGeneration;
|
|
104
|
+
},
|
|
105
|
+
get sourceGeneration() {
|
|
106
|
+
return sourceGeneration;
|
|
107
|
+
},
|
|
108
|
+
get pluginId() {
|
|
109
|
+
return source.pluginId;
|
|
110
|
+
},
|
|
111
|
+
updateSource(value) {
|
|
112
|
+
const updated = sourceCopy(value);
|
|
113
|
+
if (updated.pluginId !== source.pluginId)
|
|
114
|
+
throw new Error("Selected source change requires explicit replacement");
|
|
115
|
+
source = updated;
|
|
116
|
+
},
|
|
117
|
+
replaceSource(value) {
|
|
118
|
+
const updated = sourceCopy(value), sg = next(sourceGeneration), og = next(ownerGeneration);
|
|
119
|
+
source = updated;
|
|
120
|
+
sourceGeneration = sg;
|
|
121
|
+
ownerGeneration = og;
|
|
122
|
+
held = undefined;
|
|
123
|
+
epoch = Symbol("source");
|
|
124
|
+
actual?.cancel?.("source-replaced");
|
|
125
|
+
},
|
|
126
|
+
setActive(value) {
|
|
127
|
+
if (value === active)
|
|
128
|
+
return;
|
|
129
|
+
active = value;
|
|
130
|
+
epoch = Symbol("view");
|
|
131
|
+
if (!active)
|
|
132
|
+
actual?.cancel?.("inactive");
|
|
133
|
+
},
|
|
134
|
+
read(options2 = {}) {
|
|
135
|
+
const { signal, full } = options2;
|
|
136
|
+
options2 = {};
|
|
137
|
+
if (!active)
|
|
138
|
+
return Promise.reject(new RecentSnapshotNotStarted("inactive"));
|
|
139
|
+
if (signal?.aborted)
|
|
140
|
+
return Promise.reject(new RecentSnapshotNotStarted("aborted-before-start"));
|
|
141
|
+
if (actual)
|
|
142
|
+
return Promise.reject(new RecentSnapshotNotStarted("busy"));
|
|
143
|
+
const input = !full && held ? { ifRevision: held.snapshot.revision } : {};
|
|
144
|
+
let selected = source;
|
|
145
|
+
const binding = sourceGeneration, view = epoch, control = new AbortController, slot = { cancel: undefined };
|
|
146
|
+
actual = slot;
|
|
147
|
+
let finished = false;
|
|
148
|
+
const current = () => !finished && active && epoch === view && sourceGeneration === binding;
|
|
149
|
+
let resolve, reject;
|
|
150
|
+
const result = new Promise((yes, no) => {
|
|
151
|
+
resolve = yes;
|
|
152
|
+
reject = no;
|
|
153
|
+
});
|
|
154
|
+
let cancelTimer;
|
|
155
|
+
const finish = (error, observation) => {
|
|
156
|
+
if (finished)
|
|
157
|
+
return;
|
|
158
|
+
finished = true;
|
|
159
|
+
cancelTimer?.();
|
|
160
|
+
cancelTimer = undefined;
|
|
161
|
+
signal?.removeEventListener("abort", abort);
|
|
162
|
+
slot.cancel = undefined;
|
|
163
|
+
const yes = resolve, no = reject;
|
|
164
|
+
resolve = undefined;
|
|
165
|
+
reject = undefined;
|
|
166
|
+
if (observation === undefined)
|
|
167
|
+
no?.(error);
|
|
168
|
+
else
|
|
169
|
+
yes?.(observation);
|
|
170
|
+
};
|
|
171
|
+
const cancel = (reason) => {
|
|
172
|
+
finish(new RecentSnapshotSessionError(reason));
|
|
173
|
+
control.abort();
|
|
174
|
+
};
|
|
175
|
+
const abort = () => cancel("aborted");
|
|
176
|
+
slot.cancel = cancel;
|
|
177
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
178
|
+
try {
|
|
179
|
+
cancelTimer = time.later(() => cancel("timeout"), timeoutMs);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
finish(error);
|
|
182
|
+
actual = undefined;
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
if (finished) {
|
|
186
|
+
actual = undefined;
|
|
187
|
+
cancelTimer?.();
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
let pending;
|
|
191
|
+
try {
|
|
192
|
+
pending = Promise.resolve(selected.recentSnapshot(Object.freeze(input), control.signal));
|
|
193
|
+
} catch (error) {
|
|
194
|
+
pending = Promise.reject(error);
|
|
195
|
+
} finally {
|
|
196
|
+
selected = undefined;
|
|
197
|
+
}
|
|
198
|
+
pending.then((value) => {
|
|
199
|
+
try {
|
|
200
|
+
if (!finished)
|
|
201
|
+
finish(undefined, admit(input, value, current));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
finish(error);
|
|
204
|
+
} finally {
|
|
205
|
+
if (actual === slot)
|
|
206
|
+
actual = undefined;
|
|
207
|
+
}
|
|
208
|
+
}, (error) => {
|
|
209
|
+
try {
|
|
210
|
+
finish(error instanceof RecentSnapshotNotStarted ? new Error("Recent snapshot source rejected after invocation", { cause: error }) : error);
|
|
211
|
+
} finally {
|
|
212
|
+
if (actual === slot)
|
|
213
|
+
actual = undefined;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/bb.ts
|
|
222
|
+
function registerHeatSettingsOwner(bb, port) {
|
|
223
|
+
bb.rpc.register({
|
|
224
|
+
threadMetricsHeatSettingsReadV1: { input: sdkSchema(heatSettingsReadInputSchema, HEAT_SETTINGS_LIMITS.inputBytes), output: sdkSchema(z.unknown()) },
|
|
225
|
+
threadMetricsHeatSettingsSaveV1: { input: sdkSchema(heatSettingsSaveInputSchema, HEAT_SETTINGS_LIMITS.inputBytes), output: sdkSchema(z.unknown()) }
|
|
226
|
+
}, {
|
|
227
|
+
threadMetricsHeatSettingsReadV1: async (raw) => bounded(heatSettingsReadResultSchema, await port.read(bounded(heatSettingsReadInputSchema, raw, HEAT_SETTINGS_LIMITS.inputBytes)), HEAT_SETTINGS_LIMITS.outputBytes),
|
|
228
|
+
threadMetricsHeatSettingsSaveV1: async (raw) => bounded(heatSettingsSaveResultSchema, await port.save(validateHeatSaveInput(raw)), HEAT_SETTINGS_LIMITS.outputBytes)
|
|
229
|
+
}, { experimental_discoverable: true });
|
|
230
|
+
}
|
|
231
|
+
function createHeatSettingsClient(sdk, pluginId) {
|
|
232
|
+
if (!pluginId || pluginId.length > 200)
|
|
233
|
+
throw new Error("Invalid target plugin id");
|
|
234
|
+
const call = async (method, input, signal) => sdk.plugins.callRpc({ pluginId, method, input, outputSchema: z.unknown(), ...signal ? { signal } : {} });
|
|
235
|
+
return {
|
|
236
|
+
read: async (input, signal) => bounded(heatSettingsReadResultSchema, await call(HEAT_SETTINGS_METHODS.read, bounded(heatSettingsReadInputSchema, input, HEAT_SETTINGS_LIMITS.inputBytes), signal), HEAT_SETTINGS_LIMITS.outputBytes),
|
|
237
|
+
save: async (input, signal) => {
|
|
238
|
+
const result = bounded(heatSettingsSaveResultSchema, await call(HEAT_SETTINGS_METHODS.save, validateHeatSaveInput(input), signal), HEAT_SETTINGS_LIMITS.outputBytes);
|
|
239
|
+
if (result.operationKey !== input.operationKey)
|
|
240
|
+
throw new Error("Save operation mismatch");
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
4
245
|
function sdkSchema(schema, maxBytes) {
|
|
5
246
|
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
247
|
try {
|
|
@@ -14,34 +255,38 @@ function registerThreadMetricsOwner(bb, port) {
|
|
|
14
255
|
const capabilities = capabilitiesSchema.parse(port.capabilities);
|
|
15
256
|
if (Boolean(capabilities.recentSnapshotV1) !== Boolean(port.recentSnapshot))
|
|
16
257
|
throw new Error("Recent snapshot capability/handler mismatch");
|
|
258
|
+
if (Boolean(capabilities.heatSettingsV1) !== Boolean(port.heatSettings))
|
|
259
|
+
throw new Error("Heat settings capability/handler mismatch");
|
|
17
260
|
if (capabilities.detail !== Boolean(port.detail))
|
|
18
261
|
throw new Error("Detail capability/handler mismatch");
|
|
19
262
|
const describe = () => ({ protocol: "bb-thread-metrics", versions: [1], capabilities });
|
|
20
263
|
const contract = {
|
|
21
|
-
threadMetricsDescribe: { input: sdkSchema(describeInputSchema,
|
|
22
|
-
threadMetricsReadV1: { input: sdkSchema(readInputSchema,
|
|
264
|
+
threadMetricsDescribe: { input: sdkSchema(describeInputSchema, LIMITS2.inputBytes), output: sdkSchema(z.unknown()) },
|
|
265
|
+
threadMetricsReadV1: { input: sdkSchema(readInputSchema, LIMITS2.inputBytes), output: sdkSchema(z.unknown()) }
|
|
23
266
|
};
|
|
24
267
|
const handlers = {
|
|
25
268
|
threadMetricsDescribe: (raw) => {
|
|
26
|
-
parseBounded(describeInputSchema, raw,
|
|
269
|
+
parseBounded(describeInputSchema, raw, LIMITS2.inputBytes);
|
|
27
270
|
return describe();
|
|
28
271
|
},
|
|
29
272
|
threadMetricsReadV1: async (raw) => {
|
|
30
|
-
const input = parseBounded(readInputSchema, raw,
|
|
273
|
+
const input = parseBounded(readInputSchema, raw, LIMITS2.inputBytes);
|
|
31
274
|
if (input.series && !capabilities.series)
|
|
32
275
|
throw new Error("Series not supported");
|
|
33
276
|
return validateReadResult(input, await port.read(input));
|
|
34
277
|
}
|
|
35
278
|
};
|
|
36
279
|
bb.rpc.register(contract, handlers, { experimental_discoverable: true });
|
|
280
|
+
if (port.heatSettings)
|
|
281
|
+
registerHeatSettingsOwner(bb, port.heatSettings);
|
|
37
282
|
if (port.recentSnapshot)
|
|
38
283
|
bb.rpc.register({ threadMetricsRecentSnapshotV1: { input: sdkSchema(recentSnapshotInputSchema, RECENT_LIMITS.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsRecentSnapshotV1: async (raw) => {
|
|
39
284
|
const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
|
|
40
|
-
return
|
|
285
|
+
return validateRecentSnapshot2(input, await port.recentSnapshot(input));
|
|
41
286
|
} }, { experimental_discoverable: true });
|
|
42
287
|
if (port.detail)
|
|
43
|
-
bb.rpc.register({ threadMetricsDetailV1: { input: sdkSchema(detailInputSchema,
|
|
44
|
-
const input = parseBounded(detailInputSchema, raw,
|
|
288
|
+
bb.rpc.register({ threadMetricsDetailV1: { input: sdkSchema(detailInputSchema, LIMITS2.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsDetailV1: async (raw) => {
|
|
289
|
+
const input = parseBounded(detailInputSchema, raw, LIMITS2.inputBytes);
|
|
45
290
|
return validateDetailResult(input, await port.detail(input));
|
|
46
291
|
} }, { experimental_discoverable: true });
|
|
47
292
|
}
|
|
@@ -55,30 +300,30 @@ function createThreadMetricsClient(sdk, pluginId) {
|
|
|
55
300
|
pluginId,
|
|
56
301
|
async describe(signal) {
|
|
57
302
|
const raw = await call(METHODS.describe, {}, signal);
|
|
58
|
-
return negotiateVersion(parseBounded(describeEnvelopeSchema, raw,
|
|
303
|
+
return negotiateVersion(parseBounded(describeEnvelopeSchema, raw, LIMITS2.describeBytes));
|
|
59
304
|
},
|
|
60
305
|
async recentSnapshot(raw = {}, signal) {
|
|
61
306
|
const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
|
|
62
|
-
return
|
|
307
|
+
return validateRecentSnapshot2(input, await call(METHODS.recentSnapshot, input, signal));
|
|
63
308
|
},
|
|
64
309
|
async read(raw, signal) {
|
|
65
|
-
const input = parseBounded(readInputSchema, raw,
|
|
310
|
+
const input = parseBounded(readInputSchema, raw, LIMITS2.inputBytes);
|
|
66
311
|
return validateReadResult(input, await call(METHODS.read, input, signal));
|
|
67
312
|
},
|
|
68
313
|
async detail(raw, signal) {
|
|
69
|
-
const input = parseBounded(detailInputSchema, raw,
|
|
314
|
+
const input = parseBounded(detailInputSchema, raw, LIMITS2.inputBytes);
|
|
70
315
|
return validateDetailResult(input, await call(METHODS.detail, input, signal));
|
|
71
316
|
}
|
|
72
317
|
};
|
|
73
318
|
}
|
|
74
319
|
async function discoverThreadMetricsOwners(options) {
|
|
75
|
-
const
|
|
320
|
+
const bounded2 = (v, max) => {
|
|
76
321
|
const n = v ?? max;
|
|
77
322
|
if (!Number.isInteger(n) || n < 1 || n > max)
|
|
78
323
|
throw new Error("Invalid discovery budget");
|
|
79
324
|
return n;
|
|
80
325
|
};
|
|
81
|
-
const count =
|
|
326
|
+
const count = bounded2(options.maxCandidates, LIMITS2.candidates), concurrency = bounded2(options.concurrency, LIMITS2.concurrency), callMs = bounded2(options.callMs, LIMITS2.callMs), deadlineMs = bounded2(options.deadlineMs, LIMITS2.discoveryMs);
|
|
82
327
|
const pass = new AbortController, rows = new Map, active = new Map;
|
|
83
328
|
let done = false, ending = false, listedReady = false, omittedCount = 0, eligibleCount = 0, started = 0;
|
|
84
329
|
const timers = new Set;
|
|
@@ -131,11 +376,11 @@ async function discoverThreadMetricsOwners(options) {
|
|
|
131
376
|
return;
|
|
132
377
|
listedReady = true;
|
|
133
378
|
const unique = new Map;
|
|
134
|
-
for (const p of listed.plugins.slice(0,
|
|
379
|
+
for (const p of listed.plugins.slice(0, LIMITS2.listedPlugins))
|
|
135
380
|
if (p.id.length <= 200 && (p.status === "running" || p.status === "degraded"))
|
|
136
381
|
unique.set(p.id, p);
|
|
137
382
|
const candidates = [...unique.values()].slice(0, count);
|
|
138
|
-
omittedCount = unique.size - candidates.length + Math.max(0, listed.plugins.length -
|
|
383
|
+
omittedCount = unique.size - candidates.length + Math.max(0, listed.plugins.length - LIMITS2.listedPlugins);
|
|
139
384
|
eligibleCount = candidates.length;
|
|
140
385
|
let index = 0;
|
|
141
386
|
async function worker() {
|
|
@@ -203,6 +448,11 @@ function createThreadMetricsDiscovery(sdk) {
|
|
|
203
448
|
}
|
|
204
449
|
export {
|
|
205
450
|
registerThreadMetricsOwner,
|
|
451
|
+
registerHeatSettingsOwner,
|
|
206
452
|
createThreadMetricsDiscovery,
|
|
207
|
-
createThreadMetricsClient
|
|
453
|
+
createThreadMetricsClient,
|
|
454
|
+
createRecentSnapshotSession,
|
|
455
|
+
createHeatSettingsClient,
|
|
456
|
+
RecentSnapshotSessionError,
|
|
457
|
+
RecentSnapshotNotStarted
|
|
208
458
|
};
|