@fedify/cli 2.3.0-dev.994 → 2.3.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.
Files changed (83) hide show
  1. package/dist/bench/action.js +469 -0
  2. package/dist/bench/actor/documents.js +39 -0
  3. package/dist/bench/actor/fleet.js +39 -0
  4. package/dist/bench/actor/keys.js +35 -0
  5. package/dist/bench/command.js +72 -0
  6. package/dist/bench/compare/schema.js +16 -0
  7. package/dist/bench/compare.js +667 -0
  8. package/dist/bench/discovery/discover.js +67 -0
  9. package/dist/bench/discovery/probe.js +50 -0
  10. package/dist/bench/load/arrival.js +27 -0
  11. package/dist/bench/load/clock.js +33 -0
  12. package/dist/bench/load/generator.js +145 -0
  13. package/dist/bench/metrics/aggregate.js +64 -0
  14. package/dist/bench/metrics/histogram.js +141 -0
  15. package/dist/bench/metrics/stats-client.js +216 -0
  16. package/dist/bench/mod.js +11 -0
  17. package/dist/bench/render/format.js +46 -0
  18. package/dist/bench/render/index.js +20 -0
  19. package/dist/bench/render/json.js +12 -0
  20. package/dist/bench/render/markdown.js +63 -0
  21. package/dist/bench/render/text.js +75 -0
  22. package/dist/bench/result/build.js +252 -0
  23. package/dist/bench/result/expect/assert.js +74 -0
  24. package/dist/bench/result/expect/evaluate.js +128 -0
  25. package/dist/bench/result/expect/metrics.js +34 -0
  26. package/dist/bench/result/schema.js +365 -0
  27. package/dist/bench/safety/gate.js +62 -0
  28. package/dist/bench/safety/tiers.js +97 -0
  29. package/dist/bench/scenario/coerce.js +24 -0
  30. package/dist/bench/scenario/errors.js +36 -0
  31. package/dist/bench/scenario/load.js +69 -0
  32. package/dist/bench/scenario/normalize.js +125 -0
  33. package/dist/bench/scenario/schema.js +399 -0
  34. package/dist/bench/scenario/units.js +56 -0
  35. package/dist/bench/scenario/validate.js +29 -0
  36. package/dist/bench/scenarios/actor.js +38 -0
  37. package/dist/bench/scenarios/failure.js +363 -0
  38. package/dist/bench/scenarios/fanout.js +261 -0
  39. package/dist/bench/scenarios/inbox.js +147 -0
  40. package/dist/bench/scenarios/mixed.js +244 -0
  41. package/dist/bench/scenarios/object-discovery.js +211 -0
  42. package/dist/bench/scenarios/object.js +54 -0
  43. package/dist/bench/scenarios/read.js +108 -0
  44. package/dist/bench/scenarios/registry.js +39 -0
  45. package/dist/bench/scenarios/runner.js +96 -0
  46. package/dist/bench/scenarios/webfinger.js +44 -0
  47. package/dist/bench/server/synthetic.js +118 -0
  48. package/dist/bench/signing/activity-id.js +18 -0
  49. package/dist/bench/signing/pipeline.js +134 -0
  50. package/dist/bench/signing/signer.js +39 -0
  51. package/dist/bench/template/generate.js +90 -0
  52. package/dist/bench/template/helpers.js +19 -0
  53. package/dist/bench/template/template.js +132 -0
  54. package/dist/cache.js +2 -2
  55. package/dist/commands.js +110 -0
  56. package/dist/config.js +15 -3
  57. package/dist/deno.js +1 -1
  58. package/dist/docloader.js +1 -1
  59. package/dist/generate-vocab/action.js +3 -3
  60. package/dist/generate-vocab/command.js +6 -4
  61. package/dist/imagerenderer.js +3 -3
  62. package/dist/inbox/command.js +6 -4
  63. package/dist/inbox/view.js +1 -1
  64. package/dist/inbox.js +4 -4
  65. package/dist/log.js +2 -2
  66. package/dist/lookup/command.js +121 -0
  67. package/dist/lookup.js +27 -138
  68. package/dist/mod.js +2 -20
  69. package/dist/nodeinfo.js +53 -12
  70. package/dist/options.js +1 -1
  71. package/dist/relay/command.js +6 -4
  72. package/dist/relay.js +3 -3
  73. package/dist/runner.js +70 -45
  74. package/dist/tunnel.js +8 -6
  75. package/dist/utils.js +9 -4
  76. package/dist/webfinger/action.js +1 -1
  77. package/dist/webfinger/command.js +6 -4
  78. package/dist/webfinger/error.js +2 -0
  79. package/dist/webfinger/lib.js +1 -1
  80. package/package.json +28 -23
  81. package/dist/generate-vocab/mod.js +0 -4
  82. package/dist/init/mod.js +0 -3
  83. package/dist/webfinger/mod.js +0 -4
@@ -0,0 +1,252 @@
1
+ import "@js-temporal/polyfill";
2
+ import { version } from "../../deno.js";
3
+ import { evaluateExpect } from "./expect/evaluate.js";
4
+ import { REPORT_SCHEMA_ID } from "./schema.js";
5
+ import process from "node:process";
6
+ import { cpus } from "node:os";
7
+ import { createHash } from "node:crypto";
8
+ //#region src/bench/result/build.ts
9
+ /**
10
+ * Assembly of the canonical benchmark report from measured scenario data.
11
+ *
12
+ * The runners produce per-scenario measurements; this module turns each into a
13
+ * {@link ScenarioResult} (evaluating its `expect` block) and assembles the
14
+ * top-level {@link BenchReport} with reproducibility metadata.
15
+ * @since 2.3.0
16
+ * @module
17
+ */
18
+ /**
19
+ * Builds a scenario result from its resolved definition and measurement,
20
+ * evaluating the `expect` block in the process.
21
+ * @param scenario The resolved scenario.
22
+ * @param measurement The measured client and server metrics.
23
+ * @returns The assembled scenario result.
24
+ */
25
+ function buildScenarioResult(scenario, measurement) {
26
+ const measurements = Array.isArray(measurement) ? measurement : [measurement];
27
+ if (measurements.length < 1) throw new RangeError("At least one scenario measurement is required.");
28
+ const aggregate = measurements.length === 1 ? measurements[0] : aggregateMeasurements(measurements);
29
+ const { results, passed } = evaluateExpect(scenario.expect, aggregate);
30
+ return {
31
+ name: scenario.name,
32
+ type: scenario.type,
33
+ load: loadSummary(scenario),
34
+ requests: aggregate.requests,
35
+ throughputPerSec: aggregate.throughputPerSec,
36
+ ...aggregate.deliveryThroughputPerSec == null ? {} : { deliveryThroughputPerSec: aggregate.deliveryThroughputPerSec },
37
+ client: aggregate.client,
38
+ server: aggregate.server,
39
+ errors: aggregate.errors,
40
+ expectations: results,
41
+ passed: passed && measurements.every((m) => m.requests.total > 0),
42
+ runCount: measurements.length,
43
+ ...measurements.length > 1 ? { runs: measurements.map((m, index) => runResult(index + 1, m)) } : {},
44
+ ...aggregate.histogram ? { histogram: aggregate.histogram } : {}
45
+ };
46
+ }
47
+ /**
48
+ * Assembles the top-level report. The gate passes only when every scenario
49
+ * passes.
50
+ * @param input The report inputs.
51
+ * @returns The complete report.
52
+ */
53
+ function buildReport(input) {
54
+ return {
55
+ $schema: REPORT_SCHEMA_ID,
56
+ schemaVersion: 3,
57
+ tool: {
58
+ name: "@fedify/cli",
59
+ version
60
+ },
61
+ environment: input.environment,
62
+ target: input.target,
63
+ startedAt: input.startedAt,
64
+ finishedAt: input.finishedAt,
65
+ suite: input.suite,
66
+ passed: input.scenarios.every((s) => s.passed),
67
+ scenarios: input.scenarios
68
+ };
69
+ }
70
+ function aggregateMeasurements(measurements) {
71
+ const errors = sumErrorBuckets(measurements.flatMap((m) => m.errors));
72
+ const total = sum(measurements.map((m) => m.requests.total));
73
+ const ok = sum(measurements.map((m) => m.requests.ok));
74
+ const failed = sum(measurements.map((m) => m.requests.failed));
75
+ const delivery = medianPresent(measurements.map((m) => m.deliveryThroughputPerSec));
76
+ return {
77
+ requests: {
78
+ total,
79
+ ok,
80
+ failed,
81
+ successRate: Math.min(...measurements.map((m) => m.requests.successRate))
82
+ },
83
+ throughputPerSec: median(measurements.map((m) => m.throughputPerSec)),
84
+ ...delivery == null ? {} : { deliveryThroughputPerSec: delivery },
85
+ client: { latencyMs: {
86
+ p50: median(measurements.map((m) => m.client.latencyMs.p50)),
87
+ p95: median(measurements.map((m) => m.client.latencyMs.p95)),
88
+ p99: median(measurements.map((m) => m.client.latencyMs.p99)),
89
+ mean: median(measurements.map((m) => m.client.latencyMs.mean)),
90
+ max: median(measurements.map((m) => m.client.latencyMs.max))
91
+ } },
92
+ server: aggregateServer(measurements.map((m) => m.server)),
93
+ errors
94
+ };
95
+ }
96
+ function runResult(run, measurement) {
97
+ return {
98
+ run,
99
+ requests: measurement.requests,
100
+ throughputPerSec: measurement.throughputPerSec,
101
+ ...measurement.deliveryThroughputPerSec == null ? {} : { deliveryThroughputPerSec: measurement.deliveryThroughputPerSec },
102
+ client: measurement.client,
103
+ server: measurement.server,
104
+ errors: measurement.errors,
105
+ ...measurement.histogram ? { histogram: measurement.histogram } : {}
106
+ };
107
+ }
108
+ function aggregateServer(servers) {
109
+ const present = servers.filter((s) => s != null);
110
+ if (present.length !== servers.length) return null;
111
+ const signature = aggregateSignatureVerification(present);
112
+ const queue = aggregateQueue(present);
113
+ return {
114
+ ...signature == null ? {} : { signatureVerificationMs: signature },
115
+ ...queue == null ? {} : { queue }
116
+ };
117
+ }
118
+ function aggregateSignatureVerification(servers) {
119
+ const values = servers.map((s) => s.signatureVerificationMs).filter((s) => s != null);
120
+ if (values.length !== servers.length) return null;
121
+ const standards = /* @__PURE__ */ new Set();
122
+ for (const value of values) for (const key of Object.keys(value.byStandard ?? {})) standards.add(key);
123
+ const byStandard = {};
124
+ for (const standard of standards) byStandard[standard] = aggregatePartial(values.map((v) => v.byStandard?.[standard]), "present");
125
+ return {
126
+ overall: aggregatePartial(values.map((v) => v.overall)),
127
+ ...Object.keys(byStandard).length < 1 ? {} : { byStandard }
128
+ };
129
+ }
130
+ function aggregateQueue(servers) {
131
+ const values = servers.map((s) => s.queue).filter((q) => q != null);
132
+ if (values.length !== servers.length) return null;
133
+ const drainMs = aggregatePartial(values.map((v) => v.drainMs));
134
+ const depths = values.map((v) => v.depthMax);
135
+ return {
136
+ ...hasPartial(drainMs) ? { drainMs } : {},
137
+ ...depths.every(isNumber) ? { depthMax: Math.max(...depths) } : {}
138
+ };
139
+ }
140
+ function aggregatePartial(values, mode = "complete") {
141
+ return {
142
+ ...partialField(values, "p50", mode),
143
+ ...partialField(values, "p95", mode),
144
+ ...partialField(values, "p99", mode)
145
+ };
146
+ }
147
+ function partialField(values, key, mode) {
148
+ const fieldValues = values.map((v) => v?.[key]);
149
+ if (mode === "present") {
150
+ const present = fieldValues.filter(isNumber);
151
+ return present.length < 1 ? {} : { [key]: median(present) };
152
+ }
153
+ return fieldValues.every(isNumber) ? { [key]: median(fieldValues) } : {};
154
+ }
155
+ function hasPartial(value) {
156
+ return value.p50 != null || value.p95 != null || value.p99 != null;
157
+ }
158
+ function sumErrorBuckets(errors) {
159
+ const buckets = /* @__PURE__ */ new Map();
160
+ for (const error of errors) {
161
+ const key = `${error.kind}|${error.status ?? ""}|${error.reason}`;
162
+ const previous = buckets.get(key);
163
+ buckets.set(key, {
164
+ ...error,
165
+ count: (previous?.count ?? 0) + error.count
166
+ });
167
+ }
168
+ return [...buckets.values()].sort((a, b) => b.count - a.count);
169
+ }
170
+ function medianPresent(values) {
171
+ const present = values.filter(isNumber);
172
+ return present.length < 1 ? null : median(present);
173
+ }
174
+ function median(values) {
175
+ if (values.length < 1) return 0;
176
+ const sorted = [...values].sort((a, b) => a - b);
177
+ const middle = Math.floor(sorted.length / 2);
178
+ if (sorted.length % 2 === 1) return sorted[middle];
179
+ return (sorted[middle - 1] + sorted[middle]) / 2;
180
+ }
181
+ function sum(values) {
182
+ return values.reduce((a, b) => a + b, 0);
183
+ }
184
+ function isNumber(value) {
185
+ return typeof value === "number" && Number.isFinite(value);
186
+ }
187
+ /** Detects the current runtime environment for reproducibility metadata. */
188
+ function detectEnvironment() {
189
+ const g = globalThis;
190
+ let runtime = "node";
191
+ let runtimeVersion = process.versions?.node ?? "unknown";
192
+ if (g.Deno?.version?.deno != null) {
193
+ runtime = "deno";
194
+ runtimeVersion = g.Deno.version.deno;
195
+ } else if (g.Bun?.version != null) {
196
+ runtime = "bun";
197
+ runtimeVersion = g.Bun.version;
198
+ }
199
+ let cpuCount = 0;
200
+ try {
201
+ cpuCount = cpus().length;
202
+ } catch {
203
+ cpuCount = 0;
204
+ }
205
+ return {
206
+ runtime,
207
+ runtimeVersion,
208
+ os: process.platform,
209
+ cpuCount
210
+ };
211
+ }
212
+ /**
213
+ * Computes a stable `sha256:` hash of a resolved configuration, so CI only
214
+ * compares runs from the same configuration.
215
+ * @param config The configuration object to hash.
216
+ * @returns A `sha256:`-prefixed hex digest.
217
+ */
218
+ function configHash(config) {
219
+ return `sha256:${createHash("sha256").update(canonicalJson(config)).digest("hex")}`;
220
+ }
221
+ /** A guard against unbounded recursion on pathologically nested input. */
222
+ const MAX_HASH_DEPTH = 100;
223
+ function canonicalJson(value, depth = 0) {
224
+ if (depth > MAX_HASH_DEPTH) throw new RangeError("Maximum depth exceeded while hashing the config.");
225
+ if (value === void 0) return "null";
226
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
227
+ const toJson = value.toJSON;
228
+ if (typeof toJson === "function") return canonicalJson(toJson.call(value), depth + 1);
229
+ if (Array.isArray(value)) return `[${value.map((v) => canonicalJson(v, depth + 1)).join(",")}]`;
230
+ return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v, depth + 1)}`).join(",")}}`;
231
+ }
232
+ function loadSummary(scenario) {
233
+ const { load, durationMs, warmupMs } = scenario;
234
+ const maxInFlight = load.maxInFlight == null ? {} : { maxInFlight: load.maxInFlight };
235
+ if (load.kind === "closed") return {
236
+ model: "closed",
237
+ concurrency: load.concurrency,
238
+ durationMs,
239
+ warmupMs,
240
+ ...maxInFlight
241
+ };
242
+ return {
243
+ model: "open",
244
+ ratePerSec: load.ratePerSec,
245
+ arrival: load.arrival,
246
+ durationMs,
247
+ warmupMs,
248
+ ...maxInFlight
249
+ };
250
+ }
251
+ //#endregion
252
+ export { buildReport, buildScenarioResult, configHash, detectEnvironment };
@@ -0,0 +1,74 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/result/expect/assert.ts
3
+ const ASSERT_RE = /^\s*(<=|>=|==|=|<|>)\s*(\d+(?:\.\d+)?)\s*(%|ms|s|\/s)?\s*$/;
4
+ const OP_MAP = {
5
+ "<": "lt",
6
+ "<=": "lte",
7
+ ">": "gt",
8
+ ">=": "gte",
9
+ "==": "eq",
10
+ "=": "eq"
11
+ };
12
+ /** An error raised when an `expect` assertion cannot be parsed. */
13
+ var AssertionParseError = class extends Error {};
14
+ /**
15
+ * Parses an `expect` assertion string.
16
+ * @param text The assertion, e.g. `">= 99%"`.
17
+ * @returns The parsed operator, normalized threshold, and unit.
18
+ * @throws {AssertionParseError} If the assertion cannot be parsed.
19
+ */
20
+ function parseAssertion(text) {
21
+ const match = text.match(ASSERT_RE);
22
+ if (match == null) throw new AssertionParseError(`Invalid expect assertion: ${JSON.stringify(text)}.`);
23
+ const op = OP_MAP[match[1]];
24
+ const value = Number.parseFloat(match[2]);
25
+ switch (match[3]) {
26
+ case "%": return {
27
+ op,
28
+ threshold: value / 100,
29
+ unit: "%"
30
+ };
31
+ case "ms": return {
32
+ op,
33
+ threshold: value,
34
+ unit: "ms"
35
+ };
36
+ case "s": return {
37
+ op,
38
+ threshold: value * 1e3,
39
+ unit: "ms"
40
+ };
41
+ case "/s": return {
42
+ op,
43
+ threshold: value,
44
+ unit: "/s"
45
+ };
46
+ default: return {
47
+ op,
48
+ threshold: value,
49
+ unit: null
50
+ };
51
+ }
52
+ }
53
+ /**
54
+ * Compares a measured value against a threshold using a comparison operator.
55
+ * @param actual The measured value.
56
+ * @param op The comparison operator.
57
+ * @param threshold The threshold.
58
+ * @param tolerant Whether `eq` allows a small floating-point tolerance. Pass
59
+ * `false` for exact (count) metrics; defaults to `true` so
60
+ * float-normalized thresholds (e.g. `"99.4%"` ->
61
+ * `0.9940000000000001`) still match a measured `0.994`.
62
+ * @returns Whether the comparison holds.
63
+ */
64
+ function compare(actual, op, threshold, tolerant = true) {
65
+ switch (op) {
66
+ case "lt": return actual < threshold;
67
+ case "lte": return actual <= threshold;
68
+ case "gt": return actual > threshold;
69
+ case "gte": return actual >= threshold;
70
+ case "eq": return tolerant ? Math.abs(actual - threshold) <= 1e-9 + 1e-9 * Math.abs(threshold) : actual === threshold;
71
+ }
72
+ }
73
+ //#endregion
74
+ export { AssertionParseError, compare, parseAssertion };
@@ -0,0 +1,128 @@
1
+ import "@js-temporal/polyfill";
2
+ import { AssertionParseError, compare, parseAssertion } from "./assert.js";
3
+ import { metricUnit } from "./metrics.js";
4
+ //#region src/bench/result/expect/evaluate.ts
5
+ /**
6
+ * Parses every assertion in an `expect` block, throwing on the first malformed
7
+ * one. Run during preflight so that a typo in a CI gate is reported as a
8
+ * configuration error before any load is sent, instead of crashing the run with
9
+ * an uncaught {@link AssertionParseError} after the traffic has already gone out.
10
+ * @param expect The scenario's `expect` block.
11
+ * @throws {AssertionParseError} If an entry has no assertion string or its
12
+ * assertion cannot be parsed.
13
+ */
14
+ function validateExpectBlock(expect) {
15
+ for (const [metric, value] of Object.entries(expect)) {
16
+ const assertion = typeof value === "string" ? value : value.assert;
17
+ if (typeof assertion !== "string") throw new AssertionParseError(`The \`expect\` entry for "${metric}" has no assertion string.`);
18
+ try {
19
+ parseAssertion(assertion);
20
+ } catch (error) {
21
+ if (!(error instanceof AssertionParseError)) throw error;
22
+ throw new AssertionParseError(`Invalid \`expect\` assertion for "${metric}": ${JSON.stringify(assertion)}.`);
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Evaluates an `expect` block against measured metrics.
28
+ * @param expect The scenario's `expect` block.
29
+ * @param metrics The measured metrics to evaluate against.
30
+ * @returns The evaluated assertions and whether the gate passed.
31
+ */
32
+ function evaluateExpect(expect, metrics) {
33
+ const results = [];
34
+ for (const [metric, value] of Object.entries(expect)) {
35
+ const assertion = typeof value === "string" ? value : value.assert;
36
+ const severity = typeof value === "string" ? "fail" : value.severity ?? "fail";
37
+ const { op, threshold, unit } = parseAssertion(assertion);
38
+ const lookup = lookupMetric(metrics, metric);
39
+ const actual = lookup?.value ?? null;
40
+ const pass = lookup != null && actual != null && unitCompatible(unit, lookup.unit) && compare(actual, op, threshold, lookup.unit !== "count");
41
+ results.push({
42
+ metric,
43
+ op,
44
+ threshold,
45
+ unit,
46
+ actual,
47
+ severity,
48
+ pass
49
+ });
50
+ }
51
+ return {
52
+ results,
53
+ passed: results.every((r) => r.severity === "warn" || r.pass)
54
+ };
55
+ }
56
+ /**
57
+ * Whether an assertion's (normalized) unit is compatible with a metric's
58
+ * natural unit. A unitless assertion is always compatible.
59
+ */
60
+ function unitCompatible(assertionUnit, unit) {
61
+ if (assertionUnit == null) return true;
62
+ switch (unit) {
63
+ case "ratio": return assertionUnit === "%";
64
+ case "ms": return assertionUnit === "ms";
65
+ case "rate": return assertionUnit === "/s";
66
+ case "count": return false;
67
+ }
68
+ }
69
+ function lookupMetric(metrics, metric) {
70
+ const unit = metricUnit(metric);
71
+ if (unit == null) return null;
72
+ return {
73
+ value: lookupValue(metrics, metric),
74
+ unit
75
+ };
76
+ }
77
+ function lookupValue(metrics, metric) {
78
+ switch (metric) {
79
+ case "successRate": return metrics.requests.successRate;
80
+ case "throughputPerSec": return metrics.throughputPerSec;
81
+ case "deliveryThroughput": return metrics.deliveryThroughputPerSec ?? null;
82
+ case "errors.total": return sumErrors(metrics.errors);
83
+ case "errors.4xx": return sumErrors(metrics.errors, {
84
+ min: 400,
85
+ max: 500
86
+ });
87
+ case "errors.5xx": return sumErrors(metrics.errors, {
88
+ min: 500,
89
+ max: 600
90
+ });
91
+ }
92
+ if (metric.startsWith("latency.")) return latencyField(metrics.client.latencyMs, metric.slice(8));
93
+ if (metric.startsWith("signatureVerification.")) return partialField(metrics.server?.signatureVerificationMs?.overall, metric.slice(22));
94
+ if (metric.startsWith("queueDrain.")) return partialField(metrics.server?.queue?.drainMs, metric.slice(11));
95
+ return null;
96
+ }
97
+ function latencyField(latency, key) {
98
+ switch (key) {
99
+ case "p50": return latency.p50;
100
+ case "p95": return latency.p95;
101
+ case "p99": return latency.p99;
102
+ case "mean": return latency.mean;
103
+ case "max": return latency.max;
104
+ default: return null;
105
+ }
106
+ }
107
+ function partialField(source, key) {
108
+ if (source == null) return null;
109
+ switch (key) {
110
+ case "p50": return source.p50 ?? null;
111
+ case "p95": return source.p95 ?? null;
112
+ case "p99": return source.p99 ?? null;
113
+ default: return null;
114
+ }
115
+ }
116
+ /**
117
+ * Sums error counts, optionally restricted to a half-open HTTP status range.
118
+ * The bounds are a single coupled argument so a caller cannot pass one without
119
+ * the other.
120
+ */
121
+ function sumErrors(errors, range) {
122
+ let total = 0;
123
+ for (const error of errors) if (range == null) total += error.count;
124
+ else if (error.status != null && error.status >= range.min && error.status < range.max) total += error.count;
125
+ return total;
126
+ }
127
+ //#endregion
128
+ export { evaluateExpect, validateExpectBlock };
@@ -0,0 +1,34 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/result/expect/metrics.ts
3
+ /**
4
+ * Returns the natural unit class of a metric, or `null` if the metric name is
5
+ * not recognized.
6
+ * @param metric The metric name, e.g. `"latency.p95"`.
7
+ */
8
+ function metricUnit(metric) {
9
+ switch (metric) {
10
+ case "successRate": return "ratio";
11
+ case "throughputPerSec":
12
+ case "deliveryThroughput": return "rate";
13
+ case "errors.total":
14
+ case "errors.4xx":
15
+ case "errors.5xx": return "count";
16
+ }
17
+ if (metric.startsWith("latency.") || metric.startsWith("signatureVerification.") || metric.startsWith("queueDrain.")) return "ms";
18
+ return null;
19
+ }
20
+ /**
21
+ * Returns the human display unit for a metric (`"%"`, `"ms"`, `"/s"`), or
22
+ * `null` for counts and unknown metrics.
23
+ * @param metric The metric name.
24
+ */
25
+ function metricDisplayUnit(metric) {
26
+ switch (metricUnit(metric)) {
27
+ case "ratio": return "%";
28
+ case "ms": return "ms";
29
+ case "rate": return "/s";
30
+ default: return null;
31
+ }
32
+ }
33
+ //#endregion
34
+ export { metricDisplayUnit, metricUnit };