@venn-lang/load 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @venn-lang/load
2
+
3
+ > The `load` namespace: describe a load profile, run it, assert on the metrics.
4
+
5
+ Three builders turn a sentence about traffic into a profile value, and a fourth verb hands that
6
+ profile to the `LoadRunner` port and gives you back latency percentiles and an error rate. The
7
+ profile is plain data, so a flow can hold it, pass it and read it before anything is driven.
8
+
9
+ ## Install
10
+
11
+ Nothing to install. `@venn-lang/load` is part of the standard library, and the CLI and the language
12
+ server both load [`@venn-lang/stdlib`](../stdlib), which lists every stdlib plugin. A file reaches the
13
+ namespace with one line.
14
+
15
+ ```ruby
16
+ use "venn/load"
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ module demo.load
23
+
24
+ use "venn/load"
25
+ use "venn/assert"
26
+
27
+ flow "Checkout under load" {
28
+ step "ramp to 200 VUs" {
29
+ const profile = load.ramp 0 200 { over: "30s", hold: "5m" }
30
+ const metrics = load.run profile
31
+
32
+ expect metrics.p95 < 800
33
+ expect metrics.errorRate < 0.05
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Verbs
39
+
40
+ | Verb | Call | Result |
41
+ | --- | --- | --- |
42
+ | `load.ramp` | `load.ramp 0 200 { over: "30s", hold: "5m" }` | `load.Ramp` |
43
+ | `load.constant` | `load.constant 50 { over: "10s" }` | `load.Constant` |
44
+ | `load.spike` | `load.spike 500 { at: "5s" }` | `load.Spike` |
45
+ | `load.run` | `load.run profile` | `load.Metrics` |
46
+
47
+ The three builders are pure: they compute a descriptor and touch nothing. Only `load.run` reaches
48
+ the port.
49
+
50
+ `load.ramp` takes two positional arguments, the virtual users it starts from and the number it ends
51
+ at. The spec's `0 -> 200` arrow sugar is not in the grammar.
52
+
53
+ **Durations are written as strings.** `over`, `hold` and `at` are read by the SDK's `Duration`
54
+ schema, which accepts `"30s"`, `"2m"` or a plain count of milliseconds and yields milliseconds. The
55
+ language's own `30s` unit literal is a different value, and passing it is refused before the run
56
+ with `"over" is not a valid option`. Write `{ over: "30s" }` or `{ over: 30000 }`.
57
+
58
+ ## The types it publishes
59
+
60
+ | Type | Shape |
61
+ | --- | --- |
62
+ | `load.Ramp` | `{ kind: "ramp", from, to, over?, hold? }` |
63
+ | `load.Constant` | `{ kind: "constant", vus, over? }` |
64
+ | `load.Spike` | `{ kind: "spike", peak, at? }` |
65
+ | `load.Profile` | Whichever of the three a builder produced. This is what `load.run` takes. |
66
+ | `load.Metrics` | `{ vus, rps, p50, p95, p99, errorRate }` |
67
+
68
+ The durations inside a profile are numbers, not durations: `Duration` already turned `"30s"` into
69
+ `30000` on the way in, so what a profile holds is a count of milliseconds.
70
+
71
+ ## The LoadRunner port
72
+
73
+ | | |
74
+ | --- | --- |
75
+ | id | `venn.port.load-runner` |
76
+ | version | 1 |
77
+ | requires | `net` |
78
+ | methods | `run` |
79
+
80
+ Two implementations ship together, which is what makes this a port rather than a module with a good
81
+ interface:
82
+
83
+ - `createFakeLoadRunner()` derives canned metrics from the profile's peak VUs, with no real traffic.
84
+ This is the one [`@venn-lang/stdlib`](../stdlib) binds, so a load flow is runnable offline.
85
+ - `createRealLoadRunner()` is a stub. Every call throws a `VennError` with code `VN8090`, because
86
+ this repository is the language and drives no real load.
87
+
88
+ Both run the same conformance suite, `loadRunnerConformance` in `src/runner/load-runner.suite.ts`,
89
+ which holds them to two invariants: `p50 <= p95 <= p99`, and the reported `vus` is the profile's
90
+ peak.
91
+
92
+ ## API
93
+
94
+ | Export | What it is |
95
+ | --- | --- |
96
+ | `loadPlugin` | The plugin definition: namespace `load`, `requires: ["net"]`. Also the default export. |
97
+ | `rampProfile({ from, to, over?, hold? })` | Builds a `RampProfile`. |
98
+ | `constantProfile({ vus, over? })` | Builds a `ConstantProfile`. |
99
+ | `spikeProfile({ peak, at? })` | Builds a `SpikeProfile`. |
100
+ | `peakVus(profile)` | The peak concurrent VUs a profile reaches, which is what a runner drives toward. |
101
+ | `RampProfile`, `ConstantProfile`, `SpikeProfile` | The three descriptor types. |
102
+ | `LoadProfile` | Their union: what the runner consumes. |
103
+ | `LoadRunnerPort` | The port descriptor. |
104
+ | `LoadRunner` | The port interface: `run(profile)`. |
105
+ | `createFakeLoadRunner()` | The fake runner. |
106
+ | `createRealLoadRunner()` | The real runner, stubbed to throw `VN8090`. |
107
+ | `LoadMetrics` | The metrics type. |
108
+ | `LoadMetricsSchema` | The Zod schema registered as the plugin's nominal `LoadMetrics` type. |
109
+
110
+ Binding a different runner means one entry in the runner's port list:
111
+
112
+ ```ts
113
+ import { createFakeLoadRunner, LoadRunnerPort } from "@venn-lang/load";
114
+ import { createRunner } from "@venn-lang/runtime";
115
+
116
+ const runner = createRunner({
117
+ host,
118
+ plugins,
119
+ sink,
120
+ uri,
121
+ ports: [{ port: LoadRunnerPort, impl: createFakeLoadRunner() }],
122
+ });
123
+ ```
124
+
125
+ ## See also
126
+
127
+ - [`@venn-lang/sdk`](../sdk) for `definePlugin`, `defineAction` and the `Duration` schema.
128
+ - [`@venn-lang/http`](../std-http) for the requests a load profile is usually pointed at.
129
+ - [`@venn-lang/artifacts`](../std-artifacts) for filing what a run produced.
@@ -0,0 +1,124 @@
1
+ import { PluginDefinition, ZodType } from "@venn-lang/sdk";
2
+ import { Port } from "@venn-lang/contracts";
3
+ //#region src/metrics/load-metrics.types.d.ts
4
+ /**
5
+ * What a load run reports back. Latencies are in milliseconds and the ordering
6
+ * `p50 <= p95 <= p99` always holds; the conformance suite enforces it.
7
+ */
8
+ interface LoadMetrics {
9
+ /** Peak concurrent virtual users the run reached. */
10
+ vus: number;
11
+ /** Requests per second, averaged over the run. */
12
+ rps: number;
13
+ p50: number;
14
+ p95: number;
15
+ p99: number;
16
+ /** Share of failed requests, from 0 to 1. */
17
+ errorRate: number;
18
+ }
19
+ //#endregion
20
+ //#region src/metrics/load-metrics.schema.d.ts
21
+ /** Runtime validator for the nominal `load.Metrics` type. */
22
+ declare const LoadMetricsSchema: ZodType<LoadMetrics>;
23
+ //#endregion
24
+ //#region src/plugin.d.ts
25
+ /**
26
+ * The `@venn-lang/load` plugin. Registers the `load` namespace: three profile
27
+ * builders, the `run` verb that executes one, and the profile and metrics
28
+ * types. Requires the `net` capability, since a run drives real traffic.
29
+ */
30
+ declare const loadPlugin: PluginDefinition;
31
+ //#endregion
32
+ //#region src/profiles/load-profile.types.d.ts
33
+ /** A ramp from `from` to `to` VUs over `over`, then held for `hold` (ms). */
34
+ interface RampProfile {
35
+ kind: "ramp";
36
+ from: number;
37
+ to: number;
38
+ over?: number;
39
+ hold?: number;
40
+ }
41
+ /** A constant `vus` load sustained over `over` (ms). */
42
+ interface ConstantProfile {
43
+ kind: "constant";
44
+ vus: number;
45
+ over?: number;
46
+ }
47
+ /** A single spike to `peak` VUs at `at` (ms into the run). */
48
+ interface SpikeProfile {
49
+ kind: "spike";
50
+ peak: number;
51
+ at?: number;
52
+ }
53
+ /** Whatever a builder produced. `kind` tells the runner which shape it holds. */
54
+ type LoadProfile = RampProfile | ConstantProfile | SpikeProfile;
55
+ //#endregion
56
+ //#region src/profiles/build-profiles.d.ts
57
+ /**
58
+ * Builds a ramp profile: a climb from `from` to `to` virtual users across
59
+ * `over` milliseconds, then held at `to` for `hold` milliseconds.
60
+ *
61
+ * @returns the profile. Nothing is executed here.
62
+ */
63
+ declare function rampProfile(args: {
64
+ from: number;
65
+ to: number;
66
+ over?: number;
67
+ hold?: number;
68
+ }): RampProfile;
69
+ /** Builds a flat profile: `vus` virtual users held for `over` milliseconds. */
70
+ declare function constantProfile(args: {
71
+ vus: number;
72
+ over?: number;
73
+ }): ConstantProfile;
74
+ /** Builds a burst profile: one spike to `peak` virtual users, `at` ms into the run. */
75
+ declare function spikeProfile(args: {
76
+ peak: number;
77
+ at?: number;
78
+ }): SpikeProfile;
79
+ /**
80
+ * The highest concurrency a profile calls for, whichever shape it is. This is
81
+ * the number a runner sizes itself against.
82
+ */
83
+ declare function peakVus(profile: LoadProfile): number;
84
+ //#endregion
85
+ //#region src/runner/load-runner.types.d.ts
86
+ /** What turns a profile into traffic. */
87
+ interface LoadRunner {
88
+ /**
89
+ * Executes one profile to completion.
90
+ *
91
+ * @returns the metrics observed, with `p50 <= p95 <= p99`.
92
+ * @throws {VennError} `VN8090` from an implementation that cannot run.
93
+ */
94
+ run(profile: LoadProfile): Promise<LoadMetrics>;
95
+ }
96
+ //#endregion
97
+ //#region src/runner/fake-runner.d.ts
98
+ /**
99
+ * The offline `LoadRunner`. Derives plausible metrics from the profile's peak
100
+ * concurrency and sends no traffic.
101
+ *
102
+ * @returns a runner whose results are deterministic for a given profile.
103
+ */
104
+ declare function createFakeLoadRunner(): LoadRunner;
105
+ //#endregion
106
+ //#region src/runner/load-runner.port.d.ts
107
+ /**
108
+ * The port `load.run` drives traffic through. Requires the `net` capability, so
109
+ * a host without it fails to load the plugin rather than failing mid-run.
110
+ */
111
+ declare const LoadRunnerPort: Port<LoadRunner>;
112
+ //#endregion
113
+ //#region src/runner/real-runner.d.ts
114
+ /**
115
+ * The traffic-generating `LoadRunner`. Not wired up in this build.
116
+ *
117
+ * @returns a runner whose `run` throws, so the gap is a legible failure rather
118
+ * than metrics nobody measured.
119
+ * @throws {VennError} `VN8090` on every `run`.
120
+ */
121
+ declare function createRealLoadRunner(): LoadRunner;
122
+ //#endregion
123
+ export { type ConstantProfile, type LoadMetrics, LoadMetricsSchema, type LoadProfile, type LoadRunner, LoadRunnerPort, type RampProfile, type SpikeProfile, constantProfile, createFakeLoadRunner, createRealLoadRunner, loadPlugin as default, loadPlugin, peakVus, rampProfile, spikeProfile };
124
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/metrics/load-metrics.types.ts","../src/metrics/load-metrics.schema.ts","../src/plugin.ts","../src/profiles/load-profile.types.ts","../src/profiles/build-profiles.ts","../src/runner/load-runner.types.ts","../src/runner/fake-runner.ts","../src/runner/load-runner.port.ts","../src/runner/real-runner.ts"],"mappings":";;;;;;;UAIiB;;EAEf;;EAEA;EACA;EACA;EACA;;EAEA;;;;;cCTW,mBAAmB,QAAQ;;;;;;;;cCM3B,YAAY;;;;UCTR;EACf;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA;;;UAIe;EACf;EACA;EACA;;;KAIU,cAAc,cAAc,kBAAkB;;;;;;;;;iBCX1C,YAAY;EAC1B;EACA;EACA;EACA;IACE;;iBAKY,gBAAgB;EAAQ;EAAa;IAAkB;;iBAKvD,aAAa;EAAQ;EAAc;IAAgB;;;;;iBAQnD,QAAQ,SAAS;;;;UChChB;;;;;;;EAOf,IAAI,SAAS,cAAc,QAAQ;;;;;;;;;;iBCDrB,wBAAwB;;;;;;;cCH3B,gBAAgB,KAAK;;;;;;;;;;iBCGlB,wBAAwB"}
package/dist/index.js ADDED
@@ -0,0 +1,280 @@
1
+ import { Duration, arg, defineAction, definePlugin, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { VennError } from "@venn-lang/contracts";
4
+ //#region src/metrics/load-metrics.schema.ts
5
+ /** Runtime validator for the nominal `load.Metrics` type. */
6
+ const LoadMetricsSchema = z.object({
7
+ vus: z.number(),
8
+ rps: z.number(),
9
+ p50: z.number(),
10
+ p95: z.number(),
11
+ p99: z.number(),
12
+ errorRate: z.number()
13
+ });
14
+ //#endregion
15
+ //#region src/profiles/build-profiles.ts
16
+ /**
17
+ * Builds a ramp profile: a climb from `from` to `to` virtual users across
18
+ * `over` milliseconds, then held at `to` for `hold` milliseconds.
19
+ *
20
+ * @returns the profile. Nothing is executed here.
21
+ */
22
+ function rampProfile(args) {
23
+ return {
24
+ kind: "ramp",
25
+ from: args.from,
26
+ to: args.to,
27
+ over: args.over,
28
+ hold: args.hold
29
+ };
30
+ }
31
+ /** Builds a flat profile: `vus` virtual users held for `over` milliseconds. */
32
+ function constantProfile(args) {
33
+ return {
34
+ kind: "constant",
35
+ vus: args.vus,
36
+ over: args.over
37
+ };
38
+ }
39
+ /** Builds a burst profile: one spike to `peak` virtual users, `at` ms into the run. */
40
+ function spikeProfile(args) {
41
+ return {
42
+ kind: "spike",
43
+ peak: args.peak,
44
+ at: args.at
45
+ };
46
+ }
47
+ /**
48
+ * The highest concurrency a profile calls for, whichever shape it is. This is
49
+ * the number a runner sizes itself against.
50
+ */
51
+ function peakVus(profile) {
52
+ if (profile.kind === "ramp") return profile.to;
53
+ if (profile.kind === "constant") return profile.vus;
54
+ return profile.peak;
55
+ }
56
+ /**
57
+ * `load.constant 50 { over: "10s" }`.
58
+ *
59
+ * Describes a flat load: `vus` virtual users held steady for `over`. Nothing
60
+ * runs until the profile is handed to `load.run`.
61
+ *
62
+ * @returns a `load.Constant` profile.
63
+ */
64
+ const constantAction = defineAction({
65
+ name: "constant",
66
+ doc: "Build a constant-VUs load profile.",
67
+ params: z.object({ over: Duration.optional() }).optional(),
68
+ args: [arg("vus", t.number, "How many virtual users, held steady.")],
69
+ result: t.ref("load.Constant"),
70
+ run: (_ctx, input) => buildConstant(input)
71
+ });
72
+ function buildConstant(input) {
73
+ const params = input.params ?? {};
74
+ return constantProfile({
75
+ vus: Number(input.args[0] ?? 0),
76
+ over: params.over
77
+ });
78
+ }
79
+ /**
80
+ * `load.ramp 0 200 { over: "30s", hold: "5m" }`.
81
+ *
82
+ * Describes a climb from `from` to `to` virtual users across `over`, then held
83
+ * at `to` for `hold`. Nothing runs until the profile is handed to `load.run`.
84
+ *
85
+ * The grammar has no `0 -> 200` arrow sugar, so the two ends are separate
86
+ * positional arguments. `over` and `hold` accept `"30s"` or a millisecond count.
87
+ *
88
+ * @returns a `load.Ramp` profile.
89
+ */
90
+ const rampAction = defineAction({
91
+ name: "ramp",
92
+ doc: "Build a ramp load profile from `from` to `to` VUs.",
93
+ params: z.object({
94
+ over: Duration.optional(),
95
+ hold: Duration.optional()
96
+ }).optional(),
97
+ args: [arg("from", t.number, "Virtual users at the start."), arg("to", t.number, "Virtual users at the end.")],
98
+ result: t.ref("load.Ramp"),
99
+ run: (_ctx, input) => buildRamp(input)
100
+ });
101
+ function buildRamp(input) {
102
+ const params = input.params ?? {};
103
+ return rampProfile({
104
+ from: Number(input.args[0] ?? 0),
105
+ to: Number(input.args[1] ?? 0),
106
+ over: params.over,
107
+ hold: params.hold
108
+ });
109
+ }
110
+ //#endregion
111
+ //#region src/runner/fake-runner.ts
112
+ /**
113
+ * The offline `LoadRunner`. Derives plausible metrics from the profile's peak
114
+ * concurrency and sends no traffic.
115
+ *
116
+ * @returns a runner whose results are deterministic for a given profile.
117
+ */
118
+ function createFakeLoadRunner() {
119
+ return { run: async (profile) => metricsFor(profile) };
120
+ }
121
+ function metricsFor(profile) {
122
+ const vus = peakVus(profile);
123
+ return {
124
+ vus,
125
+ rps: vus * 10,
126
+ p50: 50,
127
+ p95: 120,
128
+ p99: 200,
129
+ errorRate: .01
130
+ };
131
+ }
132
+ //#endregion
133
+ //#region src/runner/load-runner.port.ts
134
+ /**
135
+ * The port `load.run` drives traffic through. Requires the `net` capability, so
136
+ * a host without it fails to load the plugin rather than failing mid-run.
137
+ */
138
+ const LoadRunnerPort = {
139
+ id: "venn.port.load-runner",
140
+ version: 1,
141
+ requires: ["net"],
142
+ methods: ["run"]
143
+ };
144
+ //#endregion
145
+ //#region src/runner/real-runner.ts
146
+ /**
147
+ * The traffic-generating `LoadRunner`. Not wired up in this build.
148
+ *
149
+ * @returns a runner whose `run` throws, so the gap is a legible failure rather
150
+ * than metrics nobody measured.
151
+ * @throws {VennError} `VN8090` on every `run`.
152
+ */
153
+ function createRealLoadRunner() {
154
+ return { run: async () => notImplemented() };
155
+ }
156
+ function notImplemented() {
157
+ throw new VennError({
158
+ code: "VN8090",
159
+ message: "The load runner is not implemented in this build."
160
+ });
161
+ }
162
+ //#endregion
163
+ //#region src/actions/run.ts
164
+ /**
165
+ * `load.run (load.ramp 0 200 { over: "30s" })`.
166
+ *
167
+ * Drives one profile through the `LoadRunner` port. This is the verb that
168
+ * generates traffic; the builders only describe a shape.
169
+ *
170
+ * @returns the `load.Metrics` the run produced.
171
+ * @throws {VennError} `VN8090` when the host's runner cannot execute a profile.
172
+ */
173
+ const runAction = defineAction({
174
+ name: "run",
175
+ doc: "Run a load profile and return its metrics.",
176
+ args: [arg("profile", t.ref("load.Profile"), "A profile, as `load.ramp` and friends build.")],
177
+ result: t.ref("load.Metrics"),
178
+ run: (ctx, input) => ctx.port(LoadRunnerPort).run(profileOf(input))
179
+ });
180
+ function profileOf(input) {
181
+ return input.args[0];
182
+ }
183
+ /**
184
+ * `load.spike 500 { at: "5s" }`.
185
+ *
186
+ * Describes a single burst to `peak` virtual users, `at` into the run. Nothing
187
+ * runs until the profile is handed to `load.run`.
188
+ *
189
+ * @returns a `load.Spike` profile.
190
+ */
191
+ const spikeAction = defineAction({
192
+ name: "spike",
193
+ doc: "Build a spike load profile that peaks at `peak` VUs.",
194
+ params: z.object({ at: Duration.optional() }).optional(),
195
+ args: [arg("peak", t.number, "Virtual users at the top of the spike.")],
196
+ result: t.ref("load.Spike"),
197
+ run: (_ctx, input) => buildSpike(input)
198
+ });
199
+ function buildSpike(input) {
200
+ const params = input.params ?? {};
201
+ return spikeProfile({
202
+ peak: Number(input.args[0] ?? 0),
203
+ at: params.at
204
+ });
205
+ }
206
+ //#endregion
207
+ //#region src/actions/index.ts
208
+ /** Every verb in the `load` namespace, in the order the plugin registers them. */
209
+ const loadActions = [
210
+ rampAction,
211
+ constantAction,
212
+ spikeAction,
213
+ runAction
214
+ ];
215
+ //#endregion
216
+ //#region src/types.ts
217
+ /**
218
+ * The types `@venn-lang/load` publishes to the checker, under the `load` namespace:
219
+ * the three profiles its builders return, the union `load.run` accepts, and the
220
+ * metrics it yields. They mirror `profiles/load-profile.types.ts` and
221
+ * `metrics/load-metrics.types.ts` field by field, dropping the `Load` and
222
+ * `Profile` affixes the namespace already says: a flow reads `load.Ramp`.
223
+ *
224
+ * Durations are `number` here on purpose. The `Duration` schema accepts `"30s"`
225
+ * or a plain millisecond count, not the language's own `duration` value, and
226
+ * yields milliseconds. What a profile holds is therefore a count.
227
+ */
228
+ const loadTypeDefs = {
229
+ /** A ramp from `from` to `to` VUs over `over`, then held for `hold`. */
230
+ Ramp: t.record({
231
+ kind: t.literal("ramp"),
232
+ from: t.number,
233
+ to: t.number,
234
+ over: t.number,
235
+ hold: t.number
236
+ }, { optional: ["over", "hold"] }),
237
+ /** A constant `vus` load sustained over `over`. */
238
+ Constant: t.record({
239
+ kind: t.literal("constant"),
240
+ vus: t.number,
241
+ over: t.number
242
+ }, { optional: ["over"] }),
243
+ /** A single spike to `peak` VUs at `at` into the run. */
244
+ Spike: t.record({
245
+ kind: t.literal("spike"),
246
+ peak: t.number,
247
+ at: t.number
248
+ }, { optional: ["at"] }),
249
+ /** Whatever a builder produced. This is what `load.run` takes. */
250
+ Profile: t.union(t.ref("load.Ramp"), t.ref("load.Constant"), t.ref("load.Spike")),
251
+ /** What a run yields. The invariant `p50 <= p95 <= p99` always holds. */
252
+ Metrics: t.record({
253
+ vus: t.number,
254
+ rps: t.number,
255
+ p50: t.number,
256
+ p95: t.number,
257
+ p99: t.number,
258
+ errorRate: t.number
259
+ })
260
+ };
261
+ //#endregion
262
+ //#region src/plugin.ts
263
+ /**
264
+ * The `@venn-lang/load` plugin. Registers the `load` namespace: three profile
265
+ * builders, the `run` verb that executes one, and the profile and metrics
266
+ * types. Requires the `net` capability, since a run drives real traffic.
267
+ */
268
+ const loadPlugin = definePlugin({
269
+ name: "venn/load",
270
+ version: "0.0.0",
271
+ namespace: "load",
272
+ requires: ["net"],
273
+ actions: loadActions,
274
+ types: { LoadMetrics: LoadMetricsSchema },
275
+ typeDefs: loadTypeDefs
276
+ });
277
+ //#endregion
278
+ export { LoadMetricsSchema, LoadRunnerPort, constantProfile, createFakeLoadRunner, createRealLoadRunner, loadPlugin as default, loadPlugin, peakVus, rampProfile, spikeProfile };
279
+
280
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/metrics/load-metrics.schema.ts","../src/profiles/build-profiles.ts","../src/actions/constant.ts","../src/actions/ramp.ts","../src/runner/fake-runner.ts","../src/runner/load-runner.port.ts","../src/runner/real-runner.ts","../src/actions/run.ts","../src/actions/spike.ts","../src/actions/index.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import { type ZodType, z } from \"@venn-lang/sdk\";\nimport type { LoadMetrics } from \"./load-metrics.types.js\";\n\n/** Runtime validator for the nominal `load.Metrics` type. */\nexport const LoadMetricsSchema: ZodType<LoadMetrics> = z.object({\n vus: z.number(),\n rps: z.number(),\n p50: z.number(),\n p95: z.number(),\n p99: z.number(),\n errorRate: z.number(),\n});\n","import type {\n ConstantProfile,\n LoadProfile,\n RampProfile,\n SpikeProfile,\n} from \"./load-profile.types.js\";\n\n/**\n * Builds a ramp profile: a climb from `from` to `to` virtual users across\n * `over` milliseconds, then held at `to` for `hold` milliseconds.\n *\n * @returns the profile. Nothing is executed here.\n */\nexport function rampProfile(args: {\n from: number;\n to: number;\n over?: number;\n hold?: number;\n}): RampProfile {\n return { kind: \"ramp\", from: args.from, to: args.to, over: args.over, hold: args.hold };\n}\n\n/** Builds a flat profile: `vus` virtual users held for `over` milliseconds. */\nexport function constantProfile(args: { vus: number; over?: number }): ConstantProfile {\n return { kind: \"constant\", vus: args.vus, over: args.over };\n}\n\n/** Builds a burst profile: one spike to `peak` virtual users, `at` ms into the run. */\nexport function spikeProfile(args: { peak: number; at?: number }): SpikeProfile {\n return { kind: \"spike\", peak: args.peak, at: args.at };\n}\n\n/**\n * The highest concurrency a profile calls for, whichever shape it is. This is\n * the number a runner sizes itself against.\n */\nexport function peakVus(profile: LoadProfile): number {\n if (profile.kind === \"ramp\") return profile.to;\n if (profile.kind === \"constant\") return profile.vus;\n return profile.peak;\n}\n","import {\n type ActionDefinition,\n type ActionInput,\n arg,\n Duration,\n defineAction,\n z,\n} from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { type ConstantProfile, constantProfile } from \"../profiles/index.js\";\n\nconst constantParams = z.object({ over: Duration.optional() });\n\n/**\n * `load.constant 50 { over: \"10s\" }`.\n *\n * Describes a flat load: `vus` virtual users held steady for `over`. Nothing\n * runs until the profile is handed to `load.run`.\n *\n * @returns a `load.Constant` profile.\n */\nexport const constantAction: ActionDefinition = defineAction({\n name: \"constant\",\n doc: \"Build a constant-VUs load profile.\",\n params: constantParams.optional(),\n args: [arg(\"vus\", t.number, \"How many virtual users, held steady.\")],\n result: t.ref(\"load.Constant\"),\n run: (_ctx, input) => buildConstant(input),\n});\n\nfunction buildConstant(input: ActionInput<unknown>): ConstantProfile {\n const params = (input.params ?? {}) as { over?: number };\n return constantProfile({ vus: Number(input.args[0] ?? 0), over: params.over });\n}\n","import {\n type ActionDefinition,\n type ActionInput,\n arg,\n Duration,\n defineAction,\n z,\n} from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { type RampProfile, rampProfile } from \"../profiles/index.js\";\n\nconst rampParams = z.object({ over: Duration.optional(), hold: Duration.optional() });\n\n/**\n * `load.ramp 0 200 { over: \"30s\", hold: \"5m\" }`.\n *\n * Describes a climb from `from` to `to` virtual users across `over`, then held\n * at `to` for `hold`. Nothing runs until the profile is handed to `load.run`.\n *\n * The grammar has no `0 -> 200` arrow sugar, so the two ends are separate\n * positional arguments. `over` and `hold` accept `\"30s\"` or a millisecond count.\n *\n * @returns a `load.Ramp` profile.\n */\nexport const rampAction: ActionDefinition = defineAction({\n name: \"ramp\",\n doc: \"Build a ramp load profile from `from` to `to` VUs.\",\n params: rampParams.optional(),\n args: [\n arg(\"from\", t.number, \"Virtual users at the start.\"),\n arg(\"to\", t.number, \"Virtual users at the end.\"),\n ],\n result: t.ref(\"load.Ramp\"),\n run: (_ctx, input) => buildRamp(input),\n});\n\nfunction buildRamp(input: ActionInput<unknown>): RampProfile {\n const params = (input.params ?? {}) as { over?: number; hold?: number };\n return rampProfile({\n from: Number(input.args[0] ?? 0),\n to: Number(input.args[1] ?? 0),\n over: params.over,\n hold: params.hold,\n });\n}\n","import type { LoadMetrics } from \"../metrics/index.js\";\nimport { type LoadProfile, peakVus } from \"../profiles/index.js\";\nimport type { LoadRunner } from \"./load-runner.types.js\";\n\n/**\n * The offline `LoadRunner`. Derives plausible metrics from the profile's peak\n * concurrency and sends no traffic.\n *\n * @returns a runner whose results are deterministic for a given profile.\n */\nexport function createFakeLoadRunner(): LoadRunner {\n return {\n run: async (profile) => metricsFor(profile),\n };\n}\n\nfunction metricsFor(profile: LoadProfile): LoadMetrics {\n const vus = peakVus(profile);\n return { vus, rps: vus * 10, p50: 50, p95: 120, p99: 200, errorRate: 0.01 };\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { LoadRunner } from \"./load-runner.types.js\";\n\n/**\n * The port `load.run` drives traffic through. Requires the `net` capability, so\n * a host without it fails to load the plugin rather than failing mid-run.\n */\nexport const LoadRunnerPort: Port<LoadRunner> = {\n id: \"venn.port.load-runner\",\n version: 1,\n requires: [\"net\"],\n methods: [\"run\"],\n};\n","import { VennError } from \"@venn-lang/contracts\";\nimport type { LoadRunner } from \"./load-runner.types.js\";\n\n/**\n * The traffic-generating `LoadRunner`. Not wired up in this build.\n *\n * @returns a runner whose `run` throws, so the gap is a legible failure rather\n * than metrics nobody measured.\n * @throws {VennError} `VN8090` on every `run`.\n */\nexport function createRealLoadRunner(): LoadRunner {\n return {\n run: async () => notImplemented(),\n };\n}\n\nfunction notImplemented(): never {\n throw new VennError({\n code: \"VN8090\",\n message: \"The load runner is not implemented in this build.\",\n });\n}\n","import { type ActionDefinition, type ActionInput, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport type { LoadProfile } from \"../profiles/index.js\";\nimport { LoadRunnerPort } from \"../runner/index.js\";\n\n/**\n * `load.run (load.ramp 0 200 { over: \"30s\" })`.\n *\n * Drives one profile through the `LoadRunner` port. This is the verb that\n * generates traffic; the builders only describe a shape.\n *\n * @returns the `load.Metrics` the run produced.\n * @throws {VennError} `VN8090` when the host's runner cannot execute a profile.\n */\nexport const runAction: ActionDefinition = defineAction({\n name: \"run\",\n doc: \"Run a load profile and return its metrics.\",\n args: [arg(\"profile\", t.ref(\"load.Profile\"), \"A profile, as `load.ramp` and friends build.\")],\n result: t.ref(\"load.Metrics\"),\n run: (ctx, input) => ctx.port(LoadRunnerPort).run(profileOf(input)),\n});\n\nfunction profileOf(input: ActionInput<unknown>): LoadProfile {\n return input.args[0] as LoadProfile;\n}\n","import {\n type ActionDefinition,\n type ActionInput,\n arg,\n Duration,\n defineAction,\n z,\n} from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { type SpikeProfile, spikeProfile } from \"../profiles/index.js\";\n\nconst spikeParams = z.object({ at: Duration.optional() });\n\n/**\n * `load.spike 500 { at: \"5s\" }`.\n *\n * Describes a single burst to `peak` virtual users, `at` into the run. Nothing\n * runs until the profile is handed to `load.run`.\n *\n * @returns a `load.Spike` profile.\n */\nexport const spikeAction: ActionDefinition = defineAction({\n name: \"spike\",\n doc: \"Build a spike load profile that peaks at `peak` VUs.\",\n params: spikeParams.optional(),\n args: [arg(\"peak\", t.number, \"Virtual users at the top of the spike.\")],\n result: t.ref(\"load.Spike\"),\n run: (_ctx, input) => buildSpike(input),\n});\n\nfunction buildSpike(input: ActionInput<unknown>): SpikeProfile {\n const params = (input.params ?? {}) as { at?: number };\n return spikeProfile({ peak: Number(input.args[0] ?? 0), at: params.at });\n}\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { constantAction } from \"./constant.js\";\nimport { rampAction } from \"./ramp.js\";\nimport { runAction } from \"./run.js\";\nimport { spikeAction } from \"./spike.js\";\n\n/** Every verb in the `load` namespace, in the order the plugin registers them. */\nexport const loadActions: ActionDefinition[] = [rampAction, constantAction, spikeAction, runAction];\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types `@venn-lang/load` publishes to the checker, under the `load` namespace:\n * the three profiles its builders return, the union `load.run` accepts, and the\n * metrics it yields. They mirror `profiles/load-profile.types.ts` and\n * `metrics/load-metrics.types.ts` field by field, dropping the `Load` and\n * `Profile` affixes the namespace already says: a flow reads `load.Ramp`.\n *\n * Durations are `number` here on purpose. The `Duration` schema accepts `\"30s\"`\n * or a plain millisecond count, not the language's own `duration` value, and\n * yields milliseconds. What a profile holds is therefore a count.\n */\nexport const loadTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /** A ramp from `from` to `to` VUs over `over`, then held for `hold`. */\n Ramp: t.record(\n { kind: t.literal(\"ramp\"), from: t.number, to: t.number, over: t.number, hold: t.number },\n { optional: [\"over\", \"hold\"] },\n ),\n /** A constant `vus` load sustained over `over`. */\n Constant: t.record(\n { kind: t.literal(\"constant\"), vus: t.number, over: t.number },\n { optional: [\"over\"] },\n ),\n /** A single spike to `peak` VUs at `at` into the run. */\n Spike: t.record({ kind: t.literal(\"spike\"), peak: t.number, at: t.number }, { optional: [\"at\"] }),\n /** Whatever a builder produced. This is what `load.run` takes. */\n Profile: t.union(t.ref(\"load.Ramp\"), t.ref(\"load.Constant\"), t.ref(\"load.Spike\")),\n /** What a run yields. The invariant `p50 <= p95 <= p99` always holds. */\n Metrics: t.record({\n vus: t.number,\n rps: t.number,\n p50: t.number,\n p95: t.number,\n p99: t.number,\n errorRate: t.number,\n }),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { loadActions } from \"./actions/index.js\";\nimport { LoadMetricsSchema } from \"./metrics/index.js\";\nimport { loadTypeDefs } from \"./types.js\";\n\n/**\n * The `@venn-lang/load` plugin. Registers the `load` namespace: three profile\n * builders, the `run` verb that executes one, and the profile and metrics\n * types. Requires the `net` capability, since a run drives real traffic.\n */\nexport const loadPlugin: PluginDefinition = definePlugin({\n name: \"venn/load\",\n version: \"0.0.0\",\n namespace: \"load\",\n requires: [\"net\"],\n actions: loadActions,\n types: { LoadMetrics: LoadMetricsSchema },\n typeDefs: loadTypeDefs,\n});\n"],"mappings":";;;;;AAIA,MAAa,oBAA0C,EAAE,OAAO;CAC9D,KAAK,EAAE,OAAO;CACd,KAAK,EAAE,OAAO;CACd,KAAK,EAAE,OAAO;CACd,KAAK,EAAE,OAAO;CACd,KAAK,EAAE,OAAO;CACd,WAAW,EAAE,OAAO;AACtB,CAAC;;;;;;;;;ACED,SAAgB,YAAY,MAKZ;CACd,OAAO;EAAE,MAAM;EAAQ,MAAM,KAAK;EAAM,IAAI,KAAK;EAAI,MAAM,KAAK;EAAM,MAAM,KAAK;CAAK;AACxF;;AAGA,SAAgB,gBAAgB,MAAuD;CACrF,OAAO;EAAE,MAAM;EAAY,KAAK,KAAK;EAAK,MAAM,KAAK;CAAK;AAC5D;;AAGA,SAAgB,aAAa,MAAmD;CAC9E,OAAO;EAAE,MAAM;EAAS,MAAM,KAAK;EAAM,IAAI,KAAK;CAAG;AACvD;;;;;AAMA,SAAgB,QAAQ,SAA8B;CACpD,IAAI,QAAQ,SAAS,QAAQ,OAAO,QAAQ;CAC5C,IAAI,QAAQ,SAAS,YAAY,OAAO,QAAQ;CAChD,OAAO,QAAQ;AACjB;;;;;;;;;ACnBA,MAAa,iBAAmC,aAAa;CAC3D,MAAM;CACN,KAAK;CACL,QAbqB,EAAE,OAAO,EAAE,MAAM,SAAS,SAAS,EAAE,CAalD,CAAA,CAAe,SAAS;CAChC,MAAM,CAAC,IAAI,OAAO,EAAE,QAAQ,sCAAsC,CAAC;CACnE,QAAQ,EAAE,IAAI,eAAe;CAC7B,MAAM,MAAM,UAAU,cAAc,KAAK;AAC3C,CAAC;AAED,SAAS,cAAc,OAA8C;CACnE,MAAM,SAAU,MAAM,UAAU,CAAC;CACjC,OAAO,gBAAgB;EAAE,KAAK,OAAO,MAAM,KAAK,MAAM,CAAC;EAAG,MAAM,OAAO;CAAK,CAAC;AAC/E;;;;;;;;;;;;ACTA,MAAa,aAA+B,aAAa;CACvD,MAAM;CACN,KAAK;CACL,QAhBiB,EAAE,OAAO;EAAE,MAAM,SAAS,SAAS;EAAG,MAAM,SAAS,SAAS;CAAE,CAgBzE,CAAA,CAAW,SAAS;CAC5B,MAAM,CACJ,IAAI,QAAQ,EAAE,QAAQ,6BAA6B,GACnD,IAAI,MAAM,EAAE,QAAQ,2BAA2B,CACjD;CACA,QAAQ,EAAE,IAAI,WAAW;CACzB,MAAM,MAAM,UAAU,UAAU,KAAK;AACvC,CAAC;AAED,SAAS,UAAU,OAA0C;CAC3D,MAAM,SAAU,MAAM,UAAU,CAAC;CACjC,OAAO,YAAY;EACjB,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC;EAC/B,IAAI,OAAO,MAAM,KAAK,MAAM,CAAC;EAC7B,MAAM,OAAO;EACb,MAAM,OAAO;CACf,CAAC;AACH;;;;;;;;;AClCA,SAAgB,uBAAmC;CACjD,OAAO,EACL,KAAK,OAAO,YAAY,WAAW,OAAO,EAC5C;AACF;AAEA,SAAS,WAAW,SAAmC;CACrD,MAAM,MAAM,QAAQ,OAAO;CAC3B,OAAO;EAAE;EAAK,KAAK,MAAM;EAAI,KAAK;EAAI,KAAK;EAAK,KAAK;EAAK,WAAW;CAAK;AAC5E;;;;;;;ACZA,MAAa,iBAAmC;CAC9C,IAAI;CACJ,SAAS;CACT,UAAU,CAAC,KAAK;CAChB,SAAS,CAAC,KAAK;AACjB;;;;;;;;;;ACFA,SAAgB,uBAAmC;CACjD,OAAO,EACL,KAAK,YAAY,eAAe,EAClC;AACF;AAEA,SAAS,iBAAwB;CAC/B,MAAM,IAAI,UAAU;EAClB,MAAM;EACN,SAAS;CACX,CAAC;AACH;;;;;;;;;;;;ACPA,MAAa,YAA8B,aAAa;CACtD,MAAM;CACN,KAAK;CACL,MAAM,CAAC,IAAI,WAAW,EAAE,IAAI,cAAc,GAAG,8CAA8C,CAAC;CAC5F,QAAQ,EAAE,IAAI,cAAc;CAC5B,MAAM,KAAK,UAAU,IAAI,KAAK,cAAc,CAAC,CAAC,IAAI,UAAU,KAAK,CAAC;AACpE,CAAC;AAED,SAAS,UAAU,OAA0C;CAC3D,OAAO,MAAM,KAAK;AACpB;;;;;;;;;ACHA,MAAa,cAAgC,aAAa;CACxD,MAAM;CACN,KAAK;CACL,QAbkB,EAAE,OAAO,EAAE,IAAI,SAAS,SAAS,EAAE,CAa7C,CAAA,CAAY,SAAS;CAC7B,MAAM,CAAC,IAAI,QAAQ,EAAE,QAAQ,wCAAwC,CAAC;CACtE,QAAQ,EAAE,IAAI,YAAY;CAC1B,MAAM,MAAM,UAAU,WAAW,KAAK;AACxC,CAAC;AAED,SAAS,WAAW,OAA2C;CAC7D,MAAM,SAAU,MAAM,UAAU,CAAC;CACjC,OAAO,aAAa;EAAE,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC;EAAG,IAAI,OAAO;CAAG,CAAC;AACzE;;;;AC1BA,MAAa,cAAkC;CAAC;CAAY;CAAgB;CAAa;AAAS;;;;;;;;;;;;;;ACMlG,MAAa,eAAmD;;CAE9D,MAAM,EAAE,OACN;EAAE,MAAM,EAAE,QAAQ,MAAM;EAAG,MAAM,EAAE;EAAQ,IAAI,EAAE;EAAQ,MAAM,EAAE;EAAQ,MAAM,EAAE;CAAO,GACxF,EAAE,UAAU,CAAC,QAAQ,MAAM,EAAE,CAC/B;;CAEA,UAAU,EAAE,OACV;EAAE,MAAM,EAAE,QAAQ,UAAU;EAAG,KAAK,EAAE;EAAQ,MAAM,EAAE;CAAO,GAC7D,EAAE,UAAU,CAAC,MAAM,EAAE,CACvB;;CAEA,OAAO,EAAE,OAAO;EAAE,MAAM,EAAE,QAAQ,OAAO;EAAG,MAAM,EAAE;EAAQ,IAAI,EAAE;CAAO,GAAG,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;;CAEhG,SAAS,EAAE,MAAM,EAAE,IAAI,WAAW,GAAG,EAAE,IAAI,eAAe,GAAG,EAAE,IAAI,YAAY,CAAC;;CAEhF,SAAS,EAAE,OAAO;EAChB,KAAK,EAAE;EACP,KAAK,EAAE;EACP,KAAK,EAAE;EACP,KAAK,EAAE;EACP,KAAK,EAAE;EACP,WAAW,EAAE;CACf,CAAC;AACH;;;;;;;;AC3BA,MAAa,aAA+B,aAAa;CACvD,MAAM;CACN,SAAS;CACT,WAAW;CACX,UAAU,CAAC,KAAK;CAChB,SAAS;CACT,OAAO,EAAE,aAAa,kBAAkB;CACxC,UAAU;AACZ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@venn-lang/load",
3
+ "version": "0.1.0",
4
+ "description": "The load namespace: describe a load profile, run it, assert on the metrics.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "load"
10
+ ],
11
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-load#readme",
12
+ "bugs": "https://github.com/venn-lang/venn/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/venn-lang/venn.git",
16
+ "directory": "packages/std-load"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Vinicius Borges",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "development": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "!src/**/*.test.ts",
34
+ "!src/**/*.suite.ts"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@venn-lang/sdk": "0.1.0",
41
+ "@venn-lang/contracts": "0.1.0",
42
+ "@venn-lang/types": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "^0.22.14",
46
+ "typescript": "^7.0.2",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "vitest run",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ type ActionDefinition,
3
+ type ActionInput,
4
+ arg,
5
+ Duration,
6
+ defineAction,
7
+ z,
8
+ } from "@venn-lang/sdk";
9
+ import { t } from "@venn-lang/types";
10
+ import { type ConstantProfile, constantProfile } from "../profiles/index.js";
11
+
12
+ const constantParams = z.object({ over: Duration.optional() });
13
+
14
+ /**
15
+ * `load.constant 50 { over: "10s" }`.
16
+ *
17
+ * Describes a flat load: `vus` virtual users held steady for `over`. Nothing
18
+ * runs until the profile is handed to `load.run`.
19
+ *
20
+ * @returns a `load.Constant` profile.
21
+ */
22
+ export const constantAction: ActionDefinition = defineAction({
23
+ name: "constant",
24
+ doc: "Build a constant-VUs load profile.",
25
+ params: constantParams.optional(),
26
+ args: [arg("vus", t.number, "How many virtual users, held steady.")],
27
+ result: t.ref("load.Constant"),
28
+ run: (_ctx, input) => buildConstant(input),
29
+ });
30
+
31
+ function buildConstant(input: ActionInput<unknown>): ConstantProfile {
32
+ const params = (input.params ?? {}) as { over?: number };
33
+ return constantProfile({ vus: Number(input.args[0] ?? 0), over: params.over });
34
+ }
@@ -0,0 +1,8 @@
1
+ import type { ActionDefinition } from "@venn-lang/sdk";
2
+ import { constantAction } from "./constant.js";
3
+ import { rampAction } from "./ramp.js";
4
+ import { runAction } from "./run.js";
5
+ import { spikeAction } from "./spike.js";
6
+
7
+ /** Every verb in the `load` namespace, in the order the plugin registers them. */
8
+ export const loadActions: ActionDefinition[] = [rampAction, constantAction, spikeAction, runAction];
@@ -0,0 +1,45 @@
1
+ import {
2
+ type ActionDefinition,
3
+ type ActionInput,
4
+ arg,
5
+ Duration,
6
+ defineAction,
7
+ z,
8
+ } from "@venn-lang/sdk";
9
+ import { t } from "@venn-lang/types";
10
+ import { type RampProfile, rampProfile } from "../profiles/index.js";
11
+
12
+ const rampParams = z.object({ over: Duration.optional(), hold: Duration.optional() });
13
+
14
+ /**
15
+ * `load.ramp 0 200 { over: "30s", hold: "5m" }`.
16
+ *
17
+ * Describes a climb from `from` to `to` virtual users across `over`, then held
18
+ * at `to` for `hold`. Nothing runs until the profile is handed to `load.run`.
19
+ *
20
+ * The grammar has no `0 -> 200` arrow sugar, so the two ends are separate
21
+ * positional arguments. `over` and `hold` accept `"30s"` or a millisecond count.
22
+ *
23
+ * @returns a `load.Ramp` profile.
24
+ */
25
+ export const rampAction: ActionDefinition = defineAction({
26
+ name: "ramp",
27
+ doc: "Build a ramp load profile from `from` to `to` VUs.",
28
+ params: rampParams.optional(),
29
+ args: [
30
+ arg("from", t.number, "Virtual users at the start."),
31
+ arg("to", t.number, "Virtual users at the end."),
32
+ ],
33
+ result: t.ref("load.Ramp"),
34
+ run: (_ctx, input) => buildRamp(input),
35
+ });
36
+
37
+ function buildRamp(input: ActionInput<unknown>): RampProfile {
38
+ const params = (input.params ?? {}) as { over?: number; hold?: number };
39
+ return rampProfile({
40
+ from: Number(input.args[0] ?? 0),
41
+ to: Number(input.args[1] ?? 0),
42
+ over: params.over,
43
+ hold: params.hold,
44
+ });
45
+ }
@@ -0,0 +1,25 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import type { LoadProfile } from "../profiles/index.js";
4
+ import { LoadRunnerPort } from "../runner/index.js";
5
+
6
+ /**
7
+ * `load.run (load.ramp 0 200 { over: "30s" })`.
8
+ *
9
+ * Drives one profile through the `LoadRunner` port. This is the verb that
10
+ * generates traffic; the builders only describe a shape.
11
+ *
12
+ * @returns the `load.Metrics` the run produced.
13
+ * @throws {VennError} `VN8090` when the host's runner cannot execute a profile.
14
+ */
15
+ export const runAction: ActionDefinition = defineAction({
16
+ name: "run",
17
+ doc: "Run a load profile and return its metrics.",
18
+ args: [arg("profile", t.ref("load.Profile"), "A profile, as `load.ramp` and friends build.")],
19
+ result: t.ref("load.Metrics"),
20
+ run: (ctx, input) => ctx.port(LoadRunnerPort).run(profileOf(input)),
21
+ });
22
+
23
+ function profileOf(input: ActionInput<unknown>): LoadProfile {
24
+ return input.args[0] as LoadProfile;
25
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ type ActionDefinition,
3
+ type ActionInput,
4
+ arg,
5
+ Duration,
6
+ defineAction,
7
+ z,
8
+ } from "@venn-lang/sdk";
9
+ import { t } from "@venn-lang/types";
10
+ import { type SpikeProfile, spikeProfile } from "../profiles/index.js";
11
+
12
+ const spikeParams = z.object({ at: Duration.optional() });
13
+
14
+ /**
15
+ * `load.spike 500 { at: "5s" }`.
16
+ *
17
+ * Describes a single burst to `peak` virtual users, `at` into the run. Nothing
18
+ * runs until the profile is handed to `load.run`.
19
+ *
20
+ * @returns a `load.Spike` profile.
21
+ */
22
+ export const spikeAction: ActionDefinition = defineAction({
23
+ name: "spike",
24
+ doc: "Build a spike load profile that peaks at `peak` VUs.",
25
+ params: spikeParams.optional(),
26
+ args: [arg("peak", t.number, "Virtual users at the top of the spike.")],
27
+ result: t.ref("load.Spike"),
28
+ run: (_ctx, input) => buildSpike(input),
29
+ });
30
+
31
+ function buildSpike(input: ActionInput<unknown>): SpikeProfile {
32
+ const params = (input.params ?? {}) as { at?: number };
33
+ return spikeProfile({ peak: Number(input.args[0] ?? 0), at: params.at });
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@venn-lang/load`: the verbs that describe a load shape (`ramp`, `constant`,
3
+ * `spike`), the `LoadRunner` port that drives it, and the metrics it yields.
4
+ */
5
+
6
+ export * from "./metrics/index.js";
7
+ export { loadPlugin, loadPlugin as default } from "./plugin.js";
8
+ export * from "./profiles/index.js";
9
+ export * from "./runner/index.js";
@@ -0,0 +1,2 @@
1
+ export { LoadMetricsSchema } from "./load-metrics.schema.js";
2
+ export type { LoadMetrics } from "./load-metrics.types.js";
@@ -0,0 +1,12 @@
1
+ import { type ZodType, z } from "@venn-lang/sdk";
2
+ import type { LoadMetrics } from "./load-metrics.types.js";
3
+
4
+ /** Runtime validator for the nominal `load.Metrics` type. */
5
+ export const LoadMetricsSchema: ZodType<LoadMetrics> = z.object({
6
+ vus: z.number(),
7
+ rps: z.number(),
8
+ p50: z.number(),
9
+ p95: z.number(),
10
+ p99: z.number(),
11
+ errorRate: z.number(),
12
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What a load run reports back. Latencies are in milliseconds and the ordering
3
+ * `p50 <= p95 <= p99` always holds; the conformance suite enforces it.
4
+ */
5
+ export interface LoadMetrics {
6
+ /** Peak concurrent virtual users the run reached. */
7
+ vus: number;
8
+ /** Requests per second, averaged over the run. */
9
+ rps: number;
10
+ p50: number;
11
+ p95: number;
12
+ p99: number;
13
+ /** Share of failed requests, from 0 to 1. */
14
+ errorRate: number;
15
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { loadActions } from "./actions/index.js";
3
+ import { LoadMetricsSchema } from "./metrics/index.js";
4
+ import { loadTypeDefs } from "./types.js";
5
+
6
+ /**
7
+ * The `@venn-lang/load` plugin. Registers the `load` namespace: three profile
8
+ * builders, the `run` verb that executes one, and the profile and metrics
9
+ * types. Requires the `net` capability, since a run drives real traffic.
10
+ */
11
+ export const loadPlugin: PluginDefinition = definePlugin({
12
+ name: "venn/load",
13
+ version: "0.0.0",
14
+ namespace: "load",
15
+ requires: ["net"],
16
+ actions: loadActions,
17
+ types: { LoadMetrics: LoadMetricsSchema },
18
+ typeDefs: loadTypeDefs,
19
+ });
@@ -0,0 +1,41 @@
1
+ import type {
2
+ ConstantProfile,
3
+ LoadProfile,
4
+ RampProfile,
5
+ SpikeProfile,
6
+ } from "./load-profile.types.js";
7
+
8
+ /**
9
+ * Builds a ramp profile: a climb from `from` to `to` virtual users across
10
+ * `over` milliseconds, then held at `to` for `hold` milliseconds.
11
+ *
12
+ * @returns the profile. Nothing is executed here.
13
+ */
14
+ export function rampProfile(args: {
15
+ from: number;
16
+ to: number;
17
+ over?: number;
18
+ hold?: number;
19
+ }): RampProfile {
20
+ return { kind: "ramp", from: args.from, to: args.to, over: args.over, hold: args.hold };
21
+ }
22
+
23
+ /** Builds a flat profile: `vus` virtual users held for `over` milliseconds. */
24
+ export function constantProfile(args: { vus: number; over?: number }): ConstantProfile {
25
+ return { kind: "constant", vus: args.vus, over: args.over };
26
+ }
27
+
28
+ /** Builds a burst profile: one spike to `peak` virtual users, `at` ms into the run. */
29
+ export function spikeProfile(args: { peak: number; at?: number }): SpikeProfile {
30
+ return { kind: "spike", peak: args.peak, at: args.at };
31
+ }
32
+
33
+ /**
34
+ * The highest concurrency a profile calls for, whichever shape it is. This is
35
+ * the number a runner sizes itself against.
36
+ */
37
+ export function peakVus(profile: LoadProfile): number {
38
+ if (profile.kind === "ramp") return profile.to;
39
+ if (profile.kind === "constant") return profile.vus;
40
+ return profile.peak;
41
+ }
@@ -0,0 +1,7 @@
1
+ export { constantProfile, peakVus, rampProfile, spikeProfile } from "./build-profiles.js";
2
+ export type {
3
+ ConstantProfile,
4
+ LoadProfile,
5
+ RampProfile,
6
+ SpikeProfile,
7
+ } from "./load-profile.types.js";
@@ -0,0 +1,25 @@
1
+ /** A ramp from `from` to `to` VUs over `over`, then held for `hold` (ms). */
2
+ export interface RampProfile {
3
+ kind: "ramp";
4
+ from: number;
5
+ to: number;
6
+ over?: number;
7
+ hold?: number;
8
+ }
9
+
10
+ /** A constant `vus` load sustained over `over` (ms). */
11
+ export interface ConstantProfile {
12
+ kind: "constant";
13
+ vus: number;
14
+ over?: number;
15
+ }
16
+
17
+ /** A single spike to `peak` VUs at `at` (ms into the run). */
18
+ export interface SpikeProfile {
19
+ kind: "spike";
20
+ peak: number;
21
+ at?: number;
22
+ }
23
+
24
+ /** Whatever a builder produced. `kind` tells the runner which shape it holds. */
25
+ export type LoadProfile = RampProfile | ConstantProfile | SpikeProfile;
@@ -0,0 +1,20 @@
1
+ import type { LoadMetrics } from "../metrics/index.js";
2
+ import { type LoadProfile, peakVus } from "../profiles/index.js";
3
+ import type { LoadRunner } from "./load-runner.types.js";
4
+
5
+ /**
6
+ * The offline `LoadRunner`. Derives plausible metrics from the profile's peak
7
+ * concurrency and sends no traffic.
8
+ *
9
+ * @returns a runner whose results are deterministic for a given profile.
10
+ */
11
+ export function createFakeLoadRunner(): LoadRunner {
12
+ return {
13
+ run: async (profile) => metricsFor(profile),
14
+ };
15
+ }
16
+
17
+ function metricsFor(profile: LoadProfile): LoadMetrics {
18
+ const vus = peakVus(profile);
19
+ return { vus, rps: vus * 10, p50: 50, p95: 120, p99: 200, errorRate: 0.01 };
20
+ }
@@ -0,0 +1,4 @@
1
+ export { createFakeLoadRunner } from "./fake-runner.js";
2
+ export { LoadRunnerPort } from "./load-runner.port.js";
3
+ export type { LoadRunner } from "./load-runner.types.js";
4
+ export { createRealLoadRunner } from "./real-runner.js";
@@ -0,0 +1,13 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { LoadRunner } from "./load-runner.types.js";
3
+
4
+ /**
5
+ * The port `load.run` drives traffic through. Requires the `net` capability, so
6
+ * a host without it fails to load the plugin rather than failing mid-run.
7
+ */
8
+ export const LoadRunnerPort: Port<LoadRunner> = {
9
+ id: "venn.port.load-runner",
10
+ version: 1,
11
+ requires: ["net"],
12
+ methods: ["run"],
13
+ };
@@ -0,0 +1,13 @@
1
+ import type { LoadMetrics } from "../metrics/index.js";
2
+ import type { LoadProfile } from "../profiles/index.js";
3
+
4
+ /** What turns a profile into traffic. */
5
+ export interface LoadRunner {
6
+ /**
7
+ * Executes one profile to completion.
8
+ *
9
+ * @returns the metrics observed, with `p50 <= p95 <= p99`.
10
+ * @throws {VennError} `VN8090` from an implementation that cannot run.
11
+ */
12
+ run(profile: LoadProfile): Promise<LoadMetrics>;
13
+ }
@@ -0,0 +1,22 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import type { LoadRunner } from "./load-runner.types.js";
3
+
4
+ /**
5
+ * The traffic-generating `LoadRunner`. Not wired up in this build.
6
+ *
7
+ * @returns a runner whose `run` throws, so the gap is a legible failure rather
8
+ * than metrics nobody measured.
9
+ * @throws {VennError} `VN8090` on every `run`.
10
+ */
11
+ export function createRealLoadRunner(): LoadRunner {
12
+ return {
13
+ run: async () => notImplemented(),
14
+ };
15
+ }
16
+
17
+ function notImplemented(): never {
18
+ throw new VennError({
19
+ code: "VN8090",
20
+ message: "The load runner is not implemented in this build.",
21
+ });
22
+ }
package/src/types.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The types `@venn-lang/load` publishes to the checker, under the `load` namespace:
5
+ * the three profiles its builders return, the union `load.run` accepts, and the
6
+ * metrics it yields. They mirror `profiles/load-profile.types.ts` and
7
+ * `metrics/load-metrics.types.ts` field by field, dropping the `Load` and
8
+ * `Profile` affixes the namespace already says: a flow reads `load.Ramp`.
9
+ *
10
+ * Durations are `number` here on purpose. The `Duration` schema accepts `"30s"`
11
+ * or a plain millisecond count, not the language's own `duration` value, and
12
+ * yields milliseconds. What a profile holds is therefore a count.
13
+ */
14
+ export const loadTypeDefs: Readonly<Record<string, TypeSpec>> = {
15
+ /** A ramp from `from` to `to` VUs over `over`, then held for `hold`. */
16
+ Ramp: t.record(
17
+ { kind: t.literal("ramp"), from: t.number, to: t.number, over: t.number, hold: t.number },
18
+ { optional: ["over", "hold"] },
19
+ ),
20
+ /** A constant `vus` load sustained over `over`. */
21
+ Constant: t.record(
22
+ { kind: t.literal("constant"), vus: t.number, over: t.number },
23
+ { optional: ["over"] },
24
+ ),
25
+ /** A single spike to `peak` VUs at `at` into the run. */
26
+ Spike: t.record({ kind: t.literal("spike"), peak: t.number, at: t.number }, { optional: ["at"] }),
27
+ /** Whatever a builder produced. This is what `load.run` takes. */
28
+ Profile: t.union(t.ref("load.Ramp"), t.ref("load.Constant"), t.ref("load.Spike")),
29
+ /** What a run yields. The invariant `p50 <= p95 <= p99` always holds. */
30
+ Metrics: t.record({
31
+ vus: t.number,
32
+ rps: t.number,
33
+ p50: t.number,
34
+ p95: t.number,
35
+ p99: t.number,
36
+ errorRate: t.number,
37
+ }),
38
+ };