@fedify/cli 2.3.0-dev.994 → 2.3.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 (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,125 @@
1
+ import "@js-temporal/polyfill";
2
+ import { asList } from "./coerce.js";
3
+ import { parseDuration, parseRate } from "./units.js";
4
+ //#region src/bench/scenario/normalize.ts
5
+ /**
6
+ * Normalizes a validated scenario suite into a fully resolved form the engine
7
+ * can execute: defaults applied, top-level scalar-or-list fields (`recipient`,
8
+ * `collection`, `fault`) coerced to arrays, durations and rates parsed to
9
+ * numbers, and the load model determined. Nested specs (`activity`, `source`)
10
+ * are passed through and coerced where they are consumed.
11
+ *
12
+ * It also enforces the cross-field rules that the JSON Schema cannot express,
13
+ * notably that the buffered signing modes require the target's signature time
14
+ * window to be off.
15
+ * @since 2.3.0
16
+ * @module
17
+ */
18
+ const DEFAULT_DURATION_MS = 6e4;
19
+ const DEFAULT_WARMUP_MS = 0;
20
+ const DEFAULT_RATE_PER_SEC = 50;
21
+ const DEFAULT_SIGNING = "pipeline";
22
+ const DEFAULT_RUNS = 3;
23
+ /** An error raised when a suite cannot be normalized. */
24
+ var SuiteNormalizeError = class extends Error {};
25
+ /**
26
+ * Normalizes a validated suite into resolved form.
27
+ * @param suite The validated suite.
28
+ * @param options Normalization options, such as a target override.
29
+ * @returns The resolved suite.
30
+ * @throws {SuiteNormalizeError} If the target is missing or a cross-field rule
31
+ * is violated.
32
+ */
33
+ function normalizeSuite(suite, options = {}) {
34
+ const targetString = options.target ?? suite.target;
35
+ if (targetString == null || targetString.trim() === "") throw new SuiteNormalizeError("No target URL: set `target` in the suite or pass --target.");
36
+ let target;
37
+ try {
38
+ target = new URL(targetString);
39
+ } catch {
40
+ throw new SuiteNormalizeError(`Invalid target URL: ${targetString}.`);
41
+ }
42
+ if (target.protocol !== "http:" && target.protocol !== "https:" || target.hostname === "" || target.username !== "" || target.password !== "") throw new SuiteNormalizeError(`Invalid target URL ${JSON.stringify(targetString)}: a benchmark target must be an http: or https: URL with a host and no embedded credentials (for example http://localhost:3000).`);
43
+ return {
44
+ target,
45
+ actors: suite.actors ?? [],
46
+ scenarios: suite.scenarios.map((scenario) => resolveScenario(scenario, suite))
47
+ };
48
+ }
49
+ function resolveScenario(scenario, suite) {
50
+ const defaults = suite.defaults ?? {};
51
+ const load = resolveLoad(defaults.load, scenario.load);
52
+ const signing = scenario.signing ?? defaults.signing ?? DEFAULT_SIGNING;
53
+ const signatureTimeWindow = scenario.signatureTimeWindow ?? defaults.signatureTimeWindow ?? false;
54
+ if (signing !== "jit" && signatureTimeWindow) throw new SuiteNormalizeError(`Scenario "${scenario.name}": ${signing} signing pre-signs requests, which requires the target's signature time window to be off; use signing: jit for a time-windowed target.`);
55
+ if (signing === "presign" && load.kind === "closed") throw new SuiteNormalizeError(`Scenario "${scenario.name}": presign signing needs a fixed request count, which a closed-loop (concurrency) load does not have; use an open-loop rate, or signing: pipeline or jit.`);
56
+ const durationMs = resolveDuration(scenario.duration ?? defaults.duration, DEFAULT_DURATION_MS);
57
+ const warmupMs = resolveDuration(scenario.warmup ?? defaults.warmup, DEFAULT_WARMUP_MS);
58
+ if (warmupMs >= durationMs) throw new SuiteNormalizeError(`Scenario "${scenario.name}": warmup (${warmupMs}ms) must be shorter than duration (${durationMs}ms); otherwise no requests are measured.`);
59
+ const runs = scenario.runs ?? defaults.runs ?? DEFAULT_RUNS;
60
+ return {
61
+ name: scenario.name,
62
+ type: scenario.type,
63
+ load,
64
+ durationMs,
65
+ warmupMs,
66
+ signing,
67
+ signatureTimeWindow,
68
+ runs,
69
+ recipients: asList(scenario.recipient),
70
+ inbox: scenario.inbox,
71
+ activity: scenario.activity,
72
+ authenticated: scenario.authenticated ?? false,
73
+ collections: asList(scenario.collection),
74
+ source: scenario.source,
75
+ sender: scenario.sender,
76
+ followers: scenario.followers,
77
+ queueDrainTimeoutMs: scenario.queueDrainTimeout == null ? void 0 : parseDuration(scenario.queueDrainTimeout),
78
+ faults: asList(scenario.fault),
79
+ expect: scenario.expect ?? {},
80
+ raw: scenario
81
+ };
82
+ }
83
+ /**
84
+ * Resolves the load model from suite defaults and a scenario override. The
85
+ * scenario's choice of `rate`/`concurrency` wins outright (it selects the
86
+ * model), while compatible fields such as `arrival` and `maxInFlight` are
87
+ * inherited from the defaults when the scenario does not set them.
88
+ */
89
+ function resolveLoad(defaults, scenario) {
90
+ const arrival = scenario?.arrival ?? defaults?.arrival ?? "constant";
91
+ const maxInFlight = scenario?.maxInFlight ?? defaults?.maxInFlight;
92
+ if (scenario?.concurrency != null) return {
93
+ kind: "closed",
94
+ concurrency: scenario.concurrency,
95
+ maxInFlight
96
+ };
97
+ if (scenario?.rate != null) return {
98
+ kind: "open",
99
+ ratePerSec: parseRate(scenario.rate),
100
+ arrival,
101
+ maxInFlight
102
+ };
103
+ if (defaults?.concurrency != null) return {
104
+ kind: "closed",
105
+ concurrency: defaults.concurrency,
106
+ maxInFlight
107
+ };
108
+ if (defaults?.rate != null) return {
109
+ kind: "open",
110
+ ratePerSec: parseRate(defaults.rate),
111
+ arrival,
112
+ maxInFlight
113
+ };
114
+ return {
115
+ kind: "open",
116
+ ratePerSec: DEFAULT_RATE_PER_SEC,
117
+ arrival,
118
+ maxInFlight
119
+ };
120
+ }
121
+ function resolveDuration(value, fallback) {
122
+ return value == null ? fallback : parseDuration(value);
123
+ }
124
+ //#endregion
125
+ export { normalizeSuite };
@@ -0,0 +1,399 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/scenario/schema.ts
3
+ /**
4
+ * The embedded JSON Schema (draft 2020-12) for benchmark scenario suite files.
5
+ *
6
+ * These objects are the runtime copies used by the validator; they are
7
+ * published, byte-for-byte, under *schema/bench/* and a drift guard keeps them
8
+ * in sync. The matching TypeScript types live in {@link ./types.ts}.
9
+ *
10
+ * The schema expresses every scenario type discussed for `fedify bench`
11
+ * (`inbox`, `webfinger`, `actor`, `object`, `fanout`, `collection`, `failure`,
12
+ * `mixed`). All but `collection` have runners in this version. Three
13
+ * cross-field rules are enforced here rather than in code:
14
+ *
15
+ * - exactly one HTTP request signature scheme per actor group
16
+ * (`contains` + `minContains`/`maxContains`);
17
+ * - `rate` XOR `concurrency` in a load block (`oneOf`);
18
+ * - the allowed `expect` metrics per scenario type (`if`/`then` +
19
+ * `propertyNames`).
20
+ * @since 2.3.0
21
+ * @module
22
+ */
23
+ /** The hosted URL that serves the current scenario schema. */
24
+ const SCENARIO_SCHEMA_ID = "https://json-schema.fedify.dev/bench/scenario-v2.json";
25
+ /** The hosted URL that serves the version 1 scenario schema. */
26
+ const SCENARIO_SCHEMA_ID_V1 = "https://json-schema.fedify.dev/bench/scenario-v1.json";
27
+ const READ_METRICS = [
28
+ "successRate",
29
+ "throughputPerSec",
30
+ "errors.total",
31
+ "errors.4xx",
32
+ "errors.5xx",
33
+ "latency.p50",
34
+ "latency.p95",
35
+ "latency.p99",
36
+ "latency.mean",
37
+ "latency.max"
38
+ ];
39
+ const INBOX_METRICS = [
40
+ ...READ_METRICS,
41
+ "signatureVerification.p50",
42
+ "signatureVerification.p95",
43
+ "signatureVerification.p99"
44
+ ];
45
+ const FANOUT_METRICS = [
46
+ "successRate",
47
+ "deliveryThroughput",
48
+ "errors.total",
49
+ "errors.4xx",
50
+ "errors.5xx",
51
+ "queueDrain.p50",
52
+ "queueDrain.p95",
53
+ "queueDrain.p99"
54
+ ];
55
+ const MIXED_METRICS = [...new Set([...INBOX_METRICS, ...FANOUT_METRICS])];
56
+ const MIXED_V2_METRICS = [...READ_METRICS, "deliveryThroughput"];
57
+ /** The benchmark scenario suite JSON Schema (draft 2020-12). */
58
+ const scenarioSchemaV1 = {
59
+ $schema: "https://json-schema.org/draft/2020-12/schema",
60
+ $id: SCENARIO_SCHEMA_ID_V1,
61
+ title: "Fedify benchmark scenario suite",
62
+ type: "object",
63
+ required: ["version", "scenarios"],
64
+ additionalProperties: false,
65
+ properties: {
66
+ $schema: {
67
+ type: "string",
68
+ description: "An optional editor hint pointing at this schema."
69
+ },
70
+ version: { const: 1 },
71
+ target: {
72
+ type: "string",
73
+ format: "uri",
74
+ description: "The target base URL; may be overridden by --target."
75
+ },
76
+ defaults: { $ref: "#/$defs/defaults" },
77
+ actors: {
78
+ type: "array",
79
+ items: { $ref: "#/$defs/actorGroup" }
80
+ },
81
+ scenarios: {
82
+ type: "array",
83
+ minItems: 1,
84
+ items: { $ref: "#/$defs/scenario" }
85
+ }
86
+ },
87
+ $defs: {
88
+ duration: {
89
+ type: "string",
90
+ pattern: "^\\d+(\\.\\d+)?(ms|s|m|h)$",
91
+ description: "A duration such as 500ms, 30s, 2m, or 1h."
92
+ },
93
+ rate: {
94
+ description: "An open-loop arrival rate such as 200/s, or a number.",
95
+ oneOf: [{
96
+ type: "number",
97
+ exclusiveMinimum: 0
98
+ }, {
99
+ type: "string",
100
+ pattern: "^\\d+(\\.\\d+)?\\s*/\\s*(s|m|h)$"
101
+ }]
102
+ },
103
+ size: {
104
+ description: "A byte size such as 2KB or a plain number of bytes.",
105
+ oneOf: [{
106
+ type: "number",
107
+ minimum: 0
108
+ }, {
109
+ type: "string",
110
+ pattern: "^\\s*\\d+(\\.\\d+)?\\s*([Bb]|[Kk][Bb]|[Kk][Ii][Bb]|[Mm][Bb]|[Mm][Ii][Bb]|[Gg][Bb]|[Gg][Ii][Bb])?\\s*$"
111
+ }]
112
+ },
113
+ signatureStandard: { enum: [
114
+ "draft-cavage-http-signatures-12",
115
+ "rfc9421",
116
+ "ld-signatures",
117
+ "fep8b32"
118
+ ] },
119
+ signingMode: { enum: [
120
+ "jit",
121
+ "pipeline",
122
+ "presign"
123
+ ] },
124
+ arrival: { enum: ["constant", "poisson"] },
125
+ scalarOrListString: { oneOf: [{ type: "string" }, {
126
+ type: "array",
127
+ items: { type: "string" },
128
+ minItems: 1
129
+ }] },
130
+ load: {
131
+ type: "object",
132
+ additionalProperties: false,
133
+ properties: {
134
+ rate: { $ref: "#/$defs/rate" },
135
+ concurrency: {
136
+ type: "integer",
137
+ minimum: 1
138
+ },
139
+ arrival: { $ref: "#/$defs/arrival" },
140
+ maxInFlight: {
141
+ type: "integer",
142
+ minimum: 1
143
+ }
144
+ },
145
+ not: { required: ["rate", "concurrency"] }
146
+ },
147
+ defaults: {
148
+ type: "object",
149
+ additionalProperties: false,
150
+ properties: {
151
+ duration: { $ref: "#/$defs/duration" },
152
+ warmup: { $ref: "#/$defs/duration" },
153
+ load: { $ref: "#/$defs/load" },
154
+ signing: { $ref: "#/$defs/signingMode" },
155
+ signatureTimeWindow: { type: "boolean" },
156
+ runs: {
157
+ type: "integer",
158
+ minimum: 1
159
+ }
160
+ }
161
+ },
162
+ actorGroup: {
163
+ type: "object",
164
+ additionalProperties: false,
165
+ required: ["signatureStandards"],
166
+ properties: {
167
+ name: { type: "string" },
168
+ count: {
169
+ type: "integer",
170
+ minimum: 1
171
+ },
172
+ signatureStandards: {
173
+ type: "array",
174
+ uniqueItems: true,
175
+ minItems: 1,
176
+ items: { $ref: "#/$defs/signatureStandard" },
177
+ contains: { enum: ["draft-cavage-http-signatures-12", "rfc9421"] },
178
+ minContains: 1,
179
+ maxContains: 1,
180
+ description: "Exactly one HTTP request signature scheme, plus optional document signature schemes."
181
+ }
182
+ }
183
+ },
184
+ generateDirective: {
185
+ type: "object",
186
+ additionalProperties: false,
187
+ required: ["generate"],
188
+ properties: {
189
+ generate: { enum: ["lorem"] },
190
+ size: { $ref: "#/$defs/size" }
191
+ }
192
+ },
193
+ content: { oneOf: [{ type: "string" }, { $ref: "#/$defs/generateDirective" }] },
194
+ objectSpec: {
195
+ type: "object",
196
+ properties: {
197
+ type: { $ref: "#/$defs/scalarOrListString" },
198
+ content: { $ref: "#/$defs/content" }
199
+ }
200
+ },
201
+ activitySpec: {
202
+ type: "object",
203
+ additionalProperties: false,
204
+ properties: {
205
+ type: { $ref: "#/$defs/scalarOrListString" },
206
+ embedObject: { type: "boolean" },
207
+ object: { $ref: "#/$defs/objectSpec" }
208
+ }
209
+ },
210
+ objectSource: { oneOf: [{ $ref: "#/$defs/scalarOrListString" }, {
211
+ type: "object",
212
+ additionalProperties: false,
213
+ required: ["seed"],
214
+ properties: {
215
+ seed: { $ref: "#/$defs/scalarOrListString" },
216
+ collection: { $ref: "#/$defs/scalarOrListString" },
217
+ limit: {
218
+ type: "integer",
219
+ minimum: 1
220
+ },
221
+ type: { $ref: "#/$defs/scalarOrListString" }
222
+ }
223
+ }] },
224
+ expectSeverity: { enum: ["warn", "fail"] },
225
+ expectValue: { oneOf: [{
226
+ type: "string",
227
+ description: "An assertion such as '>= 99%' or '< 100ms'."
228
+ }, {
229
+ type: "object",
230
+ additionalProperties: false,
231
+ required: ["assert"],
232
+ properties: {
233
+ assert: { type: "string" },
234
+ severity: { $ref: "#/$defs/expectSeverity" }
235
+ }
236
+ }] },
237
+ mixEntry: {
238
+ type: "object",
239
+ additionalProperties: false,
240
+ required: ["scenario", "weight"],
241
+ properties: {
242
+ scenario: { type: "string" },
243
+ weight: {
244
+ type: "number",
245
+ exclusiveMinimum: 0
246
+ }
247
+ }
248
+ },
249
+ scenario: {
250
+ type: "object",
251
+ additionalProperties: false,
252
+ required: ["name", "type"],
253
+ properties: {
254
+ name: { type: "string" },
255
+ type: { enum: [
256
+ "inbox",
257
+ "webfinger",
258
+ "actor",
259
+ "object",
260
+ "fanout",
261
+ "collection",
262
+ "failure",
263
+ "mixed"
264
+ ] },
265
+ load: { $ref: "#/$defs/load" },
266
+ duration: { $ref: "#/$defs/duration" },
267
+ warmup: { $ref: "#/$defs/duration" },
268
+ signing: { $ref: "#/$defs/signingMode" },
269
+ signatureTimeWindow: { type: "boolean" },
270
+ runs: {
271
+ type: "integer",
272
+ minimum: 1
273
+ },
274
+ expect: {
275
+ type: "object",
276
+ additionalProperties: { $ref: "#/$defs/expectValue" }
277
+ },
278
+ recipient: { $ref: "#/$defs/scalarOrListString" },
279
+ inbox: { type: "string" },
280
+ activity: { $ref: "#/$defs/activitySpec" },
281
+ authenticated: { type: "boolean" },
282
+ collection: { $ref: "#/$defs/scalarOrListString" },
283
+ source: { $ref: "#/$defs/objectSource" },
284
+ sender: { type: "string" },
285
+ followers: {
286
+ type: "integer",
287
+ minimum: 1
288
+ },
289
+ trigger: { type: "object" },
290
+ sinkBehavior: { type: "object" },
291
+ queueDrainTimeout: { $ref: "#/$defs/duration" },
292
+ fault: { $ref: "#/$defs/scalarOrListString" },
293
+ mix: {
294
+ type: "array",
295
+ minItems: 1,
296
+ items: { $ref: "#/$defs/mixEntry" }
297
+ }
298
+ },
299
+ allOf: [
300
+ {
301
+ if: { properties: { type: { const: "inbox" } } },
302
+ then: {
303
+ required: ["recipient"],
304
+ properties: { expect: { propertyNames: { enum: INBOX_METRICS } } }
305
+ }
306
+ },
307
+ {
308
+ if: { properties: { type: { const: "webfinger" } } },
309
+ then: {
310
+ required: ["recipient"],
311
+ properties: { expect: { propertyNames: { enum: READ_METRICS } } }
312
+ }
313
+ },
314
+ {
315
+ if: { properties: { type: { const: "actor" } } },
316
+ then: {
317
+ required: ["recipient"],
318
+ properties: { expect: { propertyNames: { enum: INBOX_METRICS } } }
319
+ }
320
+ },
321
+ {
322
+ if: { properties: { type: { const: "object" } } },
323
+ then: {
324
+ required: ["source"],
325
+ properties: { expect: { propertyNames: { enum: READ_METRICS } } }
326
+ }
327
+ },
328
+ {
329
+ if: { properties: { type: { const: "collection" } } },
330
+ then: {
331
+ required: ["recipient"],
332
+ properties: { expect: { propertyNames: { enum: READ_METRICS } } }
333
+ }
334
+ },
335
+ {
336
+ if: { properties: { type: { const: "fanout" } } },
337
+ then: {
338
+ required: ["sender"],
339
+ properties: { expect: { propertyNames: { enum: FANOUT_METRICS } } }
340
+ }
341
+ },
342
+ {
343
+ if: { properties: { type: { const: "failure" } } },
344
+ then: {
345
+ required: ["fault"],
346
+ properties: { expect: { propertyNames: { enum: READ_METRICS } } }
347
+ }
348
+ },
349
+ {
350
+ if: { properties: { type: { const: "mixed" } } },
351
+ then: {
352
+ required: ["mix"],
353
+ properties: { expect: { propertyNames: { enum: MIXED_METRICS } } }
354
+ }
355
+ }
356
+ ]
357
+ }
358
+ }
359
+ };
360
+ /** The current benchmark scenario suite JSON Schema (draft 2020-12). */
361
+ const scenarioSchemaV2 = {
362
+ ...scenarioSchemaV1,
363
+ $id: SCENARIO_SCHEMA_ID,
364
+ $defs: {
365
+ ...scenarioSchemaV1.$defs,
366
+ scenario: {
367
+ ...scenarioSchemaV1.$defs.scenario,
368
+ properties: {
369
+ ...scenarioSchemaV1.$defs.scenario.properties,
370
+ sinkBase: { type: "string" }
371
+ },
372
+ allOf: scenarioSchemaV1.$defs.scenario.allOf.map((condition) => condition.if.properties.type.const === "failure" ? {
373
+ if: condition.if,
374
+ then: { properties: condition.then.properties }
375
+ } : condition.if.properties.type.const === "actor" || condition.if.properties.type.const === "object" ? {
376
+ if: condition.if,
377
+ then: {
378
+ required: condition.then.required,
379
+ allOf: [{
380
+ if: {
381
+ required: ["authenticated"],
382
+ properties: { authenticated: { const: true } }
383
+ },
384
+ then: { properties: { expect: { propertyNames: { enum: INBOX_METRICS } } } },
385
+ else: { properties: { expect: { propertyNames: { enum: READ_METRICS } } } }
386
+ }]
387
+ }
388
+ } : condition.if.properties.type.const === "mixed" ? {
389
+ if: condition.if,
390
+ then: {
391
+ required: condition.then.required,
392
+ properties: { expect: { propertyNames: { enum: MIXED_V2_METRICS } } }
393
+ }
394
+ } : condition)
395
+ }
396
+ }
397
+ };
398
+ //#endregion
399
+ export { scenarioSchemaV2 };
@@ -0,0 +1,56 @@
1
+ import "@js-temporal/polyfill";
2
+ //#region src/bench/scenario/units.ts
3
+ /**
4
+ * Parsers for the human-friendly duration and rate units used in scenario
5
+ * files.
6
+ * @since 2.3.0
7
+ * @module
8
+ */
9
+ const DURATION_RE = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/;
10
+ const DURATION_UNITS = {
11
+ ms: 1,
12
+ s: 1e3,
13
+ m: 6e4,
14
+ h: 36e5
15
+ };
16
+ const RATE_RE = /^(\d+(?:\.\d+)?)\s*\/\s*(s|m|h)$/;
17
+ const RATE_DIVISORS = {
18
+ s: 1,
19
+ m: 60,
20
+ h: 3600
21
+ };
22
+ /**
23
+ * Parses a duration such as `"500ms"`, `"30s"`, `"2m"`, or `"1h"` into
24
+ * milliseconds.
25
+ * @param value The duration string.
26
+ * @returns The duration in milliseconds.
27
+ * @throws {RangeError} If the value cannot be parsed.
28
+ */
29
+ function parseDuration(value) {
30
+ const match = value.match(DURATION_RE);
31
+ if (match == null) throw new RangeError(`Invalid duration: ${JSON.stringify(value)}.`);
32
+ const ms = Number.parseFloat(match[1]) * DURATION_UNITS[match[2]];
33
+ if (!Number.isFinite(ms)) throw new RangeError(`Duration out of range: ${JSON.stringify(value)}.`);
34
+ return ms;
35
+ }
36
+ /**
37
+ * Parses an open-loop arrival rate into requests per second. A bare number is
38
+ * interpreted as requests per second; a string such as `"200/s"`, `"60/m"`, or
39
+ * `"3600/h"` carries an explicit time unit.
40
+ * @param value The rate string or number.
41
+ * @returns The rate in requests per second.
42
+ * @throws {RangeError} If the value cannot be parsed or is not positive.
43
+ */
44
+ function parseRate(value) {
45
+ if (typeof value === "number") {
46
+ if (!Number.isFinite(value) || value <= 0) throw new RangeError(`Invalid rate: ${value}.`);
47
+ return value;
48
+ }
49
+ const match = value.match(RATE_RE);
50
+ if (match == null) throw new RangeError(`Invalid rate: ${JSON.stringify(value)}.`);
51
+ const rate = Number.parseFloat(match[1]) / RATE_DIVISORS[match[2]];
52
+ if (!Number.isFinite(rate) || rate <= 0) throw new RangeError(`Invalid rate: ${JSON.stringify(value)}.`);
53
+ return rate;
54
+ }
55
+ //#endregion
56
+ export { parseDuration, parseRate };
@@ -0,0 +1,29 @@
1
+ import "@js-temporal/polyfill";
2
+ import { scenarioSchemaV2 } from "./schema.js";
3
+ import { SuiteValidationError } from "./errors.js";
4
+ import { Validator } from "@cfworker/json-schema";
5
+ //#region src/bench/scenario/validate.ts
6
+ /**
7
+ * Runtime validation of scenario suites against the embedded JSON Schema.
8
+ * @since 2.3.0
9
+ * @module
10
+ */
11
+ let validator;
12
+ function getValidator() {
13
+ validator ??= new Validator(scenarioSchemaV2, "2020-12");
14
+ return validator;
15
+ }
16
+ /**
17
+ * Validates a parsed scenario suite against the schema and narrows its type.
18
+ * @param raw The parsed (but untyped) suite value.
19
+ * @param source An optional source label (e.g. a file path) for error messages.
20
+ * @returns The validated suite.
21
+ * @throws {SuiteValidationError} If the value does not satisfy the schema.
22
+ */
23
+ function validateSuite(raw, source) {
24
+ const result = getValidator().validate(raw);
25
+ if (!result.valid) throw new SuiteValidationError(result.errors, source);
26
+ return raw;
27
+ }
28
+ //#endregion
29
+ export { validateSuite };
@@ -0,0 +1,38 @@
1
+ import "@js-temporal/polyfill";
2
+ import { convertUrlIfHandle } from "../../webfinger/lib.js";
3
+ import { actorUrlsFromRecipients } from "./object-discovery.js";
4
+ import { isBareHttpUrl } from "./runner.js";
5
+ import { runReadLoad } from "./read.js";
6
+ //#region src/bench/scenarios/actor.ts
7
+ /**
8
+ * The `actor` scenario runner.
9
+ * @since 2.3.0
10
+ * @module
11
+ */
12
+ /** The `actor` scenario runner. */
13
+ const actorRunner = {
14
+ validate(scenario) {
15
+ if (scenario.recipients.length < 1) throw new Error("The actor scenario requires a recipient.");
16
+ for (const recipient of scenario.recipients) {
17
+ let url;
18
+ try {
19
+ url = convertUrlIfHandle(recipient);
20
+ } catch {
21
+ throw new Error(`Scenario "${scenario.name}": invalid actor recipient ${JSON.stringify(recipient)}.`);
22
+ }
23
+ if (url.protocol !== "acct:" && !isBareHttpUrl(url)) throw new Error(`Scenario "${scenario.name}": actor recipient must be an acct: handle or a bare http(s) URL with a host and no credentials; got ${JSON.stringify(url.href)}.`);
24
+ }
25
+ },
26
+ async run(context) {
27
+ this.validate?.(context.scenario);
28
+ return await runReadLoad(context, {
29
+ urls: await actorUrlsFromRecipients(context.scenario.recipients, {
30
+ target: context.target,
31
+ fetch: context.fetch
32
+ }),
33
+ authenticated: context.scenario.authenticated
34
+ });
35
+ }
36
+ };
37
+ //#endregion
38
+ export { actorRunner };